diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 78c407cde1..93a2b8c3f4 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -29,6 +29,15 @@ jobs: name: linting-report path: | */build/reports/**/*.* + - name: Check Kover rules + run: ./gradlew koverMergedVerify $CI_GRADLE_ARG_PROPERTIES + - name: Upload reports + if: failure() + uses: actions/upload-artifact@v3 + with: + name: kover-report + path: | + **/kover/merged/verification/errors.txt - name: Prepare Danger if: always() run: | diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f4a12c9ec2..19b3192e68 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,10 +21,7 @@ jobs: cancel-in-progress: true steps: - uses: actions/checkout@v3 - - name: Run tests - run: ./gradlew test $CI_GRADLE_ARG_PROPERTIES - - name: Generate kover report - if: always() + - name: Run tests and generate kover report run: ./gradlew koverMergedReport $CI_GRADLE_ARG_PROPERTIES - name: Archive kover report @@ -39,7 +36,7 @@ jobs: if: failure() uses: actions/upload-artifact@v3 with: - name: screenshot-results + name: tests-and-screenshot-tests-results path: | **/out/failures/ **/build/reports/tests/*UnitTest/ diff --git a/.github/workflows/triage-labelled.yml b/.github/workflows/triage-labelled.yml index 74bcc29d97..16d5d5637d 100644 --- a/.github/workflows/triage-labelled.yml +++ b/.github/workflows/triage-labelled.yml @@ -28,3 +28,27 @@ jobs: env: PROJECT_ID: "PVT_kwDOAM0swc4ABTXY" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + + ex_plorers: + name: Add labelled issues to X-Plorer project + runs-on: ubuntu-latest + if: > + contains(github.event.issue.labels.*.name, 'Team: Element X Feature') + steps: + - uses: octokit/graphql-action@v2.x + id: add_to_project + with: + headers: '{"GraphQL-Features": "projects_next_graphql"}' + query: | + mutation add_to_project($projectid:ID!,$contentid:ID!) { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { + id + } + } + } + projectid: ${{ env.PROJECT_ID }} + contentid: ${{ github.event.issue.node_id }} + env: + PROJECT_ID: "PVT_kwDOAM0swc4ALoFY" + GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} diff --git a/.gitignore b/.gitignore index 01fece71fb..47c0f349c8 100644 --- a/.gitignore +++ b/.gitignore @@ -37,17 +37,22 @@ captures/ # IntelliJ *.iml -.idea/workspace.xml -.idea/tasks.xml -.idea/gradle.xml +.idea/.name .idea/assetWizardSettings.xml +.idea/compiler.xml +.idea/gradle.xml +.idea/jarRepositories.xml +.idea/misc.xml +.idea/modules.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml +.idea/tasks.xml +.idea/workspace.xml .idea/dictionaries .idea/libraries # Android Studio 3 in .gitignore file. .idea/caches -.idea/modules.xml -# Comment next line if keeping position of elements in Navigation Editor is relevant for you -.idea/navEditor.xml +.idea/inspectionProfiles # Keystore files # Uncomment the following lines if you do not want to check your keystore files in. diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d33521af..0000000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index 6a4bf02127..0000000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -ElementX \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml deleted file mode 100644 index 74709d9df5..0000000000 --- a/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,125 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index 79ee123c2b..0000000000 --- a/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index 7d1c62f24e..0000000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/icon.png b/.idea/icon.png new file mode 100644 index 0000000000..6f7872211b Binary files /dev/null and b/.idea/icon.png differ diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index ed76bea38e..0000000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml deleted file mode 100644 index d2ce72d10e..0000000000 --- a/.idea/jarRepositories.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 55ded342f7..0000000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/AUTHORS.md b/AUTHORS.md new file mode 100644 index 0000000000..89404cd73f --- /dev/null +++ b/AUTHORS.md @@ -0,0 +1,17 @@ +A full developer contributors list can be found [here](https://github.com/vector-im/element-x-android/graphs/contributors). + +# Core team: + +The element.io Android developer team. + +# Other contributors + +First of all, we thank all contributors who use Element and report problems on this GitHub project or via the integrated rageshake function. + +We do not forget all translators, for their work of translating Element into many languages. They are also the authors of Element. + +Feel free to add your name below, when you contribute to the project! + +Name | Matrix ID | GitHub +----------|-----------------------------|-------------------------------------- +name | @name:matrix.org | [githubID](https://github.com/githubID) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..5cd8785b33 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,158 @@ +# Contributing to Element Android + + + +* [Contributing code to Matrix](#contributing-code-to-matrix) +* [Android Studio settings](#android-studio-settings) +* [Compilation](#compilation) +* [I want to help translating Element](#i-want-to-help-translating-element) +* [I want to submit a PR to fix an issue](#i-want-to-submit-a-pr-to-fix-an-issue) + * [Kotlin](#kotlin) + * [Changelog](#changelog) + * [Code quality](#code-quality) + * [ktlint](#ktlint) + * [knit](#knit) + * [lint](#lint) + * [Unit tests](#unit-tests) + * [Tests](#tests) + * [Internationalisation](#internationalisation) + * [Accessibility](#accessibility) + * [Jetpack Compose](#jetpack-compose) + * [Authors](#authors) +* [Thanks](#thanks) + + + +## Contributing code to Matrix + +Please read https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.md + +Element X Android support can be found in this room: [![Element Android Matrix room #element-android:matrix.org](https://img.shields.io/matrix/element-android:matrix.org.svg?label=%23element-android:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#element-android:matrix.org). + +The rest of the document contains specific rules for Matrix Android projects + +## Android Studio settings + +Please set the "hard wrap" setting of Android Studio to 160 chars, this is the setting we use internally to format the source code (Menu `Settings/Editor/Code Style` then `Hard wrap at`). +Please ensure that you're using the project formatting rules (which are in the project at .idea/codeStyles/), and format the file before committing them. + +## Compilation + +This project should compile without any special action. Just clone it and open it with Android Studio, or compile from command line using `gradlew`. + +## I want to help translating Element + +For now strings are coming from Element Android project, so: +- If you want to fix an issue with an English string, please submit a PR on Element Android. +- If you want to fix an issue in other languages, or add a missing translation, or even add a new language, please use [Weblate](https://translate.element.io/projects/element-android/). + +## I want to submit a PR to fix an issue + +Please have a look in the [dedicated documentation](./docs/pull_request.md) about pull request. + +Please check if a corresponding issue exists. If yes, please let us know in a comment that you're working on it. +If an issue does not exist yet, it may be relevant to open a new issue and let us know that you're implementing it. + +### Kotlin + +This project is full Kotlin. Please do not write Java classes. + +### Changelog + +Please create at least one file under ./changelog.d 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. +- ``.wip``: Signifying a work in progress change, typically a component of a larger feature which will be enabled once all tasks are complete. +- ``.doc``: Signifying a documentation improvement. +- ``.misc``: Any other changes. + +See https://github.com/twisted/towncrier#news-fragments if you need more details. + +### Code quality + +Make sure the following commands execute without any error: + +
+./gradlew check
+
+ +Some separate commands can also be run, see below. + +#### ktlint + +
+./gradlew ktlintCheck --continue
+
+ +Note that you can run + +
+./gradlew ktlintFormat
+
+ +For ktlint to fix some detected errors for you (you still have to check and commit the fix of course) + +#### knit + +[knit](https://github.com/Kotlin/kotlinx-knit) is a tool which checks markdown files on the project. Also it generates/updates the table of content (toc) of the markdown files. + +So everytime the toc should be updated, just run +
+./gradlew knit
+
+ +and commit the changes. + +The CI will check that markdown files are up to date by running + +
+./gradlew knitCheck
+
+ +#### lint + +
+./gradlew lint
+
+ +### Unit tests + +Make sure the following commands execute without any error: + +
+./gradlew test
+
+ +### Tests + +Element X is currently supported on Android Lollipop (API 21+): please test your change on an Android device (or Android emulator) running with API 21. Many issues can happen (including crashes) on older devices. +Also, if possible, please test your change on a real device. Testing on Android emulator may not be sufficient. + +You should consider adding Unit tests with your PR, and also integration tests (AndroidTest). Please refer to [this document](./docs/integration_tests.md) to install and run the integration test environment. + +### Internationalisation + +For now strings are coming from Element Android project, so please read [the documentation](https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md#internationalisation) from there. + +### Accessibility + +Please consider accessibility as an important point. As a minimum requirement, in layout XML files please use attributes such as `android:contentDescription` and `android:importantForAccessibility`, and test with a screen reader if it's working well. You can add new string resources, dedicated to accessibility, in this case, please prefix theirs id with `a11y_`. + +For instance, when updating the image `src` of an ImageView, please also consider updating its `contentDescription`. A good example is a play pause button. + +### Jetpack Compose + +When adding or editing `@Composable`, make sure that you create a `@Preview` function, with suffix `Preview`. This will also create a UI test automatically. + +### Authors + +Feel free to add an entry in file AUTHORS.md + +## Thanks + +Thanks for contributing to Matrix projects! diff --git a/README.md b/README.md index 5e5227c17b..8963525631 100644 --- a/README.md +++ b/README.md @@ -6,30 +6,63 @@ [![Element Android Matrix room #element-android:matrix.org](https://img.shields.io/matrix/element-android:matrix.org.svg?label=%23element-android:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#element-android:matrix.org) [![Weblate](https://translate.element.io/widgets/element-android/-/svg-badge.svg)](https://translate.element.io/engage/element-android/?utm_source=widget) -# element-x-android-poc +# element-x-android -Proof Of Concept to run a Matrix client on Android devices using the Matrix Rust Sdk and Jetpack compose. +ElementX Android is a [Matrix](https://matrix.org/) Android Client provided by [Element](https://element.io/). -The plan is [here](https://github.com/vector-im/element-x-android-poc/issues/1)! +The application is a total rewrite of [Element-Android](https://github.com/vector-im/element-android) using the [Matrix Rust SDK](https://github.com/matrix-org/matrix-rust-sdk) underneath and targeting devices running Android 5+. The UI layer is written using Jetpack compose. + -### Modules +* [Screenshots](#screenshots) +* [Rust SDK](#rust-sdk) +* [Roadmap](#roadmap) +* [Contributing](#contributing) +* [Build instructions](#build-instructions) +* [Support](#support) +* [Copyright & License](#copyright-&-license) -This Android project is a multi modules project. + -- `app` module is the Android application module. Other modules are libraries; -- `features` modules contain some UI and can be seen as screen of the application; -- `libraries` modules contain classes that can be useful for other modules to work. +## Screenshots -A few details about some modules: +Here are some early screenshots of the application: -- `libraries-core` module contains utility classes; -- `libraries-designsystem` module contains Composables which can be used across the app (theme, etc.); -- `libraries-elementresources` module contains resource from Element Android (mainly strings); -- `libraries-matrix` module contains wrappers around the Matrix Rust SDK. +||||| +|-|-|-|-| -Here is the current module dependency graph: +## Rust SDK - - +ElementX leverages the [Matrix Rust SDK](https://github.com/matrix-org/matrix-rust-sdk) through an FFI layer that the final client can directly import and use. +We're doing this as a way to share code between platforms and while we've seen promising results it's still in the experimental stage and bound to change. + +## Roadmap + +We are aiming to have a fast and fully functional personal messaging application by the end of year 2023. + +## Contributing + +Please see our [contribution guide](CONTRIBUTING.md). + +Come chat with the community in the dedicated Matrix [room](https://matrix.to/#/#element-android:matrix.org). + +## Build instructions + +Just clone the project and open it in Android Studio. + +## Support + +When you are experiencing an issue on ElementX Android, please first search in [GitHub issues](https://github.com/vector-im/element-x-android/issues) +and then in [#element-android:matrix.org](https://matrix.to/#/#element-android:matrix.org). +If after your research you still have a question, ask at [#element-android:matrix.org](https://matrix.to/#/#element-android:matrix.org). Otherwise feel free to create a GitHub issue if you encounter a bug or a crash, by explaining clearly in detail what happened. You can also perform bug reporting (Rageshake) from the Element application by shaking your phone or going to the application settings. This is especially recommended when you encounter a crash. + +## Copyright & License + +Copyright (c) 2022 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with the License. You may obtain a copy of the License in the [LICENSE](LICENSE) file, or at: + +[http://www.apache.org/licenses/LICENSE-2.0](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. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fdae68aef4..a5baf3a1fa 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -162,4 +162,11 @@ dependencies { implementation(libs.dagger) kapt(libs.dagger.compiler) + + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(projects.libraries.matrixtest) } diff --git a/app/src/main/kotlin/io/element/android/x/MainActivity.kt b/app/src/main/kotlin/io/element/android/x/MainActivity.kt index ebd9494017..c37a35476d 100644 --- a/app/src/main/kotlin/io/element/android/x/MainActivity.kt +++ b/app/src/main/kotlin/io/element/android/x/MainActivity.kt @@ -18,16 +18,15 @@ package io.element.android.x import android.os.Bundle import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface import androidx.compose.ui.Modifier import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat import com.bumble.appyx.core.integration.NodeHost import com.bumble.appyx.core.integrationpoint.NodeComponentActivity import io.element.android.libraries.architecture.bindings -import io.element.android.libraries.designsystem.ElementXTheme +import io.element.android.libraries.designsystem.theme.ElementTheme import io.element.android.libraries.di.DaggerComponentOwner import io.element.android.x.di.AppBindings import io.element.android.x.node.RootFlowNode @@ -41,10 +40,9 @@ class MainActivity : NodeComponentActivity() { appBindings.matrixClientsHolder().restore(savedInstanceState) WindowCompat.setDecorFitsSystemWindows(window, false) setContent { - ElementXTheme { - Surface( + ElementTheme { + Box( modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background ) { NodeHost(integrationPoint = appyxIntegrationPoint) { RootFlowNode( diff --git a/app/src/main/kotlin/io/element/android/x/component/ShowkaseButton.kt b/app/src/main/kotlin/io/element/android/x/component/ShowkaseButton.kt index 689b46dcdc..c486f9b7cc 100644 --- a/app/src/main/kotlin/io/element/android/x/component/ShowkaseButton.kt +++ b/app/src/main/kotlin/io/element/android/x/component/ShowkaseButton.kt @@ -20,13 +20,13 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.theme.components.Text @Composable internal fun ShowkaseButton( diff --git a/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt b/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt index de8b29682e..94b93b8fd5 100644 --- a/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt +++ b/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt @@ -20,7 +20,6 @@ import com.squareup.anvil.annotations.ContributesTo import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.auth.MatrixAuthenticationService import io.element.android.x.root.RootPresenter -import kotlinx.coroutines.CoroutineScope @ContributesTo(AppScope::class) interface AppBindings { diff --git a/app/src/main/kotlin/io/element/android/x/di/AppModule.kt b/app/src/main/kotlin/io/element/android/x/di/AppModule.kt index 7cb3fb55c3..b562abe93e 100644 --- a/app/src/main/kotlin/io/element/android/x/di/AppModule.kt +++ b/app/src/main/kotlin/io/element/android/x/di/AppModule.kt @@ -20,6 +20,8 @@ import android.content.Context import com.squareup.anvil.annotations.ContributesTo import dagger.Module import dagger.Provides +import io.element.android.features.rageshake.reporter.BugReporter +import io.element.android.features.rageshake.reporter.DefaultBugReporter import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext @@ -58,4 +60,7 @@ object AppModule { diffUpdateDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() ) } + + @Provides + fun providesBugReporter(bugReporter: DefaultBugReporter): BugReporter = bugReporter } diff --git a/app/src/main/kotlin/io/element/android/x/node/LoggedInFlowNode.kt b/app/src/main/kotlin/io/element/android/x/node/LoggedInFlowNode.kt index b2e2aca077..10e00d30ef 100644 --- a/app/src/main/kotlin/io/element/android/x/node/LoggedInFlowNode.kt +++ b/app/src/main/kotlin/io/element/android/x/node/LoggedInFlowNode.kt @@ -19,7 +19,6 @@ package io.element.android.x.node import android.os.Parcelable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -37,6 +36,7 @@ import io.element.android.features.roomlist.RoomListNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.di.DaggerComponentOwner import io.element.android.libraries.matrix.MatrixClient import io.element.android.libraries.matrix.core.RoomId diff --git a/app/src/main/kotlin/io/element/android/x/node/RootFlowNode.kt b/app/src/main/kotlin/io/element/android/x/node/RootFlowNode.kt index ae3c31008d..37fd284b3f 100644 --- a/app/src/main/kotlin/io/element/android/x/node/RootFlowNode.kt +++ b/app/src/main/kotlin/io/element/android/x/node/RootFlowNode.kt @@ -19,7 +19,6 @@ package io.element.android.x.node import android.os.Parcelable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -36,6 +35,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push import io.element.android.features.rageshake.bugreport.BugReportNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.di.DaggerComponentOwner import io.element.android.libraries.matrix.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.core.SessionId diff --git a/app/src/test/kotlin/io/element/android/x/root/FakeBugReporter.kt b/app/src/test/kotlin/io/element/android/x/root/FakeBugReporter.kt new file mode 100644 index 0000000000..c6f7000cdf --- /dev/null +++ b/app/src/test/kotlin/io/element/android/x/root/FakeBugReporter.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2023 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 io.element.android.x.root + +import io.element.android.features.rageshake.reporter.BugReporter +import io.element.android.features.rageshake.reporter.BugReporterListener +import io.element.android.features.rageshake.reporter.ReportType +import io.element.android.libraries.matrixtest.A_FAILURE_REASON +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +// TODO Remove this duplicated class when we will rework modules. +class FakeBugReporter(val mode: FakeBugReporterMode = FakeBugReporterMode.Success) : BugReporter { + override fun sendBugReport( + coroutineScope: CoroutineScope, + reportType: ReportType, + withDevicesLogs: Boolean, + withCrashLogs: Boolean, + withKeyRequestHistory: Boolean, + withScreenshot: Boolean, + theBugDescription: String, + serverVersion: String, + canContact: Boolean, + customFields: Map?, + listener: BugReporterListener?, + ) { + coroutineScope.launch { + delay(100) + listener?.onProgress(0) + delay(100) + listener?.onProgress(50) + delay(100) + when (mode) { + FakeBugReporterMode.Success -> Unit + FakeBugReporterMode.Failure -> { + listener?.onUploadFailed(A_FAILURE_REASON) + return@launch + } + FakeBugReporterMode.Cancel -> { + listener?.onUploadCancelled() + return@launch + } + } + listener?.onProgress(100) + delay(100) + listener?.onUploadSucceed(null) + } + } +} + +enum class FakeBugReporterMode { + Success, + Failure, + Cancel +} diff --git a/app/src/test/kotlin/io/element/android/x/root/FakeCrashDataStore.kt b/app/src/test/kotlin/io/element/android/x/root/FakeCrashDataStore.kt new file mode 100644 index 0000000000..e6ede5ecc0 --- /dev/null +++ b/app/src/test/kotlin/io/element/android/x/root/FakeCrashDataStore.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 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 io.element.android.x.root + +import io.element.android.features.rageshake.crash.CrashDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +const val A_CRASH_DATA = "Some crash data" + +// TODO Remove this duplicated class when we will rework modules. + +class FakeCrashDataStore( + crashData: String = "", + appHasCrashed: Boolean = false, +) : CrashDataStore { + private val appHasCrashedFlow = MutableStateFlow(appHasCrashed) + private val crashDataFlow = MutableStateFlow(crashData) + + override fun setCrashData(crashData: String) { + crashDataFlow.value = crashData + } + + override suspend fun resetAppHasCrashed() { + appHasCrashedFlow.value = false + } + + override fun appHasCrashed(): Flow = appHasCrashedFlow + + override fun crashInfo(): Flow = crashDataFlow + + override suspend fun reset() { + appHasCrashedFlow.value = false + crashDataFlow.value = "" + } +} diff --git a/app/src/test/kotlin/io/element/android/x/root/FakeRageShake.kt b/app/src/test/kotlin/io/element/android/x/root/FakeRageShake.kt new file mode 100644 index 0000000000..9a260d9859 --- /dev/null +++ b/app/src/test/kotlin/io/element/android/x/root/FakeRageShake.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 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 io.element.android.x.root + +import io.element.android.features.rageshake.rageshake.RageShake + +// TODO Remove this duplicated class when we will rework modules. +class FakeRageShake( + private var isAvailableValue: Boolean = true +) : RageShake { + + private var interceptor: (() -> Unit)? = null + + override fun isAvailable() = isAvailableValue + + override fun start(sensitivity: Float) { + } + + override fun stop() { + } + + override fun setSensitivity(sensitivity: Float) { + } + + override fun setInterceptor(interceptor: (() -> Unit)?) { + this.interceptor = interceptor + } + + fun triggerPhoneRageshake() = interceptor?.invoke() +} diff --git a/app/src/test/kotlin/io/element/android/x/root/FakeRageshakeDataStore.kt b/app/src/test/kotlin/io/element/android/x/root/FakeRageshakeDataStore.kt new file mode 100644 index 0000000000..e8521fb74f --- /dev/null +++ b/app/src/test/kotlin/io/element/android/x/root/FakeRageshakeDataStore.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 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 io.element.android.x.root + +import io.element.android.features.rageshake.rageshake.RageshakeDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +const val A_SENSITIVITY = 1f + +// TODO Remove this duplicated class when we will rework modules. +class FakeRageshakeDataStore( + isEnabled: Boolean = true, + sensitivity: Float = A_SENSITIVITY, +) : RageshakeDataStore { + + private val isEnabledFlow = MutableStateFlow(isEnabled) + override fun isEnabled(): Flow = isEnabledFlow + + override suspend fun setIsEnabled(isEnabled: Boolean) { + isEnabledFlow.value = isEnabled + } + + private val sensitivityFlow = MutableStateFlow(sensitivity) + override fun sensitivity(): Flow = sensitivityFlow + + override suspend fun setSensitivity(sensitivity: Float) { + sensitivityFlow.value = sensitivity + } + + override suspend fun reset() = Unit +} diff --git a/app/src/test/kotlin/io/element/android/x/root/FakeScreenshotHolder.kt b/app/src/test/kotlin/io/element/android/x/root/FakeScreenshotHolder.kt new file mode 100644 index 0000000000..3a44ece6a2 --- /dev/null +++ b/app/src/test/kotlin/io/element/android/x/root/FakeScreenshotHolder.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 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 io.element.android.x.root + +import android.graphics.Bitmap +import io.element.android.features.rageshake.screenshot.ScreenshotHolder + +const val A_SCREENSHOT_URI = "file://content/uri" + +// TODO Remove this duplicated class when we will rework modules. +class FakeScreenshotHolder(private val screenshotUri: String? = null) : ScreenshotHolder { + override fun writeBitmap(data: Bitmap) = Unit + + override fun getFileUri() = screenshotUri + + override fun reset() = Unit +} diff --git a/app/src/test/kotlin/io/element/android/x/root/RootPresenterTest.kt b/app/src/test/kotlin/io/element/android/x/root/RootPresenterTest.kt new file mode 100644 index 0000000000..3f5a18d567 --- /dev/null +++ b/app/src/test/kotlin/io/element/android/x/root/RootPresenterTest.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2023 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:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.x.root + +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.rageshake.bugreport.BugReportPresenter +import io.element.android.features.rageshake.crash.ui.CrashDetectionPresenter +import io.element.android.features.rageshake.detection.RageshakeDetectionPresenter +import io.element.android.features.rageshake.preferences.RageshakePreferencesPresenter +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class RootPresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = createPresenter() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.isShowkaseButtonVisible).isTrue() + } + } + + @Test + fun `present - hide showkase button`() = runTest { + val presenter = createPresenter() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.isShowkaseButtonVisible).isTrue() + initialState.eventSink.invoke(RootEvents.HideShowkaseButton) + assertThat(awaitItem().isShowkaseButtonVisible).isFalse() + } + } + + private fun TestScope.createPresenter(): RootPresenter { + val crashDataStore = FakeCrashDataStore() + val rageshakeDataStore = FakeRageshakeDataStore() + val rageshake = FakeRageShake() + val screenshotHolder = FakeScreenshotHolder() + val bugReportPresenter = BugReportPresenter( + bugReporter = FakeBugReporter(), + crashDataStore = crashDataStore, + screenshotHolder = screenshotHolder, + appCoroutineScope = this, + ) + val crashDetectionPresenter = CrashDetectionPresenter( + crashDataStore = crashDataStore + ) + val rageshakeDetectionPresenter = RageshakeDetectionPresenter( + screenshotHolder = screenshotHolder, + rageShake = rageshake, + preferencesPresenter = RageshakePreferencesPresenter( + rageshake = rageshake, + rageshakeDataStore = rageshakeDataStore, + ) + ) + return RootPresenter( + bugReportPresenter = bugReportPresenter, + crashDetectionPresenter = crashDetectionPresenter, + rageshakeDetectionPresenter = rageshakeDetectionPresenter, + ) + } +} diff --git a/build.gradle.kts b/build.gradle.kts index f2f16714ea..0869ecc6ca 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -155,6 +155,7 @@ allprojects { apply(plugin = "kover") } +// https://kotlin.github.io/kotlinx-kover/ // Run `./gradlew koverMergedHtmlReport` to get report at ./build/reports/kover // Run `./gradlew koverMergedReport` to also get XML report koverMerged { @@ -164,14 +165,70 @@ koverMerged { classes { excludes.addAll( listOf( - /* - "*Fragment", - "*Fragment\$*", - "*Activity", - "*Activity\$*", - */ + // Exclude generated classes. + "*_ModuleKt", + "anvil.hint.binding.io.element.*", + "anvil.hint.merge.*", + "anvil.module.*", + "com.airbnb.android.showkase*", + "*_Factory", + "*_Factory$*", + "*_Module", + "*_Module$*", + "*ComposableSingletons$*", + "*_AssistedFactory_Impl*", + "*BuildConfig", + // Other + // We do not cover Nodes (normally covered by maestro, but code coverage is not computed with maestro) + "*Node", + "*Node$*", ) ) } } + + // Run ./gradlew koverMergedVerify to check the rules. + verify { + // Does not seems to work, so also run the task manually on the workflow. + onCheck.set(true) + // General rule: minimum code coverage. + rule { + name = "Global minimum code coverage." + target = kotlinx.kover.api.VerificationTarget.ALL + bound { + minValue = 45 + // Setting a max value, so that if coverage is bigger, it means that we have to change minValue. + maxValue = 50 + counter = kotlinx.kover.api.CounterType.LINE + valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE + } + } + // Rule to ensure that coverage of Presenters is sufficient. + rule { + name = "Check code coverage of presenters" + target = kotlinx.kover.api.VerificationTarget.CLASS + overrideClassFilter { + includes += "*Presenter" + excludes += "*TemplatePresenter" + } + bound { + minValue = 90 + counter = kotlinx.kover.api.CounterType.INSTRUCTION + valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE + } + } + // Rule to ensure that coverage of State is sufficient. + rule { + name = "Check code coverage of states" + target = kotlinx.kover.api.VerificationTarget.CLASS + overrideClassFilter { + includes += "*State" + } + bound { + minValue = 90 + counter = kotlinx.kover.api.CounterType.INSTRUCTION + valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE + } + } + } } diff --git a/docs/_developer_onboarding.md b/docs/_developer_onboarding.md new file mode 100644 index 0000000000..14edae584a --- /dev/null +++ b/docs/_developer_onboarding.md @@ -0,0 +1,235 @@ +# Developer on boarding + + + +* [Introduction](#introduction) + * [Quick introduction to Matrix](#quick-introduction-to-matrix) + * [Matrix data](#matrix-data) + * [Room](#room) + * [Event](#event) + * [Sync](#sync) + * [The Android project](#the-android-project) + * [Application](#application) + * [Jetpack Compose](#jetpack-compose) + * [Global architecture](#global-architecture) + * [Template](#template) + * [Push](#push) + * [Dependencies management](#dependencies-management) + * [Test](#test) + * [Other points](#other-points) + * [Logging](#logging) + * [Rageshake](#rageshake) + * [Tips](#tips) +* [Happy coding!](#happy-coding) + + + +## Introduction + +This doc is a quick introduction about the project and its architecture. + +It's aim is to help new developers to understand the overall project and where to start developing. + +Other useful documentation: +- all the docs in this folder! +- the [contributing doc](../CONTRIBUTING.md), that you should also read carefully. + +### Quick introduction to Matrix + +Matrix website: [matrix.org](https://matrix.org), [discover page](https://matrix.org/discover). +*Note*: Matrix.org is also hosting a homeserver ([.well-known file](https://matrix.org/.well-known/matrix/client)). +The reference homeserver (this is how Matrix servers are called) implementation is [Synapse](https://github.com/matrix-org/synapse/). But other implementations exist. The Matrix specification is here to ensure that any Matrix client, such as Element Android and its SDK can talk to any Matrix server. + +Have a quick look to the client-server API documentation: [Client-server documentation](https://spec.matrix.org/v1.3/client-server-api/). Other network API exist, the list is here: (https://spec.matrix.org/latest/) + +Matrix is an open source protocol. Change are possible and are tracked using [this GitHub repository](https://github.com/matrix-org/matrix-doc/). Changes to the protocol are called MSC: Matrix Spec Change. These are PullRequest to this project. + +Matrix object are Json data. Unstable prefixes must be used for Json keys when the MSC is not merged (i.e. accepted). + +#### Matrix data + +There are many object and data in the Matrix worlds. Let's focus on the most important and used, `Room` and `Event` + +##### Room + +`Room` is a place which contains ordered `Event`s. They are identified with their `room_id`. Nearly all the data are stored in rooms, and shared using homeserver to all the Room Member. + +*Note*: Spaces are also Rooms with a different `type`. + +##### Event + +`Events` are items of a Room, where data is embedded. + +There are 2 types of Room Event: + +- Regular Events: contain useful content for the user (message, image, etc.), but are not necessarily displayed as this in the timeline (reaction, message edition, call signaling). +- State Events: contain the state of the Room (name, topic, etc.). They have a non null value for the key `state_key`. + +Also all the Room Member details are in State Events: one State Event per member. In this case, the `state_key` is the matrixId (= userId). + +Important Fields of an Event: +- `event_id`: unique across the Matrix universe; +- `room_id`: the room the Event belongs to; +- `type`: describe what the Event contain, especially in the `content` section, and how the SDK should handle this Event; +- `content`: dynamic Event data; depends on the `type`. + +So we have a triple `event_id`, `type`, `state_key` which uniquely defines an Event. + +#### Sync + +This is managed by the Rust SDK. + +### The Android project + +The project should compile out of the box. + +This Android project is a multi modules project. + +- `app` module is the Android application module. Other modules are libraries; +- `features` modules contain some UI and can be seen as screen or flow of screens of the application; +- `libraries` modules contain classes that can be useful for other modules to work. + +A few details about some modules: + +- `libraries-core` module contains utility classes; +- `libraries-designsystem` module contains Composables which can be used across the app (theme, etc.); +- `libraries-elementresources` module contains resource from Element Android (mainly strings); +- `libraries-matrix` module contains wrappers around the Matrix Rust SDK. + +Most of the time a feature module should not know anything about other feature module. +The navigation glue is currently done in the `app` module. + +Here is the current module dependency graph: + + + + +### Application + +This Android project mainly handle the application layer of the whole software. The communication with the Matrix server, as well as the local storage, the cryptography (encryption and decryption of Event, key management, etc.) is managed by the Rust SDK. + +The application is responsible to store the session credentials though. + +#### Jetpack Compose + +Compose is essentially two libraries : Compose Compiler and Compose UI. The compiler (and his runtime) is actually not specific to UI at all and offer powerful +state management APIs. See https://jakewharton.com/a-jetpack-compose-by-any-other-name/ + +Some useful links: + +- https://developer.android.com/jetpack/compose/mental-model +- https://developer.android.com/jetpack/compose/libraries +- https://developer.android.com/jetpack/compose/modifiers-list +- https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-api-guidelines.md#api-guidelines-for-jetpack-compose + +About Preview + +- https://alexzh.com/jetpack-compose-preview/ + +#### Global architecture + +Main libraries and frameworks used in this application: + +- Navigation state with [Appyx](https://bumble-tech.github.io/appyx/) +- DI: [Dagger](https://dagger.dev/) and [Anvil](https://github.com/square/anvil) +- Reactive State management with Compose runtime and [Molecule](https://github.com/cashapp/molecule) + +Some patterns are inspired by [Circuit](https://slackhq.github.io/circuit/) + +Here are the main points: + +1. `Presenter` and `View` does not communicate with each other directly, but through `State` and `Event` +2. Views are compose first +3. Presenters are also compose first, and have a single `present(): State` method. It's using the power of compose-runtime/compiler. +4. The point of connection between a `View` and a `Presenter` is a `Node`. +5. A `Node` is also responsible for managing Dagger components if any. +6. A `ParentNode` has some child `Node` and only know about them. +7. This is a single activity full compose application. The `MainActivity` is responsible for holding and configuring the `RootNode`. +8. There is no more needs for Android Architecture Component ViewModel as configuration change should be handled by Composable if needed. + +#### Template + +(TODO: This is coming) +There is a template module to easily start a new feature. When creating a new module, you can just copy paste the template. + +### Push + +**Note** Firebase Push is not yet implemented on the project. + +Please see the dedicated [documentation](notifications.md) for more details. + +This is the classical scenario: + +- App receives a Push. Note: Push is ignored if app is in foreground; +- App asks the SDK to load Event data (fastlane mode). We have a change to get the data faster and display the notification faster; +- App asks the SDK to perform a sync request. + +### Dependencies management + +We are using [Gradle version catalog](https://docs.gradle.org/current/userguide/platforms.html#sub:central-declaration-of-dependencies) on this project. + +All the dependencies (including android artifact, gradle plugin, etc.) should be declared in [../gradle/libs.versions.toml](libs.versions.toml) file. +Some dependency, mainly because they are not shared can be declared in `build.gradle.kts` files. + +[Dependabot](https://github.com/dependabot) is set up on the project. This tool will automatically create Pull Request to upgrade our dependencies one by one. +**Note** Dependabot does not support yet Gradle verrsion catalog. This is tracked by [this issue](https://github.com/dependabot/dependabot-core/issues/3121). + +### Test + +We have 3 tests frameworks in place: + +- Maestro to test the global usage of the application. See the related [documentation](../.maestro/README.md). +- Combination of [Showkase](https://github.com/airbnb/Showkase) and [Paparazzi](https://github.com/cashapp/paparazzi), to test UI pixel perfect. To add test, just add `@Preview` for the composable you are adding. See the related [documentation](screenshot_testing.md). +- Tests on presenter with Molecule and [Turbine](https://github.com/cashapp/turbine) (TODO this is coming) + +**Note** For now we want to avoid using mock (such as *mockk*), because this should be note necessary. + +### Other points + +#### Logging + +**Important warning: ** NEVER log private user data, or use the flag `LOG_PRIVATE_DATA`. Be very careful when logging `data class`, all the content will be output! + +[Timber](https://github.com/JakeWharton/timber) is used to log data to logcat. We do not use directly the `Log` class. If possible please use a tag, as per + +````kotlin +Timber.tag(loggerTag.value).d("my log") +```` + +because automatic tag (= class name) will not be available on the release version. + +Also generally it is recommended to provide the `Throwable` to the Timber log functions. + +Last point, note that `Timber.v` function may have no effect on some devices. Prefer using `Timber.d` and up. + +#### Rageshake + +Rageshake is a feature to send bug report directly from the application. Just shake your phone and you will be prompted to send a bug report. + +Bug reports can contain: + +- a screenshot of the current application state +- the application logs from up to 15 application starts +- the logcat logs + +The data will be sent to an internal server, which is not publicly accessible. A GitHub issue will also be created to a private GitHub repository. + +Rageshake can be very useful to get logs from a release version of the application. + +### Tips + +- Element Android has a `developer mode` in the `Settings/Advanced settings`. Other useful options are available here; (TODO Not supported yet!) +- Show hidden Events can also help to debug feature. When developer mode is enabled, it is possible to view the source (= the Json content) of any Events; (TODO Not supported yet!) +- Type `/devtools` in a Room composer to access a developer menu. There are some other entry points. Developer mode has to be enabled; (TODO Not supported yet!) +- Hidden debug menu: when developer mode is enabled and on debug build, there are some extra screens that can be accessible using the green wheel. In those screens, it will be possible to toggle some feature flags; (TODO Not supported yet!) +- Using logcat, filtering with `Compositions` can help you to understand what screen are currently displayed on your device. Searching for string displayed on the screen can also help to find the running code in the codebase. +- When this is possible, prefer using `sealed interface` instead of `sealed class`; +- When writing temporary code, using the string "DO NOT COMMIT" in a comment can help to avoid committing things by mistake. If committed and pushed, the CI will detect this String and will warn the user about it. (TODO Not supported yet!) + +## Happy coding! + +The team is here to support you, feel free to ask anything to other developers. + +Also please feel to update this documentation, if incomplete/wrong/obsolete/etc. + +**Thanks!** diff --git a/docs/analytics.md b/docs/analytics.md new file mode 100644 index 0000000000..b3f592c227 --- /dev/null +++ b/docs/analytics.md @@ -0,0 +1,11 @@ +# Analytics in Element + + + +* [TODO](#todo) + + + +## TODO + +There is no analytics in the project yet. diff --git a/docs/danger.md b/docs/danger.md new file mode 100644 index 0000000000..e6fa74dec2 --- /dev/null +++ b/docs/danger.md @@ -0,0 +1,106 @@ +## Danger + + + +* [What does danger checks](#what-does-danger-checks) + * [PR check](#pr-check) + * [Quality check](#quality-check) +* [Setup](#setup) +* [Run danger locally](#run-danger-locally) +* [Danger user](#danger-user) +* [Useful links](#useful-links) + + + +## What does danger checks + +### PR check + +See the [dangerfile](../tools/danger/dangerfile.js). If you add rules in the dangerfile, please update the list below! + +Here are the checks that Danger does so far: + +- PR description is not empty +- Big PR got a warning to recommend to split +- PR contains a file for towncrier and extension is checked +- PR does not modify frozen classes +- PR contains a Sign-Off, with exception for Element employee contributors +- PR with change on layout should include screenshot in the description (TODO Not supported yet!) +- PR which adds png file warn about the usage of vector drawables +- non draft PR should have a reviewer +- files containing translations are not modified by developers + +### Quality check + +After all the checks that generate checkstyle XML report, such as Ktlint, lint, or Detekt, Danger is run with this [dangerfile](../tools/danger/dangerfile-lint.js), in order to post comments to the PR with the detected error and warnings. + +To run locally, you will have to install the plugin `danger-plugin-lint-report` using: + +```shell +yarn add danger-plugin-lint-report --dev +``` + +## Setup + +This operation should not be necessary, since Danger is already setup for the project. + +To setup danger to the project, run: + +```shell +bundle exec danger init +``` + +## Run danger locally + +When modifying the [dangerfile](../tools/danger/dangerfile.js), you can check it by running Danger locally. + +To run danger locally, install it and run: + +```shell +bundle exec danger pr --dangerfile=./tools/danger/dangerfile.js +``` + +For instance: + +```shell +bundle exec danger pr https://github.com/vector-im/element-android/pull/6637 --dangerfile=./tools/danger/dangerfile.js +``` + +We may need to create a GitHub token to have less API rate limiting, and then set the env var: + +```shell +export DANGER_GITHUB_API_TOKEN='YOUR_TOKEN' +``` + +Swift and Kotlin (just in case) + +```shell +bundle exec danger-swift pr --dangerfile=./tools/danger/dangerfile.js +bundle exec danger-kotlin pr --dangerfile=./tools/danger/dangerfile.js +``` + +## Danger user + +To let Danger check all the PRs, including PRs form forks, a GitHub account have been created: +- login: ElementBot +- password: Stored on Passbolt +- GitHub token: A token with limited access has been created and added to the repository https://github.com/vector-im/element-android as secret DANGER_GITHUB_API_TOKEN. This token is not saved anywhere else. In case of problem, just delete it and create a new one, then update the secret. + +PRs from forks do not always have access to the secret `secrets.DANGER_GITHUB_API_TOKEN`, so `secrets.GITHUB_TOKEN` is also provided to the job environment. If `secrets.DANGER_GITHUB_API_TOKEN` is available, it will be used, so user `ElementBot` will comment the PR. Else `secrets.GITHUB_TOKEN` will be used, and bot `github-actions` will comment the PR. + +## Useful links + +- https://danger.systems/ +- https://danger.systems/js/ +- https://danger.systems/js/guides/getting_started.html +- https://danger.systems/js/reference.html +- https://github.com/danger/awesome-danger + +Some danger files to get inspired from + +- https://github.com/artsy/emission/blob/master/dangerfile.ts +- https://github.com/facebook/react-native/blob/master/bots/dangerfile.js +- https://github.com/apollographql/apollo-client/blob/master/config/dangerfile.ts +- https://github.com/styleguidist/react-styleguidist/blob/master/dangerfile.js +- https://github.com/storybooks/storybook/blob/master/dangerfile.js +- https://github.com/ReactiveX/rxjs/blob/master/dangerfile.js diff --git a/docs/design.md b/docs/design.md new file mode 100644 index 0000000000..c46ee0e84d --- /dev/null +++ b/docs/design.md @@ -0,0 +1,143 @@ +# Element Android design + + + +* [Introduction](#introduction) +* [How to import from Figma to the Element Android project](#how-to-import-from-figma-to-the-element-android-project) + * [Colors](#colors) + * [Text](#text) + * [Dimension, position and margin](#dimension-position-and-margin) + * [Icons](#icons) + * [Export drawable from Figma](#export-drawable-from-figma) + * [Import in Android Studio](#import-in-android-studio) + * [Images](#images) +* [Figma links](#figma-links) + * [Coumpound](#coumpound) + * [Login](#login) + * [Login v2](#login-v2) + * [Room list](#room-list) + * [Timeline](#timeline) + * [Voice message](#voice-message) + * [Room settings](#room-settings) + * [VoIP](#voip) + * [Presence](#presence) + * [Spaces](#spaces) + * [List to be continued...](#list-to-be-continued) + + + +**TODO This documentation is a bit outdated and must be updated when we will set up the design components.** + +## Introduction + +Design at element.io is done using Figma - https://www.figma.com + +## How to import from Figma to the Element Android project + +Integration should be done using the Android development best practice, and should follow the existing convention in the code. + +### Colors + +Element Android already contains all the colors which can be used by the designer, in the module `ui-style`. +Some of them depend on the theme, so ensure to use theme attributes and not colors directly. + +### Text + + - click on a text on Figma + - on the right panel, information about the style and colors are displayed + - in Element Android, text style are already defined, generally you should not create new style + - apply the style and the color to the layout + +### Dimension, position and margin + + - click on an item on Figma + - dimensions of the item will be displayed. + - move the mouse to other items to get relative positioning, margin, etc. + +### Icons + +#### Export drawable from Figma + + - click on the element to export + - ensure that the correct layer is selected. Sometimes the parent layer has to be selected on the left panel + - on the right panel, click on "export" + - select SVG + - you can check the preview of what will be exported + - click on "export" and save the file locally + - unzip the file if necessary + +It's also possible for any icon to go to the main component by right-clicking on the icon. + +#### Import in Android Studio + + - right click on the drawable folder where the drawable will be created + - click on "New"/"Vector Asset" + - select the exported file + - update the filename if necessary + - click on "Next" and click on "Finish" + - open the created vector drawable + - optionally update the color(s) to "#FF0000" (red) to ensure that the drawable is correctly tinted at runtime. + +### Images + +Android 4.3 (18+) fully supports the WebP image format which can often provide smaller image sizes without drastically impacting image quality (depending on the output encoding quality). +When importing non vector images, WebP is the preferred format. + +Images can be converted to the WebP within Android Studio by + - right clicking the image file within the project file explorer + - select `Convert to WebP` + +https://developer.android.com/studio/write/convert-webp + +## Figma links + +Figma links can be included in the layout, for future reference, but it is also OK to add a paragraph below here, to centralize the information + +Main entry point: https://www.figma.com/files/project/5612863/Element?fuid=779371459522484071 + +Note: all the Figma links are not publicly available. + +### Coumpound + +Coumpound contains the theme of the application, with all the components, in Light and Dark theme: palette (colors), typography, iconography, etc. + +https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound + +### Login + +TBD + +#### Login v2 + +https://www.figma.com/file/xdV4PuI3DlzA1EiBvbrggz/Login-Flow-v2 + +### Room list + +TBD + +### Timeline + +https://www.figma.com/file/x1HYYLYMmbYnhfoz2c2nGD/%5BRiotX%5D-Misc?node-id=0%3A1 + +### Voice message + +https://www.figma.com/file/uaWc62Ux2DkZC4OGtAGcNc/Voice-Messages?node-id=473%3A12 + +### Room settings + +TBD + +### VoIP + +https://www.figma.com/file/V6m2z0oAtUV1l8MdyIrAep/VoIP?node-id=4254%3A25767 + +### Presence + +https://www.figma.com/file/qmvEskET5JWva8jZJ4jX8o/Presence---User-Status?node-id=114%3A9174 +(Option B is chosen) + +### Spaces + +https://www.figma.com/file/m7L63aGPW7iHnIYStfdxCe/Spaces?node-id=192%3A30161 + +### List to be continued... diff --git a/docs/images/screen1.png b/docs/images/screen1.png new file mode 100644 index 0000000000..9f9d7747ff Binary files /dev/null and b/docs/images/screen1.png differ diff --git a/docs/images/screen2.png b/docs/images/screen2.png new file mode 100644 index 0000000000..a5733003d6 Binary files /dev/null and b/docs/images/screen2.png differ diff --git a/docs/images/screen3.png b/docs/images/screen3.png new file mode 100644 index 0000000000..3edb49d086 Binary files /dev/null and b/docs/images/screen3.png differ diff --git a/docs/images/screen4.png b/docs/images/screen4.png new file mode 100644 index 0000000000..53da801a1b Binary files /dev/null and b/docs/images/screen4.png differ diff --git a/docs/installing_from_ci.md b/docs/installing_from_ci.md new file mode 100644 index 0000000000..634ee905ab --- /dev/null +++ b/docs/installing_from_ci.md @@ -0,0 +1,49 @@ +## Installing from CI + + + + * [Installing from GitHub](#installing-from-github) + * [Create a GitHub token](#create-a-github-token) + * [Provide artifact URL](#provide-artifact-url) + * [Next steps](#next-steps) + * [Future improvement](#future-improvement) + + + +Installing APK build by the CI is possible + +### Installing from GitHub + +TODO Import the script from Element Android and make it work, then update this documentation. + +To install an APK built by a GitHub action, run the script `./tools/install/installFromGitHub.sh`. You will need to pass a GitHub token to do so. + +#### Create a GitHub token + +You can create a GitHub token going to your Github account, at this page: [https://github.com/settings/tokens](https://github.com/settings/tokens). + +You need to create a token (classic) with the scope `repo/public_repo`. So just check the corresponding checkbox. +Validity can be long since the scope of this token is limited. You will still be able to delete the token and generate a new one. +Click on Generate token and save the token locally. + +### Provide artifact URL + +The script will ask for an artifact URL. You can get this artifact URL by following these steps: + +- open the pull request +- in the check at the bottom, click on `APK Build / Build debug APKs` +- click on `Summary` +- scroll to the bottom of the page +- copy the link `vector-Fdroid-debug` if you want the F-Droid variant or `vector-Gplay-debug` if you want the Gplay variant. + +The copied link can be provided to the script. + +### Next steps + +The script will download the artifact, unzip it and install the correct version (regarding arch) on your device. + +Files will be added to the folder `./tmp/DebugApks`. Feel free to cleanup this folder from time to time, the script will not delete files. + +### Future improvement + +The script could ask the user for a Pull Request number and Gplay/Fdroid choice like it was done with Buildkite script. Using GitHub API may be possible to do that. diff --git a/docs/integration_tests.md b/docs/integration_tests.md new file mode 100644 index 0000000000..b5a830e7ff --- /dev/null +++ b/docs/integration_tests.md @@ -0,0 +1,131 @@ +# Integration tests + + + +* [Pre requirements](#pre-requirements) +* [Install and run Synapse](#install-and-run-synapse) +* [Run the test](#run-the-test) +* [Stop Synapse](#stop-synapse) +* [Troubleshoot](#troubleshoot) + * [Android Emulator does cannot reach the homeserver](#android-emulator-does-cannot-reach-the-homeserver) + * [Tests partially run but some fail with "Unable to contact localhost:8080"](#tests-partially-run-but-some-fail-with-"unable-to-contact-localhost:8080") + * [virtualenv command fails](#virtualenv-command-fails) + + + +Integration tests are useful to ensure that the code works well for any use cases. + +They can also be used as sample on how to use the Matrix SDK. + +In a ideal world, every API of the SDK should be covered by integration tests. For the moment, we have test mainly for the Crypto part, which is the tricky part. But it covers quite a lot of features: accounts creation, login to existing account, send encrypted messages, keys backup, verification, etc. + +The Matrix SDK is able to open multiple sessions, for the same user, of for different users. This way we can test communication between several sessions on a single device. + +## Pre requirements + +Integration tests need a homeserver running on localhost. + +The documentation describes what we do to have one, using [Synapse](https://github.com/matrix-org/synapse/), which is the Matrix reference homeserver. + +## Install and run Synapse + +Steps: + +- Install virtualenv + +```bash +python3 -m pip install virtualenv +``` + +- Clone Synapse repository + +```bash +git clone -b develop https://github.com/matrix-org/synapse.git +``` +or +```bash +git clone -b develop git@github.com:matrix-org/synapse.git +``` + +You should have the develop branch cloned by default. + +- Run synapse, from the Synapse folder you just cloned + +```bash +virtualenv -p python3 env +source env/bin/activate +pip install -e . +demo/start.sh --no-rate-limit + +``` + +Alternatively, to install the latest Synapse release package (and not a cloned branch) you can run the following instead of `git clone` and `pip install -e .`: + +```bash +pip install matrix-synapse +``` + +On your first run, you will want to stop the demo and edit the config to correct the `public_baseurl` to http://10.0.2.2:8080 and restart the server. + +You should now have 3 running federated Synapse instances 🎉, at http://127.0.0.1:8080/, http://127.0.0.1:8081/ and http://127.0.0.1:8082/, which should display a "It Works! Synapse is running" message. + +## Run the test + +It's recommended to run tests using an Android Emulator and not a real device. First reason for that is that the tests will use http://10.0.2.2:8080 to connect to Synapse, which run locally on your machine. + +You can run all the tests in the `androidTest` folders. + +It can be done using this command: + +```bash +./gradlew vector:connectedAndroidTest matrix-sdk-android:connectedAndroidTest +``` + +## Stop Synapse + +To stop Synapse, you can run the following commands: + +```bash +./demo/stop.sh +``` + +And you can deactivate the virtualenv: + +```bash +deactivate +``` + +## Troubleshoot + +You'll need python3 to be able to run synapse + +### Android Emulator does cannot reach the homeserver + +Try on the Emulator browser to open "http://10.0.2.2:8080". You should see the "Synapse is running" message. + +### Tests partially run but some fail with "Unable to contact localhost:8080" + +This is because the `public_baseurl` of synapse is not consistent with the endpoint that the tests are connecting to. + +Ensure you have the following configuration in `demo/etc/8080.config`. + +``` +public_baseurl: http://10.0.2.2:8080/ +``` + +After changing this you will need to restart synapse using `demo/stop.sh` and `demo/start.sh` to load the new configuration. + +### virtualenv command fails + +You can try using +```bash +python3 -m venv env +``` +or +```bash +python3 -m virtualenv env +``` +instead of +```bash +virtualenv -p python3 env +``` diff --git a/docs/nightly_build.md b/docs/nightly_build.md new file mode 100644 index 0000000000..9abd59a67b --- /dev/null +++ b/docs/nightly_build.md @@ -0,0 +1,52 @@ +# Nightly builds + + + +* [Configuration](#configuration) +* [How to register to get nightly build](#how-to-register-to-get-nightly-build) +* [Build nightly manually](#build-nightly-manually) + + + +## Configuration + +The nightly build will contain what's on develop, in release mode, for the main variant. It is signed using a dedicated signature, and has a dedicated appId (`io.element.android.x.nightly`), so it can be installed along with the production version of ElementX Android. The only other difference compared to ElementX Android is a different app name. We do not want to change the app name since it will also affect some strings in the app, and we do want to do that. (TODO today, the app name is changed.) + +Nightly builds are built and released to Firebase every days, and automatically. + +This is recommended to exclusively use this app, with your main account, instead of ElementX Android, and fallback to ElementX Android just in case of regression, to discover as soon as possible any regression, and report it to the team. To avoid double notification, you may want to disable the notification from the Element Android production version. Just open Element Android, navigate to `Settings/Notifications` and uncheck `Enable notifications for this session` (TODO Not supported yet). + +*Note:* Due to a limitation of Firebase, the nightly build is the universal build, which means that the size of the APK is a bit bigger, but this should not have any other side effect. + +## How to register to get nightly build + +Click on this link and follow the instruction: [https://appdistribution.firebase.dev/i/7de2dbc61e7fb2a6](https://appdistribution.firebase.dev/i/7de2dbc61e7fb2a6) + +## Build nightly manually + +Nightly build can be built manually from your computer. You will need to retrieved some secrets from Passbolt and add them to your file `~/.gradle/gradle.properties`: + +``` +signing.element.nightly.storePassword=VALUE_FROM_PASSBOLT +signing.element.nightly.keyId=VALUE_FROM_PASSBOLT +signing.element.nightly.keyPassword=VALUE_FROM_PASSBOLT +``` + +You will also need to add the environment variable `FIREBASE_TOKEN`: + +```sh +export FIREBASE_TOKEN=VALUE_FROM_PASSBOLT +``` + +Then you can run the following commands (which are also used in the file for [the GitHub action](../.github/workflows/nightly.yml)): + +```sh +git checkout develop +mv towncrier.toml towncrier.toml.bak +sed 's/CHANGES\.md/CHANGES_NIGHTLY\.md/' towncrier.toml.bak > towncrier.toml +rm towncrier.toml.bak +yes n | towncrier build --version nightly +./gradlew assembleGplayNightly appDistributionUploadGplayNightly $CI_GRADLE_ARG_PROPERTIES +``` + +Then you can reset the change on the codebase. diff --git a/docs/notifications.md b/docs/notifications.md new file mode 100644 index 0000000000..612b8785b8 --- /dev/null +++ b/docs/notifications.md @@ -0,0 +1,284 @@ +This document aims to describe how Element android displays notifications to the end user. It also clarifies notifications and background settings in the app. + +# Table of Contents + + + +* [Prerequisites Knowledge](#prerequisites-knowledge) + * [How does a matrix client get a message from a homeserver?](#how-does-a-matrix-client-get-a-message-from-a-homeserver?) + * [How does a mobile app receives push notification](#how-does-a-mobile-app-receives-push-notification) + * [Push VS Notification](#push-vs-notification) + * [Push in the matrix federated world](#push-in-the-matrix-federated-world) + * [How does the homeserver know when to notify a client?](#how-does-the-homeserver-know-when-to-notify-a-client?) + * [Push vs privacy, and mitigation](#push-vs-privacy-and-mitigation) + * [Background processing limitations](#background-processing-limitations) +* [Element Notification implementations](#element-notification-implementations) + * [Requirements](#requirements) + * [Foreground sync mode (Gplay and F-Droid)](#foreground-sync-mode-gplay-and-f-droid) + * [Push (FCM) received in background](#push-fcm-received-in-background) + * [FCM Fallback mode](#fcm-fallback-mode) + * [F-Droid background Mode](#f-droid-background-mode) +* [Application Settings](#application-settings) + + + + +First let's start with some prerequisite knowledge + +## Prerequisites Knowledge + +### How does a matrix client get a message from a homeserver? + +In order to get messages from a homeserver, a matrix client need to perform a ``sync`` operation. + +`To read events, the intended flow of operation is for clients to first call the /sync API without a since parameter. This returns the most recent message events for each room, as well as the state of the room at the start of the returned timeline. ` + +The client need to call the `sync` API periodically in order to get incremental updates of the server state (new messages). +This mechanism is known as **HTTP long Polling**. + +Using the **HTTP Long Polling** mechanism a client polls a server requesting new information. +The server *holds the request open until new data is available*. +Once available, the server responds and sends the new information. +When the client receives the new information, it immediately sends another request, and the operation is repeated. +This effectively emulates a server push feature. + +The HTTP long Polling can be fine tuned in the **SDK** using two parameters: +* timeout (Sync request timeout) +* delay (Delay between each sync) + +**timeout** is a server parameter, defined by: +``` +The maximum time to wait, in milliseconds, before returning this request.` +If no events (or other data) become available before this time elapses, the server will return a response with empty fields. +By default, this is 0, so the server will return immediately even if the response is empty. +``` + +**delay** is a client preference. When the server responds to a sync request, the client waits for `delay`before calling a new sync. + +When the Element Android app is open (i.e in foreground state), the default timeout is 30 seconds, and delay is 0. + +### How does a mobile app receives push notification + +Push notification is used as a way to wake up a mobile application when some important information is available and should be processed. + +Typically in order to get push notification, an application relies on a **Push Notification Service** or **Push Provider**. + +For example iOS uses APNS (Apple Push Notification Service). +Most of android devices relies on Google's Firebase Cloud Messaging (FCM). + > FCM has replaced Google Cloud Messaging (GCM - deprecated April 10 2018) + +FCM will only work on android devices that have Google plays services installed +(In simple terms, Google Play Services is a background service that runs on Android, which in turn helps in integrating Google’s advanced functionalities to other applications) + +De-Googlified devices need to rely on something else in order to stay up to date with a server. +There some cases when devices with google services cannot use FCM (network infrastructure limitations -firewalls-, + privacy and or independence requirement, source code licence) + +### Push VS Notification + +This need some disambiguation, because it is the source of common confusion: + + +*The fact that you see a notification on your screen does not mean that you have successfully configured your PUSH platform.* + + Technically there is a difference between a push and a notification. A notification is what you see on screen and/or in the notification Menu/Drawer (in the top bar of the phone). + + Notifications are not always triggered by a push (One can display a notification locally triggered by an alarm) + + +### Push in the matrix federated world + +In order to send a push to a mobile, App developers need to have a server that will use the FCM APIs, and these APIs requires authentication! +This server is called a **Push Gateway** in the matrix world + +That means that Element Android, a matrix client created by New Vector, is using a **Push Gateway** with the needed credentials (FCM API secret Key) in order to send push to the New Vector client. + +If you create your own matrix client, you will also need to deploy an instance of a **Push Gateway** with the credentials needed to use FCM for your app. + +On registration, a matrix client must tell its homeserver what Push Gateway to use. + +See [Sygnal](https://github.com/matrix-org/sygnal/) for a reference implementation. +``` + + +--------------------+ +-------------------+ + Matrix HTTP | | | | + Notification Protocol | App Developer | | Device Vendor | + | | | | + +-------------------+ | +----------------+ | | +---------------+ | + | | | | | | | | | | + | Matrix homeserver +-----> Push Gateway +------> Push Provider | | + | | | | | | | | | | + +-^-----------------+ | +----------------+ | | +----+----------+ | + | | | | | | + Matrix | | | | | | +Client/Server API + | | | | | + | | +--------------------+ +-------------------+ + | +--+-+ | + | | <-------------------------------------------+ + +---+ | + | | Provider Push Protocol + +----+ + + Mobile Device or Client +``` + +Recommended reading: +* https://thomask.sdf.org/blog/2016/12/11/riots-magical-push-notifications-in-ios.html +* https://matrix.org/docs/spec/client_server/r0.4.0.html#id128 + + +### How does the homeserver know when to notify a client? + +This is defined by [**push rules**](https://matrix.org/docs/spec/client_server/r0.4.0.html#push-rules-). + +`A push rule is a single rule that states under what conditions an event should be passed onto a push gateway and how the notification should be presented (sound / importance).` + +A homeserver can be configured with default rules (for Direct messages, group messages, mentions, etc.. ). + +There are different kind of push rules, it can be per room (each new message on this room should be notified), it can also define a pattern that a message should match (when you are mentioned, or key word based). + +Notifications have 2 'levels' (`highlighted = true/false sound = default/custom`). In Element these notifications level are reflected as Noisy/Silent. + +**What about encrypted messages?** + +Of course, content patterns matching cannot be used for encrypted messages server side (as the content is encrypted). + +That is why clients are able to **process the push rules client side** to decide what kind of notification should be presented for a given event. + +### Push vs privacy, and mitigation + +As seen previously, App developers don't directly send a push to the end user's device, they use a Push Provider as intermediary. So technically this intermediary is able to read the content of what is sent. + +App developers usually mitigate this by sending a `silent notification`, that is a notification with no identifiable data, or with an encrypted payload. When the push is received the app can then synchronise to it's server in order to generate a local notification. + + +### Background processing limitations + +A mobile applications process live in a managed word, meaning that its process can be limited (e.g no network access), stopped or killed at almost anytime by the Operating System. + +In order to improve the battery life of their devices some constructors started to implement mechanism to drastically limit background execution of applications (e.g MIUI/Xiaomi restrictions, Sony stamina mode). +Then starting android M, android has also put more focus on improving device performances, introducing several IDLE modes, App-Standby, Light Doze, Doze. + +In a nutshell, apps can't do much in background now. + +If the devices is not plugged and stays IDLE for a certain amount of time, radio (mobile connectivity) and CPU can/will be turned off. + +For an application like Element, where users can receive important information at anytime, the best option is to rely on a push system (Google's Firebase Message a.k.a FCM). FCM high priority push can wake up the device and whitelist an application to perform background task (for a limited but unspecified amount of time). + +Notice that this is still evolving, and in future versions application that has been 'background restricted' by users won't be able to wake up even when a high priority push is received. Also high priority notifications could be rate limited (not defined anywhere) + +It's getting a lot more complicated when you cannot rely on FCM (because: closed sources, network/firewall restrictions, privacy concerns). +The documentation on this subject is vague, and as per our experiments not always exact, also device's behaviour is fragmented. + +It is getting more and more complex to have reliable notifications when FCM is not used. + +## Element Notification implementations + +### Requirements + +Element Android must work with and without FCM. +* The Element android app published on F-Droid do not rely on FCM (all related dependencies are not present) +* The Element android app published on google play rely on FCM, with a fallback mode when FCM registration has failed (e.g outdated or missing Google Play Services) + +### Foreground sync mode (Gplay and F-Droid) + +When in foreground, Element performs sync continuously with a timeout value set to 10 seconds (see HttpPooling). + +As this mode does not need to live beyond the scope of the application, and as per Google recommendation, Element uses the internal app resources (Thread and Timers) to perform the syncs. + +This mode is turned on when the app enters foreground, and off when enters background. + +In background, and depending on whether push is available or not, Element will use different methods to perform the syncs (Workers / Alarms / Service) + +### Push (FCM) received in background + +In order to enable Push, Element must first get a push token from the firebase SDK, then register a pusher with this token on the homeserver. + +When a message should be notified to a user, the user's homeserver notifies the registered `push gateway` for Element, that is [sygnal](https://github.com/matrix-org/sygnal) _- The reference implementation for push gateways -_ hosted by matrix.org. + +This sygnal instance is configured with the required FCM API authentication token, and will then use the FCM API in order to notify the user's device running Element. + +``` +Homeserver ----> Sygnal (configured for Element) ----> FCM ----> Element +``` + +The push gateway is configured to only send `(eventId,roomId)` in the push payload (for better [privacy](#push-vs-privacy-and-mitigation)). + +Element needs then to synchronise with the user's homeserver, in order to resolve the event and create a notification. + +As per [Google recommendation](https://android-developers.googleblog.com/2018/09/notifying-your-users-with-fcm.html), Element will then use the WorkManager API in order to trigger a background sync. + +**Google recommendations:** +> We recommend using FCM messages in combination with the WorkManager 1 or JobScheduler API + +> Avoid background services. One common pitfall is using a background service to fetch data in the FCM message handler, since background service will be stopped by the system per recent changes to Google Play Policy + +``` +Homeserver ----> Sygnal ----> FCM ----> Element + (Sync) ----> Homeserver + <---- + Display notification +``` + +**Possible outcomes** + +Upon reception of the FCM push, Element will perform a sync call to the homeserver, during this process it is possible that: + * Happy path, the sync is performed, the message resolved and displayed in the notification drawer + * The notified message is not in the sync. Can happen if a lot of things did happen since the push (`gappy sync`) + * The sync generates additional notifications (e.g an encrypted message where the user is mentioned detected locally) + * The sync takes too long and the process is killed before completion, or network is not reliable and the sync fails. + +Element implements several strategies in these cases (TODO document) + +### FCM Fallback mode + +It is possible that Element is not able to get a FCM push token. +Common errors (among several others) that can cause that: +* Google Play Services is outdated +* Google Play Service fails in someways with FCM servers (infamous `SERVICE_NOT_AVAILABLE`) + +If Element is able to detect one of this cases, it will notifies it to the users and when possible help him fix it via a dedicated troubleshoot screen. + +Meanwhile, in order to offer a minimal service, and as per Google's recommendation for background activities, Element will launch periodic background sync in order to stays in sync with servers. + +The fallback mode is impacted by all the battery life saving mechanism implemented by android. Meaning that if the app is not used for a certain amount of time (`App-Standby`), or the device stays still and unplugged (`Light Doze`) , the sync will become less frequent. + +And if the device stays unplugged and still for too long (`Doze Mode`), no background sync will be perform at all (the system's `Ignore Battery Optimization option` has no effect on that). + + Also the time interval between sync is elastic, controlled by the system to group other apps background sync request and start radio/cpu only once for all. + +Usually in this mode, what happen is when you take back your phone in your hand, you suddenly receive notifications. + +The fallback mode is supposed to be a temporary state waiting for the user to fix issues for FCM, or for App Developers that has done a fork to correctly configure their FCM settings. + +### F-Droid background Mode + +The F-Droid Element flavor has no dependencies to FCM, therefore cannot relies on Push. + +Also Google's recommended background processing method cannot be applied. This is because all of these methods are affected by IDLE modes, and will result on the user not being notified at all when the app is in a Doze mode (only in maintenance windows that could happens only after hours). + +Only solution left is to use `AlarmManager`, that offers new API to allow launching some process even if the App is in IDLE modes. + +Notice that these alarms, due to their potential impact on battery life, can still be restricted by the system. Documentation says that they will not be triggered more than every minutes under normal system operation, and when in low power mode about every 15 mn. + +These restrictions can be relaxed by requiring the app to be white listed from battery optimization. + +F-Droid version will schedule alarms that will then trigger a Broadcast Receiver, that in turn will launch a Service (in the classic android way), and the reschedule an alarm for next time. + +Depending on the system status (or device make), it is still possible that the app is not given enough time to launch the service, or that the radio is still turned off thus preventing the sync to success (that's why Alarms are not recommended for network related tasks). + +That is why on Element F-Droid, the broadcast receiver will acquire a temporary WAKE_LOCK for several seconds (thus securing cpu/network), and launch the service in foreground. The service performs the sync. + +Note that foreground services require to put a notification informing the user that the app is doing something even if not launched). + +## Application Settings + +**Notifications > Enable notifications for this account** + +Configure Sygnal to send or not notifications to all user devices. + +**Notifications > Enable notifications for this device** + +Disable notifications locally. The push server will continue to send notifications to the device but this one will ignore them. + + diff --git a/docs/pull_request.md b/docs/pull_request.md new file mode 100644 index 0000000000..2c517039e2 --- /dev/null +++ b/docs/pull_request.md @@ -0,0 +1,290 @@ +# Pull requests + + + +* [Introduction](#introduction) +* [Who should read this document?](#who-should-read-this-document?) +* [Submitting PR](#submitting-pr) + * [Who can submit pull requests?](#who-can-submit-pull-requests?) + * [Humans](#humans) + * [Draft PR?](#draft-pr?) + * [Base branch](#base-branch) + * [PR Review Assignment](#pr-review-assignment) + * [PR review time](#pr-review-time) + * [Re-request PR review](#re-request-pr-review) + * [When create split PR?](#when-create-split-pr?) + * [Avoid fixing other unrelated issue in a big PR](#avoid-fixing-other-unrelated-issue-in-a-big-pr) + * [Bots](#bots) + * [Dependabot](#dependabot) + * [Gradle wrapper](#gradle-wrapper) + * [Sync analytics plan](#sync-analytics-plan) +* [Reviewing PR](#reviewing-pr) + * [Who can review pull requests?](#who-can-review-pull-requests?) + * [What to have in mind when reviewing a PR](#what-to-have-in-mind-when-reviewing-a-pr) + * [Rules](#rules) + * [Check the form](#check-the-form) + * [PR title](#pr-title) + * [PR description](#pr-description) + * [File change](#file-change) + * [Check the commit](#check-the-commit) + * [Check the substance](#check-the-substance) + * [Make a dedicated meeting to review the PR](#make-a-dedicated-meeting-to-review-the-pr) + * [What happen to the issue(s)?](#what-happen-to-the-issues?) + * [Merge conflict](#merge-conflict) + * [When and who can merge PR](#when-and-who-can-merge-pr) + * [Merge type](#merge-type) + * [Resolve conversation](#resolve-conversation) +* [Responsibility](#responsibility) + + + +## Introduction + +This document gives some clue about how to efficiently manage Pull Requests (PR). This document is a first draft and may be improved later. + +## Who should read this document? + +Every pull request reviewers, but also probably every ones who submit PRs. + +## Submitting PR + +### Who can submit pull requests? + +Basically every one who wants to contribute to the project! But there are some rules to follow. + +#### Humans + +People with write access to the project can directly clone the project, push their branches and create PR. + +External contributors must first fork the project and create PR to the mainline from there. + +##### Draft PR? + +Draft PR can be created when the submitter does not expect the PR to be reviewed and merged yet. It can be useful to publicly show the work, or to do a self-review first. + +Draft PR can also be created when it depends on other un-merged PR. + +In any case, it is better to explicitly declare in the description why the PR is a draft PR. + +Also, draft PR should not stay indefinitely in this state. It may be removed if it is the case and the submitter does not update it after a few days. + +##### Base branch + +The `develop` branch is generally the base branch for every PRs. + +Exceptions can occur: + +- if a feature implementation is split into multiple PRs. We can have a chain of PRs in this case. PR can be merged one by one on develop, and GitHub change the target branch to `develop` for the next PR automatically. +- we want to merge a PR from the community, but there is still work to do, and the PR is not updated by the submitter. First, we can kindly ask the submitter if they will update their PR, by commenting it. If there is no answer after a few days (including a week-end), we can create a new branch, push it, and change the target branch of the PR to this new branch. The PR can then be merged, and we can add more commits to fix the issues. After that a new PR can be created with `develop` as a target branch. + +**Important notice 1:** Releases are created from the `develop` branch. So `develop` branch should always contain a "releasable" source code. So when a feature is being implemented with several PRs, it has to be disabled by default (using a feature flag for instance), until the feature is fully implemented. A last PR to enable the feature can then be created. + +**Important notice 2:** Database migration: some developers and some people from the community are using the nightly build from `develop`. Multiple database migrations should be properly handled for them. This is OK to have multiple migrations between 2 releases, this is not OK to add steps to the pending database migration on `develop`. So for instance `develop` users will migrate from version 11 to version 12, then 13, then 14, and `main` users will do all those steps after they get the app upgrade. + +##### PR Review Assignment + +We use automatic assignment for PR reviews. **A PR is automatically routed by GitHub to one team member** using the round robin algorithm. Additional reviewers can be used for complex changes or when the first reviewer is not confident enough on the changes. +The process is the following: + +- The PR creator selects the [element-x-android-reviewers](https://github.com/orgs/vector-im/teams/element-x-android-reviewers) team as a reviewer. +- GitHub automatically assign the reviewer. If the reviewer is not available (holiday, etc.), remove them and set again the team, GitHub will select another reviewer. +- Alternatively, the PR creator can directly assign specific people if they have another Android developer in their team or they think a specific reviewer should take a look at their PR. +- Reviewers get a notification to make the review: they review the code following the good practice (see the rest of this document). +- After making their own review, if they feel not confident enough, they can ask another person for a full review, or they can tag someone within a PR comment to check specific lines. + +For PRs coming from the community, the issue wrangler can assign either the team [element-x-android-reviewers](https://github.com/orgs/vector-im/teams/element-x-android-reviewers) or any member directly. + +##### PR review time + +As a PR submitter, you deserve a quick review. As a reviewer, you should do your best to unblock others. + +Some tips to achieve it: + +- Set up your GH notifications correctly +- Check your pulls page: [https://github.com/pulls](https://github.com/pulls) +- Check your pending assigned PRs before starting or resuming your day to day tasks +- If you are busy with high priority tasks, inform the author. They will find another developer + +It is hard to define a deadline for a review. It depends on the PR size and the complexity. Let's start with a goal of 24h (working day!) for a PR smaller than 500 lines. If bigger, the submitter and the reviewer should discuss. + +After this time, the submitter can ping the reviewer to get a status of the review. + +##### Re-request PR review + +Once all the remarks have been handled, it's possible to re-request a review from the (same) reviewer to let them know that the PR has been updated the PR is ready to be reviewed again. Use the double arrow next to the reviewer name to do that. + +##### When create split PR? + +To implement big new feature, it may be efficient to split the work into several smaller and scoped PRs. They will be easier to review, and they can be merged on `develop` faster. + +Big PR can take time, and there is a risk of future merge conflict. + +Feature flag can be used to avoid half implemented feature to be available in the application. + +That said, splitting into several PRs should not have the side effect to have more review to do, for instance if some code is added, then finally removed. + +##### Avoid fixing other unrelated issue in a big PR + +Each PR should focus on a single task. If other issues may be fixed when working in the area of it, it's preferable to open a dedicated PR. + +It will have the advantage to be reviewed and merged faster, and not interfere with the main PR. + +It's also applicable for code rework (such as renaming for instance), or code formatting. Sometimes, it is more efficient to extract that work to a dedicated PR, and rebase your branch once this "rework" PR has been merged. + +#### Bots + +Some bots can create PR, but they still have to be reviewed by the team + +##### Dependabot + +Dependabot is a tool which maintain all our external dependencies up to date. A dedicated PR is created for each new available release for one of our external dependency.Dependabot + +To review such PR, you have to + - **IMPORTANT** check the diff files (as always). + - Check the release note. Some existing bugs in Element project may be fixed by the upgrade + - Make sure that the CI is happy + - If the code does not compile (API break for instance), you have to checkout the branch and push new commits + - Do some smoke test, depending of the library which has been upgraded + +For some reason dependabot sometimes does not upgrade some dependencies. In this case, and when detected, the upgrade has to be done manually. + +##### Gradle wrapper + +`Update Gradle Wrapper` is a tool which can create PR to upgrade our gradle.properties file. +Review such PR is the same recipe than for PR from Dependabot + +##### Sync analytics plan + +This tools imports any update in the analytics plan. See instruction in the PR itself to handle it. +More info can be found in the file [analytics.md](./analytics.md) + +## Reviewing PR + +### Who can review pull requests? + +As an open source project, every one can review each others PR. Of course an approval from internal developer is mandatory for a PR to be merged. +But comment in PR from the community are always appreciated! + +### What to have in mind when reviewing a PR + +1. User experience: is the UX and UI correct? You will probably be the second person to test the new thing, the first one is the developer. +2. Developer experience: does the code look nice and decoupled? No big functions, new classes added to the right module, etc. +3. Code maintenance. A bit similar to point 2. Tricky code must be documented for instance +4. Fork consideration. Will configuration of forks be easy? Some documentation may help in some cases. +5. We are building long term products. "Quick and dirty" code must be avoided. +6. The PR includes new tests for the added code, updated test for the existing code +7. All PRs from external contributors **MUST** include a sign-off. It's in the checklist, and sometimes it's checked by the submitter, but there is actually no sign-off. In this case, ask nicely for a sign-off and request changes (do not approve the PR, even if everything else is fine). + +### Rules + +#### Check the form + +##### PR title + +PR title should describe in one line what's brought by the PR. Reviewer can edit the title if it's not clear enough, or to add suffix like `[BLOCKED]` or similar. Fixing typo is also a good practice, since GitHub search is quite not efficient, so the words must be spelled without any issue. Adding suffix will help when viewing the PR list. + +It's free form, but prefix tags could also be used to help understand what's in the PR. + +Examples of prefixes: +- `[Refacto]` +- `[Feature]` +- `[Bugfix]` +- etc. + +Also, it's still possible to add labels to the PRs, such as `A-` or `T-` labels, even if this is not a strong requirement. We prefer to spend time to add labels on issues. + +##### PR description + +PR description should follow the PR template, and at least provide some context about the code change. + +##### File change + +1. Code should follow the guidelines +2. Code should be formatted correctly +3. XML attribute must be sorted +4. New code is added at the correct location +5. New classes are added to the correct location +6. Naming is correct. Naming is really important, it's considered part of the documentation +7. Architecture is followed. For instance, the logic is in the ViewModel and not in the Fragment +8. There is at least one file for the changelog. Exception if the PR fixes something which has not been released yet. Changelog content should target their audience: `.sdk` extension are mainly targeted for developers, other extensions are targeted for users and forks maintainers. It should generally describe visual change rather than give technical details. More details can be found [here](../CONTRIBUTING.md#changelog). +9. PR includes tests. allScreensTest when applicable, and unit tests +10. Avoid over complicating things. Keep it simple (KISS)! +11. PR contains only the expected change. Sometimes, the diff is showing changes that are already on `develop`. This is not good, submitter has to fix that up. + +##### Check the commit + +Commit message must be short, one line and valuable. "WIP" is not a good commit message. Commit message can contain issue number, starting with `#`. GitHub will add some link between the issue and such commit, which can be useful. It's possible to change a commit message at any time (may require a force push). + +Commit messages can contain extra lines with more details, links, etc. But keep in mind that those lines are quite less visible than the first line. + +Also commit history should be nice. Having commits like "Adding temporary code" then later "Removing temporary code" is not good. The branch has to be rebased and those commit have to be dropped. + +PR merger could decide to squash and merge if commit history is not good. + +Commit like "Code review fixes" is good when reviewing the PR, since new changes can be reviewed easily, but is less valuable when looking at git history. To avoid this, PR submitter should always push new commits after a review (no commit amend with force push), and when the PR is approved decide to interactive rebase the PR to improve the git history and reduce noise. + +##### Check the substance + +1. Test the changes! +2. Test the nominal case and the edge cases +3. Run the sanity test for critical PR + +##### Make a dedicated meeting to review the PR + +Sometimes a big PR can be hard to review. Setting up a call with the PR submitter can speed up the communication, rather than putting comments and questions in GitHub comments. It has the inconvenience of making the discussion non-public, consider including a summary of the main points of the "offline" conversation in the PR. + +### What happen to the issue(s)? + +The issue(s) should be referenced in the PR description using keywords like `Closes` of `Fixes` followed by the issue number. + +Example: +> Closes #1 + +Note that you have to repeat the keyword in case of a list of issue + +> Closes #1, Closes #2, etc. + +When PR will be merged, such referenced issue will be automatically closed. +It is up to the person who has merged the PR to go to the (closed) issue(s) and to add a comment to inform in which version the issue fix will be available. Use the current version of `develop` branch. + +> Closed in Element Android v1.x.y + +### Merge conflict + +It's up to the submitter to handle merge conflict. Sometimes, they can be fixed directly from GitHub, sometimes this is not possible. The branch can be rebased on `develop`, or the `develop` branch can be merged on the branch, it's up to the submitter to decide what is best. +Keep in mind that Github Actions are not run in case of conflict. + +### When and who can merge PR + +PR can be merged by the submitter, if and only if at least one approval from another developer is done. Approval from all people added as reviewer is also a good thing to have. Approval from design team may be mandatory, but is not sufficient to merge a PR. + +PR can also be merged by the reviewer, to reduce the time the PR is open. But only if the PR is not in draft and the change are quite small, or behind a feature flag. + +Dangerous PR should not be merged just before a release. Dangerous PR are PR that could break the app. Update of Realm library, rework in the chunk of Events management in the SDK, etc. + +We prefer to merge such PR after a release so that it can be tested during several days by the team before behind included in a release candidate. + +PR from bots will always be merged by the reviewer, right after approving the changes, or in case of critical changes, right after a release. + +#### Merge type + +Generally we use "Create a merge commit", which has the advantage to keep the branch visible. + +If git history is noisy (code added, then removed, etc.), it's possible to use "Squash and merge". But the branch will not be visible anymore, a commit will be added on top of develop. Git commit message can (and probably must) be edited from the GitHub web app. It's better if the submitter do the work to cleanup the git history by using a git interactive rebase of their branch. + +### Resolve conversation + +Generally we do not close conversation added during PR review and update by clicking on "Resolve conversation" +If the submitter or the reviewer do so, it will more difficult for further readers to see again the content. They will have to open the conversation to see it again. it's a waste of time. + +When remarks are handled, a small comment like "done" is enough, commit hash can also be added to the conversation. + +Exception: for big PRs with lots of conversations, using "Resolve conversation" may help to see the remaining remarks. + +Also "Resolve conversation" should probably be hit by the creator of the conversation. + +## Responsibility + +PR submitter is responsible of the incoming change. PR reviewers who approved the PR take a part of responsibility on the code which will land to develop, and then be used by our users, and the user of our forks. + +That said, bug may still be merged on `develop`, this is still acceptable of course. In this case, please make sure an issue is created and correctly labelled. Ideally, such issues should be fixed before the next release candidate, i.e. with a higher priority. But as we release the application every 10 working days, it can be hard to fix every bugs. That's why PR should be fully tested and reviewed before being merge and we should never comment code review remark with "will be handled later", or similar comments. diff --git a/docs/usefulLinks.md b/docs/usefulLinks.md deleted file mode 100644 index 9c66616944..0000000000 --- a/docs/usefulLinks.md +++ /dev/null @@ -1,22 +0,0 @@ - - - * [VersionCatalog](#versioncatalog) - * [Jetpack Compose](#jetpack-compose) - - - -### VersionCatalog - -https://docs.gradle.org/current/userguide/platforms.html - -### Jetpack Compose - -https://developer.android.com/jetpack/compose/mental-model -https://developer.android.com/jetpack/compose/libraries -https://developer.android.com/jetpack/compose/modifiers-list - -https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-api-guidelines.md#api-guidelines-for-jetpack-compose - -Preview -https://alexzh.com/jetpack-compose-preview/ -https://github.com/airbnb/Showkase diff --git a/features/login/build.gradle.kts b/features/login/build.gradle.kts index c181d3b75e..f4f8ca4844 100644 --- a/features/login/build.gradle.kts +++ b/features/login/build.gradle.kts @@ -42,6 +42,13 @@ dependencies { implementation(projects.libraries.testtags) implementation(projects.libraries.uiStrings) ksp(libs.showkase.processor) + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(projects.libraries.matrixtest) + androidTestImplementation(libs.test.junitext) } diff --git a/features/login/src/main/kotlin/io/element/android/features/login/changeserver/ChangeServerView.kt b/features/login/src/main/kotlin/io/element/android/features/login/changeserver/ChangeServerView.kt index 2695163ac4..32fdeedb1d 100644 --- a/features/login/src/main/kotlin/io/element/android/features/login/changeserver/ChangeServerView.kt +++ b/features/login/src/main/kotlin/io/element/android/features/login/changeserver/ChangeServerView.kt @@ -14,8 +14,6 @@ * limitations under the License. */ -@file:OptIn(ExperimentalMaterial3Api::class) - package io.element.android.features.login.changeserver import androidx.compose.foundation.background @@ -33,13 +31,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Button -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Surface -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue @@ -57,6 +49,12 @@ import io.element.android.features.login.error.changeServerError import io.element.android.libraries.architecture.Async import io.element.android.libraries.designsystem.components.VectorIcon import io.element.android.libraries.designsystem.components.form.textFieldState +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator +import io.element.android.libraries.designsystem.theme.components.OutlinedTextField +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag @@ -66,126 +64,129 @@ fun ChangeServerView( modifier: Modifier = Modifier, onChangeServerSuccess: () -> Unit = {}, ) { - Surface( - modifier = modifier, - color = MaterialTheme.colorScheme.background, + val eventSink = state.eventSink + val scrollState = rememberScrollState() + Box( + modifier = modifier + .fillMaxSize() + .systemBarsPadding() + .imePadding() ) { - val eventSink = state.eventSink - val scrollState = rememberScrollState() - Box( + Column( modifier = Modifier - .fillMaxSize() - .systemBarsPadding() - .imePadding() + .verticalScroll( + state = scrollState, + ) + .padding(horizontal = 16.dp) ) { - Column( + val isError = state.changeServerAction is Async.Failure + Box( modifier = Modifier - .verticalScroll( - state = scrollState, + .padding(top = 99.dp) + .size(width = 81.dp, height = 73.dp) + .align(Alignment.CenterHorizontally) + .background( + color = MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape(32.dp) ) - .padding(horizontal = 16.dp) ) { - val isError = state.changeServerAction is Async.Failure - Box( + VectorIcon( modifier = Modifier - .padding(top = 99.dp) - .size(width = 81.dp, height = 73.dp) - .align(Alignment.CenterHorizontally) - .background( - color = MaterialTheme.colorScheme.surfaceVariant, - shape = RoundedCornerShape(32.dp) - ) - ) { - VectorIcon( - modifier = Modifier - .align(Alignment.Center) - .size(width = 48.dp, height = 48.dp), - // TODO Update with design input - resourceId = R.drawable.ic_baseline_dataset_24, - ) - } - Text( - text = "Your server", - modifier = Modifier - .fillMaxWidth() - .defaultMinSize(minHeight = 56.dp) - .align(Alignment.CenterHorizontally) - .padding(top = 38.dp), - textAlign = TextAlign.Center, - fontWeight = FontWeight.Bold, - fontSize = 24.sp, + .align(Alignment.Center) + .size(width = 48.dp, height = 48.dp), + // TODO Update with design input + resourceId = R.drawable.ic_baseline_dataset_24, ) - Text( - text = "A server is a home for all your data.\n" + - "You choose your server and it’s easy to make one.", // TODO "Learn more.", - modifier = Modifier - .fillMaxWidth() - .align(Alignment.CenterHorizontally) - .padding(top = 16.dp), - textAlign = TextAlign.Center, - fontSize = 16.sp, - color = MaterialTheme.colorScheme.secondary + } + Text( + text = "Your server", + modifier = Modifier + .fillMaxWidth() + .defaultMinSize(minHeight = 56.dp) + .align(Alignment.CenterHorizontally) + .padding(top = 38.dp), + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold, + fontSize = 24.sp, + color = MaterialTheme.colorScheme.primary, + ) + Text( + text = "A server is a home for all your data.\n" + + "You choose your server and it’s easy to make one.", // TODO "Learn more.", + modifier = Modifier + .fillMaxWidth() + .align(Alignment.CenterHorizontally) + .padding(top = 16.dp), + textAlign = TextAlign.Center, + fontSize = 16.sp, + color = MaterialTheme.colorScheme.secondary, + ) + var homeserverFieldState by textFieldState(stateValue = state.homeserver) + OutlinedTextField( + value = homeserverFieldState, + modifier = Modifier + .fillMaxWidth() + .testTag(TestTags.changeServerServer) + .padding(top = 200.dp), + onValueChange = { + homeserverFieldState = it + eventSink(ChangeServerEvents.SetServer(it)) + }, + label = { + Text(text = "Server") + }, + isError = isError, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions( + onDone = { eventSink(ChangeServerEvents.Submit) } ) - var homeserverFieldState by textFieldState(stateValue = state.homeserver) - OutlinedTextField( - value = homeserverFieldState, - modifier = Modifier - .fillMaxWidth() - .testTag(TestTags.changeServerServer) - .padding(top = 200.dp), - onValueChange = { - homeserverFieldState = it - eventSink(ChangeServerEvents.SetServer(it)) - }, - label = { - Text(text = "Server") - }, - isError = isError, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Password, - imeAction = ImeAction.Done, + ) + if (state.changeServerAction is Async.Failure) { + Text( + text = changeServerError( + state.homeserver, + state.changeServerAction.error ), - keyboardActions = KeyboardActions( - onDone = { eventSink(ChangeServerEvents.Submit) } - ) - ) - if (state.changeServerAction is Async.Failure) { - Text( - text = changeServerError( - state.homeserver, - state.changeServerAction.error - ), - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(start = 16.dp) - ) - } - Button( - onClick = { eventSink(ChangeServerEvents.Submit) }, - enabled = state.submitEnabled, - modifier = Modifier - .fillMaxWidth() - .testTag(TestTags.changeServerContinue) - .padding(top = 44.dp) - ) { - Text(text = "Continue") - } - if (state.changeServerAction is Async.Success) { - onChangeServerSuccess() - } - } - if (state.changeServerAction is Async.Loading) { - CircularProgressIndicator( - modifier = Modifier.align(Alignment.Center) + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(start = 16.dp) ) } + Button( + onClick = { eventSink(ChangeServerEvents.Submit) }, + enabled = state.submitEnabled, + modifier = Modifier + .fillMaxWidth() + .testTag(TestTags.changeServerContinue) + .padding(top = 44.dp) + ) { + Text(text = "Continue") + } + if (state.changeServerAction is Async.Success) { + onChangeServerSuccess() + } + } + if (state.changeServerAction is Async.Loading) { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center) + ) } } } -@Composable @Preview -fun ChangeServerContentPreview() { +@Composable +fun ChangeServerViewLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun ChangeServerViewDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { ChangeServerView( state = ChangeServerState(homeserver = "matrix.org"), ) diff --git a/features/login/src/main/kotlin/io/element/android/features/login/root/LoginRootScreen.kt b/features/login/src/main/kotlin/io/element/android/features/login/root/LoginRootScreen.kt index b927ae4555..052de92a52 100644 --- a/features/login/src/main/kotlin/io/element/android/features/login/root/LoginRootScreen.kt +++ b/features/login/src/main/kotlin/io/element/android/features/login/root/LoginRootScreen.kt @@ -14,8 +14,6 @@ * limitations under the License. */ -@file:OptIn(ExperimentalMaterial3Api::class) - package io.element.android.features.login.root import androidx.compose.foundation.layout.Box @@ -32,15 +30,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff -import androidx.compose.material3.Button -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Surface -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -60,12 +50,19 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import io.element.android.features.login.error.loginError import io.element.android.libraries.designsystem.components.form.textFieldState +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.theme.components.OutlinedTextField +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.core.SessionId import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.R as StringR -@OptIn(ExperimentalMaterial3Api::class) @Composable fun LoginRootScreen( state: LoginRootState, @@ -74,161 +71,164 @@ fun LoginRootScreen( onLoginWithSuccess: (SessionId) -> Unit = {}, ) { val eventSink = state.eventSink - Surface( - modifier = modifier, - color = MaterialTheme.colorScheme.background, + Box( + modifier = modifier + .fillMaxSize() + .systemBarsPadding() + .imePadding() ) { - Box( + val scrollState = rememberScrollState() + var loginFieldState by textFieldState(stateValue = state.formState.login) + var passwordFieldState by textFieldState(stateValue = state.formState.password) + + Column( modifier = Modifier - .fillMaxSize() - .systemBarsPadding() - .imePadding() + .verticalScroll( + state = scrollState, + ) + .padding(horizontal = 16.dp), ) { - val scrollState = rememberScrollState() - var loginFieldState by textFieldState(stateValue = state.formState.login) - var passwordFieldState by textFieldState(stateValue = state.formState.password) - - Column( + val isError = state.loggedInState is LoggedInState.ErrorLoggingIn + // Title + Text( + text = stringResource(id = StringR.string.ftue_auth_welcome_back_title), modifier = Modifier - .verticalScroll( - state = scrollState, - ) - .padding(horizontal = 16.dp), + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 48.dp), + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold, + fontSize = 24.sp, + color = MaterialTheme.colorScheme.primary, + ) + // Form + Column( + // modifier = Modifier.weight(1f), ) { - val isError = state.loggedInState is LoggedInState.ErrorLoggingIn - // Title - Text( - text = stringResource(id = StringR.string.ftue_auth_welcome_back_title), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 48.dp), - textAlign = TextAlign.Center, - fontWeight = FontWeight.Bold, - fontSize = 24.sp, - ) - // Form - Column( - // modifier = Modifier.weight(1f), + Box( + modifier = Modifier.fillMaxWidth() ) { - Box( - modifier = Modifier.fillMaxWidth() - ) { - OutlinedTextField( - value = state.homeserver, - modifier = Modifier.fillMaxWidth(), - onValueChange = { /* no op */ }, - enabled = false, - label = { - Text(text = "Server") - }, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Uri, - ), - ) - Button( - onClick = onChangeServer, - modifier = Modifier - .align(Alignment.CenterEnd) - .testTag(TestTags.loginChangeServer) - .padding(top = 8.dp, end = 8.dp), - content = { - Text(text = "Change") - } - ) - } OutlinedTextField( - value = loginFieldState, - modifier = Modifier - .fillMaxWidth() - .testTag(TestTags.loginEmailUsername) - .padding(top = 60.dp), + value = state.homeserver, + modifier = Modifier.fillMaxWidth(), + onValueChange = { /* no op */ }, + enabled = false, label = { - Text(text = stringResource(id = StringR.string.login_signin_username_hint)) - }, - onValueChange = { - loginFieldState = it - eventSink(LoginRootEvents.SetLogin(it)) + Text(text = "Server") }, keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Email, - imeAction = ImeAction.Next + keyboardType = KeyboardType.Uri, ), ) - var passwordVisible by remember { mutableStateOf(false) } - if (state.loggedInState is LoggedInState.LoggingIn) { - // Ensure password is hidden when user submits the form - passwordVisible = false - } - OutlinedTextField( - value = passwordFieldState, + Button( + onClick = onChangeServer, modifier = Modifier - .fillMaxWidth() - .testTag(TestTags.loginPassword) - .padding(top = 24.dp), - onValueChange = { - passwordFieldState = it - eventSink(LoginRootEvents.SetPassword(it)) - }, - label = { - Text(text = "Password") - }, - isError = isError, - visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), - trailingIcon = { - val image = - if (passwordVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff - val description = - if (passwordVisible) "Hide password" else "Show password" + .align(Alignment.CenterEnd) + .testTag(TestTags.loginChangeServer) + .padding(top = 8.dp, end = 8.dp), + content = { + Text(text = "Change") + } + ) + } + OutlinedTextField( + value = loginFieldState, + modifier = Modifier + .fillMaxWidth() + .testTag(TestTags.loginEmailUsername) + .padding(top = 60.dp), + label = { + Text(text = stringResource(id = StringR.string.login_signin_username_hint)) + }, + onValueChange = { + loginFieldState = it + eventSink(LoginRootEvents.SetLogin(it)) + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Email, + imeAction = ImeAction.Next + ), + ) + var passwordVisible by remember { mutableStateOf(false) } + if (state.loggedInState is LoggedInState.LoggingIn) { + // Ensure password is hidden when user submits the form + passwordVisible = false + } + OutlinedTextField( + value = passwordFieldState, + modifier = Modifier + .fillMaxWidth() + .testTag(TestTags.loginPassword) + .padding(top = 24.dp), + onValueChange = { + passwordFieldState = it + eventSink(LoginRootEvents.SetPassword(it)) + }, + label = { + Text(text = "Password") + }, + isError = isError, + visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), + trailingIcon = { + val image = + if (passwordVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff + val description = + if (passwordVisible) "Hide password" else "Show password" - IconButton(onClick = { passwordVisible = !passwordVisible }) { - Icon(imageVector = image, description) - } - }, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Password, - imeAction = ImeAction.Done, - ), - keyboardActions = KeyboardActions( - onDone = { eventSink(LoginRootEvents.Submit) } - ), - ) - if (state.loggedInState is LoggedInState.ErrorLoggingIn) { - Text( - text = loginError(state.formState, state.loggedInState.failure), - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(start = 16.dp) - ) - } - } - // Submit - Button( - onClick = { eventSink(LoginRootEvents.Submit) }, - enabled = state.submitEnabled, - modifier = Modifier - .fillMaxWidth() - .testTag(TestTags.loginContinue) - .padding(vertical = 32.dp) - ) { - Text(text = "Continue") - } - when (val loggedInState = state.loggedInState) { - is LoggedInState.LoggedIn -> onLoginWithSuccess(loggedInState.sessionId) - else -> Unit - } - } - if (state.loggedInState is LoggedInState.LoggingIn) { - CircularProgressIndicator( - modifier = Modifier.align(Alignment.Center) + IconButton(onClick = { passwordVisible = !passwordVisible }) { + Icon(imageVector = image, description) + } + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions( + onDone = { eventSink(LoginRootEvents.Submit) } + ), ) + if (state.loggedInState is LoggedInState.ErrorLoggingIn) { + Text( + text = loginError(state.formState, state.loggedInState.failure), + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(start = 16.dp) + ) + } } + // Submit + Button( + onClick = { eventSink(LoginRootEvents.Submit) }, + enabled = state.submitEnabled, + modifier = Modifier + .fillMaxWidth() + .testTag(TestTags.loginContinue) + .padding(vertical = 32.dp) + ) { + Text(text = "Continue") + } + when (val loggedInState = state.loggedInState) { + is LoggedInState.LoggedIn -> onLoginWithSuccess(loggedInState.sessionId) + else -> Unit + } + } + if (state.loggedInState is LoggedInState.LoggingIn) { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center) + ) } } } -@Composable @Preview -fun LoginContentPreview() { +@Composable +fun LoginRootScreenLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun LoginRootScreenDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { LoginRootScreen( state = LoginRootState( homeserver = "matrix.org", diff --git a/features/login/src/test/kotlin/io/element/android/features/login/changeserver/ChangeServerPresenterTest.kt b/features/login/src/test/kotlin/io/element/android/features/login/changeserver/ChangeServerPresenterTest.kt new file mode 100644 index 0000000000..b22c03dc35 --- /dev/null +++ b/features/login/src/test/kotlin/io/element/android/features/login/changeserver/ChangeServerPresenterTest.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2023 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:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.login.changeserver + +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrixtest.A_HOMESERVER +import io.element.android.libraries.matrixtest.auth.FakeAuthenticationService +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ChangeServerPresenterTest { + @Test + fun `present - should start with default homeserver`() = runTest { + val presenter = ChangeServerPresenter( + FakeAuthenticationService(), + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.homeserver).isEqualTo(A_HOMESERVER) + assertThat(initialState.submitEnabled).isTrue() + } + } + + @Test + fun `present - disable if empty or not correct`() = runTest { + val presenter = ChangeServerPresenter( + FakeAuthenticationService(), + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(ChangeServerEvents.SetServer("")) + val emptyState = awaitItem() + assertThat(emptyState.homeserver).isEqualTo("") + assertThat(emptyState.submitEnabled).isFalse() + } + } + + @Test + fun `present - submit`() = runTest { + val presenter = ChangeServerPresenter( + FakeAuthenticationService(), + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(ChangeServerEvents.Submit) + val loadingState = awaitItem() + assertThat(loadingState.submitEnabled).isFalse() + assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java) + val successState = awaitItem() + assertThat(successState.submitEnabled).isTrue() + assertThat(successState.changeServerAction).isInstanceOf(Async.Success::class.java) + } + } +} diff --git a/features/login/src/test/kotlin/io/element/android/features/login/root/LoginRootPresenterTest.kt b/features/login/src/test/kotlin/io/element/android/features/login/root/LoginRootPresenterTest.kt new file mode 100644 index 0000000000..3fa20ae93a --- /dev/null +++ b/features/login/src/test/kotlin/io/element/android/features/login/root/LoginRootPresenterTest.kt @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2023 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:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.login.root + +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.core.SessionId +import io.element.android.libraries.matrixtest.A_HOMESERVER +import io.element.android.libraries.matrixtest.A_HOMESERVER_2 +import io.element.android.libraries.matrixtest.A_PASSWORD +import io.element.android.libraries.matrixtest.A_SESSION_ID +import io.element.android.libraries.matrixtest.A_THROWABLE +import io.element.android.libraries.matrixtest.A_USER_NAME +import io.element.android.libraries.matrixtest.auth.FakeAuthenticationService +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class LoginRootPresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = LoginRootPresenter( + FakeAuthenticationService(), + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.homeserver).isEqualTo(A_HOMESERVER) + assertThat(initialState.loggedInState).isEqualTo(LoggedInState.NotLoggedIn) + assertThat(initialState.formState).isEqualTo(LoginFormState.Default) + assertThat(initialState.submitEnabled).isFalse() + } + } + + @Test + fun `present - enter login and password`() = runTest { + val presenter = LoginRootPresenter( + FakeAuthenticationService(), + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(LoginRootEvents.SetLogin(A_USER_NAME)) + val loginState = awaitItem() + assertThat(loginState.formState).isEqualTo(LoginFormState(login = A_USER_NAME, password = "")) + assertThat(loginState.submitEnabled).isFalse() + initialState.eventSink.invoke(LoginRootEvents.SetPassword(A_PASSWORD)) + val loginAndPasswordState = awaitItem() + assertThat(loginAndPasswordState.formState).isEqualTo(LoginFormState(login = A_USER_NAME, password = A_PASSWORD)) + assertThat(loginAndPasswordState.submitEnabled).isTrue() + } + } + + @Test + fun `present - submit`() = runTest { + val presenter = LoginRootPresenter( + FakeAuthenticationService(), + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(LoginRootEvents.SetLogin(A_USER_NAME)) + initialState.eventSink.invoke(LoginRootEvents.SetPassword(A_PASSWORD)) + skipItems(1) + val loginAndPasswordState = awaitItem() + loginAndPasswordState.eventSink.invoke(LoginRootEvents.Submit) + val submitState = awaitItem() + assertThat(submitState.loggedInState).isEqualTo(LoggedInState.LoggingIn) + val loggedInState = awaitItem() + assertThat(loggedInState.loggedInState).isEqualTo(LoggedInState.LoggedIn(SessionId(A_SESSION_ID))) + } + } + + @Test + fun `present - submit with error`() = runTest { + val authenticationService = FakeAuthenticationService() + val presenter = LoginRootPresenter( + authenticationService, + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(LoginRootEvents.SetLogin(A_USER_NAME)) + initialState.eventSink.invoke(LoginRootEvents.SetPassword(A_PASSWORD)) + skipItems(1) + val loginAndPasswordState = awaitItem() + authenticationService.givenLoginError(A_THROWABLE) + loginAndPasswordState.eventSink.invoke(LoginRootEvents.Submit) + val submitState = awaitItem() + assertThat(submitState.loggedInState).isEqualTo(LoggedInState.LoggingIn) + val loggedInState = awaitItem() + assertThat(loggedInState.loggedInState).isEqualTo(LoggedInState.ErrorLoggingIn(A_THROWABLE)) + } + } + + @Test + fun `present - refresh server`() = runTest { + val authenticationService = FakeAuthenticationService() + val presenter = LoginRootPresenter( + authenticationService, + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.homeserver).isEqualTo(A_HOMESERVER) + authenticationService.givenHomeserver(A_HOMESERVER_2) + initialState.eventSink.invoke(LoginRootEvents.RefreshHomeServer) + val refreshedState = awaitItem() + assertThat(refreshedState.homeserver).isEqualTo(A_HOMESERVER_2) + } + } +} diff --git a/features/logout/build.gradle.kts b/features/logout/build.gradle.kts index e2df3becf4..2935965b80 100644 --- a/features/logout/build.gradle.kts +++ b/features/logout/build.gradle.kts @@ -40,6 +40,13 @@ dependencies { implementation(projects.libraries.elementresources) implementation(projects.libraries.uiStrings) ksp(libs.showkase.processor) + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(projects.libraries.matrixtest) + androidTestImplementation(libs.test.junitext) } diff --git a/features/logout/src/main/kotlin/io/element/android/features/logout/LogoutPreferenceScreen.kt b/features/logout/src/main/kotlin/io/element/android/features/logout/LogoutPreferenceScreen.kt index cc94f56806..2134311672 100644 --- a/features/logout/src/main/kotlin/io/element/android/features/logout/LogoutPreferenceScreen.kt +++ b/features/logout/src/main/kotlin/io/element/android/features/logout/LogoutPreferenceScreen.kt @@ -29,6 +29,8 @@ import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory import io.element.android.libraries.designsystem.components.preferences.PreferenceText +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.ui.strings.R as StringR @Composable @@ -88,8 +90,15 @@ fun LogoutPreferenceContent( } } -@Composable @Preview -fun LogoutContentPreview() { +@Composable +fun LogoutPreferenceViewLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun LogoutPreferenceViewDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { LogoutPreferenceView(LogoutPreferenceState()) } diff --git a/features/logout/src/test/kotlin/io/element/android/features/logout/LogoutPreferencePresenterTest.kt b/features/logout/src/test/kotlin/io/element/android/features/logout/LogoutPreferencePresenterTest.kt new file mode 100644 index 0000000000..e84226b3e8 --- /dev/null +++ b/features/logout/src/test/kotlin/io/element/android/features/logout/LogoutPreferencePresenterTest.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2023 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:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.logout + +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.core.SessionId +import io.element.android.libraries.matrixtest.A_THROWABLE +import io.element.android.libraries.matrixtest.FakeMatrixClient +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class LogoutPreferencePresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = LogoutPreferencePresenter( + FakeMatrixClient(SessionId("sessionId")), + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized) + } + } + + @Test + fun `present - logout`() = runTest { + val presenter = LogoutPreferencePresenter( + FakeMatrixClient(SessionId("sessionId")), + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(LogoutPreferenceEvents.Logout) + val loadingState = awaitItem() + assertThat(loadingState.logoutAction).isInstanceOf(Async.Loading::class.java) + val successState = awaitItem() + assertThat(successState.logoutAction).isInstanceOf(Async.Success::class.java) + } + } + + @Test + fun `present - logout with error`() = runTest { + val matrixClient = FakeMatrixClient(SessionId("sessionId")) + val presenter = LogoutPreferencePresenter( + matrixClient, + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + matrixClient.givenLogoutError(A_THROWABLE) + initialState.eventSink.invoke(LogoutPreferenceEvents.Logout) + val loadingState = awaitItem() + assertThat(loadingState.logoutAction).isInstanceOf(Async.Loading::class.java) + val successState = awaitItem() + assertThat(successState.logoutAction).isEqualTo(Async.Failure(A_THROWABLE)) + } + } +} + diff --git a/features/messages/build.gradle.kts b/features/messages/build.gradle.kts index 975c7b5c0e..b6f6fe4f33 100644 --- a/features/messages/build.gradle.kts +++ b/features/messages/build.gradle.kts @@ -44,7 +44,14 @@ dependencies { implementation(libs.accompanist.flowlayout) implementation(libs.androidx.recyclerview) implementation(libs.jsoup) + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(projects.libraries.matrixtest) + androidTestImplementation(libs.test.junitext) ksp(libs.showkase.processor) } diff --git a/features/messages/src/main/kotlin/io/element/android/features/messages/MessagesView.kt b/features/messages/src/main/kotlin/io/element/android/features/messages/MessagesView.kt index ef47416b48..698d6a1952 100644 --- a/features/messages/src/main/kotlin/io/element/android/features/messages/MessagesView.kt +++ b/features/messages/src/main/kotlin/io/element/android/features/messages/MessagesView.kt @@ -39,13 +39,8 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -64,6 +59,11 @@ import io.element.android.features.messages.timeline.TimelineView import io.element.android.features.messages.timeline.model.TimelineItem import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.utils.LogCompositions import kotlinx.coroutines.launch import timber.log.Timber diff --git a/features/messages/src/main/kotlin/io/element/android/features/messages/actionlist/ActionListView.kt b/features/messages/src/main/kotlin/io/element/android/features/messages/actionlist/ActionListView.kt index b5d7428d56..bed36708f6 100644 --- a/features/messages/src/main/kotlin/io/element/android/features/messages/actionlist/ActionListView.kt +++ b/features/messages/src/main/kotlin/io/element/android/features/messages/actionlist/ActionListView.kt @@ -29,11 +29,10 @@ import androidx.compose.foundation.lazy.items import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ListItem import androidx.compose.material.LocalContentColor -import androidx.compose.material.MaterialTheme -import androidx.compose.material.ModalBottomSheetLayout import androidx.compose.material.ModalBottomSheetState import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.Text +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope @@ -44,6 +43,7 @@ import androidx.compose.ui.unit.dp import io.element.android.features.messages.actionlist.model.TimelineItemAction import io.element.android.features.messages.timeline.model.TimelineItem import io.element.android.libraries.designsystem.components.VectorIcon +import io.element.android.libraries.designsystem.theme.components.ModalBottomSheetLayout import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch @@ -85,7 +85,7 @@ fun ActionListView( .imePadding() ) } - ) {} + ) } @Composable @@ -115,13 +115,13 @@ private fun SheetContent( text = { Text( text = action.title, - color = if (action.destructive) MaterialTheme.colors.error else Color.Unspecified, + color = if (action.destructive) MaterialTheme.colorScheme.error else Color.Unspecified, ) }, icon = { VectorIcon( resourceId = action.icon, - tint = if (action.destructive) MaterialTheme.colors.error else LocalContentColor.current, + tint = if (action.destructive) MaterialTheme.colorScheme.error else LocalContentColor.current, ) } ) diff --git a/features/messages/src/main/kotlin/io/element/android/features/messages/actionlist/model/TimelineItemAction.kt b/features/messages/src/main/kotlin/io/element/android/features/messages/actionlist/model/TimelineItemAction.kt index 46d879b850..ea48797824 100644 --- a/features/messages/src/main/kotlin/io/element/android/features/messages/actionlist/model/TimelineItemAction.kt +++ b/features/messages/src/main/kotlin/io/element/android/features/messages/actionlist/model/TimelineItemAction.kt @@ -18,6 +18,7 @@ package io.element.android.features.messages.actionlist.model import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable +import io.element.android.libraries.designsystem.VectorIcons @Immutable sealed class TimelineItemAction( @@ -25,9 +26,9 @@ sealed class TimelineItemAction( @DrawableRes val icon: Int, val destructive: Boolean = false ) { - object Forward : TimelineItemAction("Forward", io.element.android.libraries.designsystem.VectorIcons.ArrowForward) - object Copy : TimelineItemAction("Copy", io.element.android.libraries.designsystem.VectorIcons.Copy) - object Redact : TimelineItemAction("Redact", io.element.android.libraries.designsystem.VectorIcons.Delete, destructive = true) - object Reply : TimelineItemAction("Reply", io.element.android.libraries.designsystem.VectorIcons.Reply) - object Edit : TimelineItemAction("Edit", io.element.android.libraries.designsystem.VectorIcons.Edit) + object Forward : TimelineItemAction("Forward", VectorIcons.ArrowForward) + object Copy : TimelineItemAction("Copy", VectorIcons.Copy) + object Redact : TimelineItemAction("Redact", VectorIcons.Delete, destructive = true) + object Reply : TimelineItemAction("Reply", VectorIcons.Reply) + object Edit : TimelineItemAction("Edit", VectorIcons.Edit) } diff --git a/features/messages/src/main/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenter.kt b/features/messages/src/main/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenter.kt index 29d5030b4e..4f7cbc287f 100644 --- a/features/messages/src/main/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenter.kt +++ b/features/messages/src/main/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenter.kt @@ -59,7 +59,10 @@ class MessageComposerPresenter @Inject constructor( when (event) { MessageComposerEvents.ToggleFullScreenState -> isFullScreen.value = !isFullScreen.value is MessageComposerEvents.UpdateText -> text.value = event.text.toStableCharSequence() - MessageComposerEvents.CloseSpecialMode -> composerMode.setToNormal() + MessageComposerEvents.CloseSpecialMode -> { + text.value = "".toStableCharSequence() + composerMode.setToNormal() + } is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage(event.message, composerMode, text) is MessageComposerEvents.SetMode -> composerMode.value = event.composerMode } diff --git a/features/messages/src/main/kotlin/io/element/android/features/messages/textcomposer/MessageComposerView.kt b/features/messages/src/main/kotlin/io/element/android/features/messages/textcomposer/MessageComposerView.kt index e3b674d32a..0d2401d667 100644 --- a/features/messages/src/main/kotlin/io/element/android/features/messages/textcomposer/MessageComposerView.kt +++ b/features/messages/src/main/kotlin/io/element/android/features/messages/textcomposer/MessageComposerView.kt @@ -18,7 +18,7 @@ package io.element.android.features.messages.textcomposer import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import io.element.android.libraries.designsystem.LocalIsDarkTheme +import io.element.android.libraries.designsystem.theme.ElementTheme import io.element.android.libraries.textcomposer.TextComposer @Composable @@ -51,7 +51,7 @@ fun MessageComposerView( onComposerTextChange = ::onComposerTextChange, composerCanSendMessage = state.isSendButtonVisible, composerText = state.text?.charSequence?.toString(), - isInDarkMode = LocalIsDarkTheme.current, + isInDarkMode = !ElementTheme.colors.isLight, modifier = modifier ) } diff --git a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/TimelineView.kt b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/TimelineView.kt index 42eb3c938b..241e328b49 100644 --- a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/TimelineView.kt +++ b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/TimelineView.kt @@ -39,10 +39,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDownward -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf @@ -69,7 +66,6 @@ import io.element.android.features.messages.timeline.components.virtual.Timeline import io.element.android.features.messages.timeline.model.AggregatedReaction import io.element.android.features.messages.timeline.model.MessagesItemGroupPosition import io.element.android.features.messages.timeline.model.TimelineItem -import io.element.android.features.messages.timeline.model.TimelineItemGroupPositionProvider import io.element.android.features.messages.timeline.model.TimelineItemReactions import io.element.android.features.messages.timeline.model.event.MessagesTimelineItemContentProvider import io.element.android.features.messages.timeline.model.event.TimelineItemEncryptedContent @@ -82,12 +78,15 @@ import io.element.android.features.messages.timeline.model.virtual.TimelineItemD import io.element.android.features.messages.timeline.model.virtual.TimelineItemLoadingModel import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarData -import io.element.android.libraries.designsystem.utils.PairCombinedPreviewParameter +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.FloatingActionButton +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.Text import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch -import timber.log.Timber @Composable fun TimelineView( @@ -358,24 +357,26 @@ internal fun BoxScope.TimelineScrollHelper( } } -class MessagesItemGroupPositionToMessagesTimelineItemContentProvider : - PairCombinedPreviewParameter( - TimelineItemGroupPositionProvider() to MessagesTimelineItemContentProvider() - ) - -@Suppress("PreviewPublic") @Preview @Composable -fun TimelineItemsPreview( - @PreviewParameter(MessagesTimelineItemContentProvider::class) - content: TimelineItemEventContent -) { +fun LoginRootScreenLightPreview( + @PreviewParameter(MessagesTimelineItemContentProvider::class) content: TimelineItemEventContent +) = ElementPreviewLight { ContentToPreview(content) } + +@Preview +@Composable +fun LoginRootScreenDarkPreview( + @PreviewParameter(MessagesTimelineItemContentProvider::class) content: TimelineItemEventContent +) = ElementPreviewDark { ContentToPreview(content) } + +@Composable +private fun ContentToPreview(content: TimelineItemEventContent) { val timelineItems = persistentListOf( // 3 items (First Middle Last) with isMine = false createMessageEvent( isMine = false, content = content, - groupPosition = MessagesItemGroupPosition.First + groupPosition = MessagesItemGroupPosition.Last ), createMessageEvent( isMine = false, @@ -385,13 +386,13 @@ fun TimelineItemsPreview( createMessageEvent( isMine = false, content = content, - groupPosition = MessagesItemGroupPosition.Last + groupPosition = MessagesItemGroupPosition.First ), // 3 items (First Middle Last) with isMine = true createMessageEvent( isMine = true, content = content, - groupPosition = MessagesItemGroupPosition.First + groupPosition = MessagesItemGroupPosition.Last ), createMessageEvent( isMine = true, @@ -401,7 +402,7 @@ fun TimelineItemsPreview( createMessageEvent( isMine = true, content = content, - groupPosition = MessagesItemGroupPosition.Last + groupPosition = MessagesItemGroupPosition.First ), ) TimelineView( diff --git a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/MessageEventBubble.kt b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/MessageEventBubble.kt index 9f6c1eb28d..02fa87ec29 100644 --- a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/MessageEventBubble.kt +++ b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/MessageEventBubble.kt @@ -23,20 +23,14 @@ import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ripple.rememberRipple -import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.dp import io.element.android.features.messages.timeline.model.MessagesItemGroupPosition -import io.element.android.libraries.designsystem.LocalIsDarkTheme -import io.element.android.libraries.designsystem.MessageHighlightDark -import io.element.android.libraries.designsystem.MessageHighlightLight -import io.element.android.libraries.designsystem.SystemGrey5Dark -import io.element.android.libraries.designsystem.SystemGrey5Light -import io.element.android.libraries.designsystem.SystemGrey6Dark -import io.element.android.libraries.designsystem.SystemGrey6Light +import io.element.android.libraries.designsystem.theme.ElementTheme +import io.element.android.libraries.designsystem.theme.components.Surface private val BUBBLE_RADIUS = 16.dp @@ -88,24 +82,12 @@ fun MessageEventBubble( } val backgroundBubbleColor = if (isHighlighted) { - if (LocalIsDarkTheme.current) { - MessageHighlightDark - } else { - MessageHighlightLight - } + ElementTheme.colors.messageHighlightedBackground } else { if (isMine) { - if (LocalIsDarkTheme.current) { - SystemGrey5Dark - } else { - SystemGrey5Light - } + ElementTheme.colors.messageFromMeBackground } else { - if (LocalIsDarkTheme.current) { - SystemGrey6Dark - } else { - SystemGrey6Light - } + ElementTheme.colors.messageFromOtherBackground } } val bubbleShape = bubbleShape() diff --git a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemImageView.kt b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemImageView.kt index 583986f3db..731dced6cf 100644 --- a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemImageView.kt +++ b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemImageView.kt @@ -14,11 +14,8 @@ * limitations under the License. */ -@file:OptIn(ExperimentalFoundationApi::class) - package io.element.android.features.messages.timeline.components -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth diff --git a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemInformativeView.kt b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemInformativeView.kt index 9806bc05c8..d7960f2ef9 100644 --- a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemInformativeView.kt +++ b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemInformativeView.kt @@ -20,16 +20,21 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.material3.Icon +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.Text @Composable fun TimelineItemInformativeView( @@ -57,3 +62,20 @@ fun TimelineItemInformativeView( ) } } + +@Preview +@Composable +fun MatrixUserRowLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun MatrixUserRowDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + TimelineItemInformativeView( + text = "Info", + iconDescription = "", + icon = Icons.Default.Delete + ) +} diff --git a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemReactionsView.kt b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemReactionsView.kt index b061f02f3d..50185bdb50 100644 --- a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemReactionsView.kt +++ b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/TimelineItemReactionsView.kt @@ -24,8 +24,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -34,6 +32,8 @@ import androidx.compose.ui.unit.sp import com.google.accompanist.flowlayout.FlowRow import io.element.android.features.messages.timeline.model.AggregatedReaction import io.element.android.features.messages.timeline.model.TimelineItemReactions +import io.element.android.libraries.designsystem.theme.components.Surface +import io.element.android.libraries.designsystem.theme.components.Text @Composable fun TimelineItemReactionsView( diff --git a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/html/HtmlDocument.kt b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/html/HtmlDocument.kt index 21a7cac714..b7e8685656 100644 --- a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/html/HtmlDocument.kt +++ b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/components/html/HtmlDocument.kt @@ -28,8 +28,6 @@ import androidx.compose.foundation.text.appendInlineContent import androidx.compose.material3.ColorScheme import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind @@ -49,6 +47,8 @@ import androidx.compose.ui.unit.sp import com.google.accompanist.flowlayout.FlowRow import io.element.android.libraries.designsystem.LinkColor import io.element.android.libraries.designsystem.components.ClickableLinkText +import io.element.android.libraries.designsystem.theme.components.Surface +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.permalink.PermalinkData import io.element.android.libraries.matrix.permalink.PermalinkParser import kotlinx.collections.immutable.persistentMapOf diff --git a/features/messages/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt new file mode 100644 index 0000000000..a16d9f7eb1 --- /dev/null +++ b/features/messages/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2022 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:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.messages + +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.messages.actionlist.ActionListPresenter +import io.element.android.features.messages.actionlist.model.TimelineItemAction +import io.element.android.features.messages.textcomposer.MessageComposerPresenter +import io.element.android.features.messages.timeline.TimelinePresenter +import io.element.android.features.messages.timeline.model.TimelineItem +import io.element.android.features.messages.timeline.model.TimelineItemReactions +import io.element.android.features.messages.timeline.model.content.TimelineItemContent +import io.element.android.features.messages.timeline.model.content.TimelineItemTextContent +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.matrix.room.MatrixRoom +import io.element.android.libraries.matrixtest.AN_EVENT_ID +import io.element.android.libraries.matrixtest.A_MESSAGE +import io.element.android.libraries.matrixtest.A_ROOM_ID +import io.element.android.libraries.matrixtest.A_USER_ID +import io.element.android.libraries.matrixtest.A_USER_NAME +import io.element.android.libraries.matrixtest.FakeMatrixClient +import io.element.android.libraries.matrixtest.room.FakeMatrixRoom +import io.element.android.libraries.textcomposer.MessageComposerMode +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class MessagesPresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = createMessagePresenter() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.roomId).isEqualTo(A_ROOM_ID) + } + } + + @Test + fun `present - handle action forward`() = runTest { + val presenter = createMessagePresenter() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Forward, aMessageEvent())) + // Still a TODO in the code + } + } + + @Test + fun `present - handle action copy`() = runTest { + val presenter = createMessagePresenter() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Copy, aMessageEvent())) + // Still a TODO in the code + } + } + + @Test + fun `present - handle action reply`() = runTest { + val presenter = createMessagePresenter() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent())) + skipItems(1) + val finalState = awaitItem() + assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) + } + } + + @Test + fun `present - handle action edit`() = runTest { + val presenter = createMessagePresenter() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent())) + skipItems(1) + val finalState = awaitItem() + assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Edit::class.java) + } + } + + @Test + fun `present - handle action redact`() = runTest { + val matrixRoom = FakeMatrixRoom() + val presenter = createMessagePresenter(matrixRoom) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Redact, aMessageEvent())) + assertThat(matrixRoom.redactEventEventIdParam).isEqualTo(AN_EVENT_ID) + } + } + + private fun TestScope.createMessagePresenter( + matrixRoom: MatrixRoom = FakeMatrixRoom() + ): MessagesPresenter { + val matrixClient = FakeMatrixClient() + val messageComposerPresenter = MessageComposerPresenter( + appCoroutineScope = this, + room = matrixRoom + ) + val timelinePresenter = TimelinePresenter( + coroutineDispatchers = testCoroutineDispatchers(), + client = matrixClient, + room = matrixRoom, + ) + val actionListPresenter = ActionListPresenter() + return MessagesPresenter( + room = matrixRoom, + composerPresenter = messageComposerPresenter, + timelinePresenter = timelinePresenter, + actionListPresenter = actionListPresenter, + ) + } +} + +// TODO Move to common module to reuse +fun testCoroutineDispatchers() = CoroutineDispatchers( + io = UnconfinedTestDispatcher(), + computation = UnconfinedTestDispatcher(), + main = UnconfinedTestDispatcher(), + diffUpdateDispatcher = UnconfinedTestDispatcher(), +) + +// TODO Move to common module to reuse and remove this duplication +private fun aMessageEvent( + isMine: Boolean = true, + content: TimelineItemContent = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null), +) = TimelineItem.MessageEvent( + id = AN_EVENT_ID, + senderId = A_USER_ID.value, + senderDisplayName = A_USER_NAME, + senderAvatar = AvatarData(), + content = content, + sentTime = "", + isMine = isMine, + reactionsState = TimelineItemReactions(persistentListOf()) +) diff --git a/features/messages/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt b/features/messages/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt new file mode 100644 index 0000000000..06d1486293 --- /dev/null +++ b/features/messages/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2023 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:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.messages.actionlist + +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.messages.actionlist.model.TimelineItemAction +import io.element.android.features.messages.timeline.model.TimelineItem +import io.element.android.features.messages.timeline.model.TimelineItemReactions +import io.element.android.features.messages.timeline.model.content.TimelineItemContent +import io.element.android.features.messages.timeline.model.content.TimelineItemRedactedContent +import io.element.android.features.messages.timeline.model.content.TimelineItemTextContent +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.matrixtest.AN_EVENT_ID +import io.element.android.libraries.matrixtest.A_MESSAGE +import io.element.android.libraries.matrixtest.A_USER_ID +import io.element.android.libraries.matrixtest.A_USER_NAME +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ActionListPresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = ActionListPresenter() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.target).isEqualTo(ActionListState.Target.None) + } + } + + @Test + fun `present - compute for message from me redacted`() = runTest { + val presenter = ActionListPresenter() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val messageEvent = aMessageEvent(true, TimelineItemRedactedContent) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent)) + // val loadingState = awaitItem() + // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) + val successState = awaitItem() + assertThat(successState.target).isEqualTo( + ActionListState.Target.Success( + messageEvent, + persistentListOf( + ) + ) + ) + initialState.eventSink.invoke(ActionListEvents.Clear) + assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None) + } + } + + @Test + fun `present - compute for message from others redacted`() = runTest { + val presenter = ActionListPresenter() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val messageEvent = aMessageEvent(false, TimelineItemRedactedContent) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent)) + // val loadingState = awaitItem() + // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) + val successState = awaitItem() + assertThat(successState.target).isEqualTo( + ActionListState.Target.Success( + messageEvent, + persistentListOf( + ) + ) + ) + initialState.eventSink.invoke(ActionListEvents.Clear) + assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None) + } + } + + @Test + fun `present - compute for others message`() = runTest { + val presenter = ActionListPresenter() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val messageEvent = aMessageEvent( + isMine = false, + content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null) + ) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent)) + // val loadingState = awaitItem() + // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) + val successState = awaitItem() + assertThat(successState.target).isEqualTo( + ActionListState.Target.Success( + messageEvent, + persistentListOf( + TimelineItemAction.Reply, + TimelineItemAction.Forward, + TimelineItemAction.Copy, + ) + ) + ) + initialState.eventSink.invoke(ActionListEvents.Clear) + assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None) + } + } + + @Test + fun `present - compute for my message`() = runTest { + val presenter = ActionListPresenter() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val messageEvent = aMessageEvent( + isMine = true, + content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null) + ) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent)) + // val loadingState = awaitItem() + // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) + val successState = awaitItem() + assertThat(successState.target).isEqualTo( + ActionListState.Target.Success( + messageEvent, + persistentListOf( + TimelineItemAction.Reply, + TimelineItemAction.Forward, + TimelineItemAction.Copy, + TimelineItemAction.Edit, + TimelineItemAction.Redact, + ) + ) + ) + initialState.eventSink.invoke(ActionListEvents.Clear) + assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None) + } + } +} + +private fun aMessageEvent( + isMine: Boolean, + content: TimelineItemContent, +) = TimelineItem.MessageEvent( + id = AN_EVENT_ID, + senderId = A_USER_ID.value, + senderDisplayName = A_USER_NAME, + senderAvatar = AvatarData(), + content = content, + sentTime = "", + isMine = isMine, + reactionsState = TimelineItemReactions(persistentListOf()) +) diff --git a/features/messages/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt b/features/messages/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt new file mode 100644 index 0000000000..8278acd13e --- /dev/null +++ b/features/messages/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt @@ -0,0 +1,252 @@ +/* + * Copyright (c) 2023 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:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.messages.textcomposer + +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.ReceiveTurbine +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.core.data.StableCharSequence +import io.element.android.libraries.matrixtest.ANOTHER_MESSAGE +import io.element.android.libraries.matrixtest.AN_EVENT_ID +import io.element.android.libraries.matrixtest.A_MESSAGE +import io.element.android.libraries.matrixtest.A_REPLY +import io.element.android.libraries.matrixtest.A_USER_NAME +import io.element.android.libraries.matrixtest.room.FakeMatrixRoom +import io.element.android.libraries.textcomposer.MessageComposerMode +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class MessageComposerPresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = MessageComposerPresenter( + this, + FakeMatrixRoom() + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.isFullScreen).isFalse() + assertThat(initialState.text).isEqualTo(StableCharSequence("")) + assertThat(initialState.mode).isEqualTo(MessageComposerMode.Normal("")) + assertThat(initialState.isSendButtonVisible).isFalse() + } + } + + @Test + fun `present - toggle fullscreen`() = runTest { + val presenter = MessageComposerPresenter( + this, + FakeMatrixRoom() + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(MessageComposerEvents.ToggleFullScreenState) + val fullscreenState = awaitItem() + assertThat(fullscreenState.isFullScreen).isTrue() + fullscreenState.eventSink.invoke(MessageComposerEvents.ToggleFullScreenState) + val notFullscreenState = awaitItem() + assertThat(notFullscreenState.isFullScreen).isFalse() + } + } + + @Test + fun `present - change message`() = runTest { + val presenter = MessageComposerPresenter( + this, + FakeMatrixRoom() + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(MessageComposerEvents.UpdateText(A_MESSAGE)) + val withMessageState = awaitItem() + assertThat(withMessageState.text).isEqualTo(StableCharSequence(A_MESSAGE)) + assertThat(withMessageState.isSendButtonVisible).isTrue() + withMessageState.eventSink.invoke(MessageComposerEvents.UpdateText("")) + val withEmptyMessageState = awaitItem() + assertThat(withEmptyMessageState.text).isEqualTo(StableCharSequence("")) + assertThat(withEmptyMessageState.isSendButtonVisible).isFalse() + } + } + + @Test + fun `present - change mode to edit`() = runTest { + val presenter = MessageComposerPresenter( + this, + FakeMatrixRoom() + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + var state = awaitItem() + val mode = anEditMode() + state.eventSink.invoke(MessageComposerEvents.SetMode(mode)) + state = awaitItem() + assertThat(state.mode).isEqualTo(mode) + state = awaitItem() + assertThat(state.text).isEqualTo(StableCharSequence(A_MESSAGE)) + assertThat(state.isSendButtonVisible).isTrue() + backToNormalMode(state, skipCount = 1) + } + } + + private suspend fun ReceiveTurbine.backToNormalMode(state: MessageComposerState, skipCount: Int = 0) { + state.eventSink.invoke(MessageComposerEvents.CloseSpecialMode) + skipItems(skipCount) + val normalState = awaitItem() + assertThat(normalState.mode).isEqualTo(MessageComposerMode.Normal("")) + assertThat(normalState.text).isEqualTo(StableCharSequence("")) + assertThat(normalState.isSendButtonVisible).isFalse() + } + + @Test + fun `present - change mode to reply`() = runTest { + val presenter = MessageComposerPresenter( + this, + FakeMatrixRoom() + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + var state = awaitItem() + val mode = aReplyMode() + state.eventSink.invoke(MessageComposerEvents.SetMode(mode)) + state = awaitItem() + assertThat(state.mode).isEqualTo(mode) + assertThat(state.text).isEqualTo(StableCharSequence("")) + assertThat(state.isSendButtonVisible).isFalse() + backToNormalMode(state) + } + } + + @Test + fun `present - change mode to quote`() = runTest { + val presenter = MessageComposerPresenter( + this, + FakeMatrixRoom() + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + var state = awaitItem() + val mode = aQuoteMode() + state.eventSink.invoke(MessageComposerEvents.SetMode(mode)) + state = awaitItem() + assertThat(state.mode).isEqualTo(mode) + assertThat(state.text).isEqualTo(StableCharSequence("")) + assertThat(state.isSendButtonVisible).isFalse() + backToNormalMode(state) + } + } + + @Test + fun `present - send message`() = runTest { + val presenter = MessageComposerPresenter( + this, + FakeMatrixRoom() + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(MessageComposerEvents.UpdateText(A_MESSAGE)) + val withMessageState = awaitItem() + assertThat(withMessageState.text).isEqualTo(StableCharSequence(A_MESSAGE)) + assertThat(withMessageState.isSendButtonVisible).isTrue() + withMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(A_MESSAGE)) + val messageSentState = awaitItem() + assertThat(messageSentState.text).isEqualTo(StableCharSequence("")) + assertThat(messageSentState.isSendButtonVisible).isFalse() + } + } + + @Test + fun `present - edit message`() = runTest { + val fakeMatrixRoom = FakeMatrixRoom() + val presenter = MessageComposerPresenter( + this, + fakeMatrixRoom + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.text).isEqualTo(StableCharSequence("")) + val mode = anEditMode() + initialState.eventSink.invoke(MessageComposerEvents.SetMode(mode)) + skipItems(1) + val withMessageState = awaitItem() + assertThat(withMessageState.mode).isEqualTo(mode) + assertThat(withMessageState.text).isEqualTo(StableCharSequence(A_MESSAGE)) + assertThat(withMessageState.isSendButtonVisible).isTrue() + withMessageState.eventSink.invoke(MessageComposerEvents.UpdateText(ANOTHER_MESSAGE)) + val withEditedMessageState = awaitItem() + assertThat(withEditedMessageState.text).isEqualTo(StableCharSequence(ANOTHER_MESSAGE)) + withEditedMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(ANOTHER_MESSAGE)) + skipItems(1) + val messageSentState = awaitItem() + assertThat(messageSentState.text).isEqualTo(StableCharSequence("")) + assertThat(messageSentState.isSendButtonVisible).isFalse() + assertThat(fakeMatrixRoom.editMessageParameter).isEqualTo(ANOTHER_MESSAGE) + } + } + + @Test + fun `present - reply message`() = runTest { + val fakeMatrixRoom = FakeMatrixRoom() + val presenter = MessageComposerPresenter( + this, + fakeMatrixRoom + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.text).isEqualTo(StableCharSequence("")) + val mode = aReplyMode() + initialState.eventSink.invoke(MessageComposerEvents.SetMode(mode)) + val state = awaitItem() + assertThat(state.mode).isEqualTo(mode) + assertThat(state.text).isEqualTo(StableCharSequence("")) + assertThat(state.isSendButtonVisible).isFalse() + initialState.eventSink.invoke(MessageComposerEvents.UpdateText(A_REPLY)) + val withMessageState = awaitItem() + assertThat(withMessageState.text).isEqualTo(StableCharSequence(A_REPLY)) + assertThat(withMessageState.isSendButtonVisible).isTrue() + withMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(A_REPLY)) + skipItems(1) + val messageSentState = awaitItem() + assertThat(messageSentState.text).isEqualTo(StableCharSequence("")) + assertThat(messageSentState.isSendButtonVisible).isFalse() + assertThat(fakeMatrixRoom.replyMessageParameter).isEqualTo(A_REPLY) + } + } +} + +fun anEditMode() = MessageComposerMode.Edit(AN_EVENT_ID, A_MESSAGE) +fun aReplyMode() = MessageComposerMode.Reply(A_USER_NAME, AN_EVENT_ID, A_MESSAGE) +fun aQuoteMode() = MessageComposerMode.Quote(AN_EVENT_ID, A_MESSAGE) diff --git a/features/messages/src/test/kotlin/io/element/android/features/messages/timeline/Test.kt b/features/messages/src/test/kotlin/io/element/android/features/messages/timeline/Test.kt new file mode 100644 index 0000000000..dd6872124b --- /dev/null +++ b/features/messages/src/test/kotlin/io/element/android/features/messages/timeline/Test.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 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:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.messages.timeline + +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher + +// TODO Move to common module to reuse +fun testCoroutineDispatchers() = CoroutineDispatchers( + io = UnconfinedTestDispatcher(), + computation = UnconfinedTestDispatcher(), + main = UnconfinedTestDispatcher(), + diffUpdateDispatcher = UnconfinedTestDispatcher(), +) diff --git a/features/messages/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt b/features/messages/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt new file mode 100644 index 0000000000..ee73dc260b --- /dev/null +++ b/features/messages/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2023 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:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.messages.timeline + +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.messages.timeline.model.TimelineItem +import io.element.android.libraries.matrix.timeline.MatrixTimelineItem +import io.element.android.libraries.matrixtest.AN_EVENT_ID +import io.element.android.libraries.matrixtest.FakeMatrixClient +import io.element.android.libraries.matrixtest.room.FakeMatrixRoom +import io.element.android.libraries.matrixtest.timeline.FakeMatrixTimeline +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class TimelinePresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = TimelinePresenter( + testCoroutineDispatchers(), + FakeMatrixClient(), + FakeMatrixRoom() + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.timelineItems).isEmpty() + } + } + + @Test + fun `present - load more`() = runTest { + val matrixTimeline = FakeMatrixTimeline() + val matrixRoom = FakeMatrixRoom(matrixTimeline = matrixTimeline) + val presenter = TimelinePresenter( + testCoroutineDispatchers(), + FakeMatrixClient(), + matrixRoom + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.hasMoreToLoad).isTrue() + matrixTimeline.givenHasMoreToLoad(false) + initialState.eventSink.invoke(TimelineEvents.LoadMore) + val loadedState = awaitItem() + assertThat(loadedState.hasMoreToLoad).isFalse() + } + } + + @Test + fun `present - set highlighted event`() = runTest { + val matrixTimeline = FakeMatrixTimeline() + val matrixRoom = FakeMatrixRoom(matrixTimeline = matrixTimeline) + val presenter = TimelinePresenter( + testCoroutineDispatchers(), + FakeMatrixClient(), + matrixRoom + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.highlightedEventId).isNull() + initialState.eventSink.invoke(TimelineEvents.SetHighlightedEvent(AN_EVENT_ID)) + val withHighlightedState = awaitItem() + assertThat(withHighlightedState.highlightedEventId).isEqualTo(AN_EVENT_ID) + initialState.eventSink.invoke(TimelineEvents.SetHighlightedEvent(null)) + val withoutHighlightedState = awaitItem() + assertThat(withoutHighlightedState.highlightedEventId).isNull() + } + } + + @Test + fun `present - test callback`() = runTest { + val matrixTimeline = FakeMatrixTimeline() + val matrixRoom = FakeMatrixRoom(matrixTimeline = matrixTimeline) + val presenter = TimelinePresenter( + testCoroutineDispatchers(), + FakeMatrixClient(), + matrixRoom + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.timelineItems).isEmpty() + // Simulate callback from the SDK + matrixTimeline.callback?.onPushedTimelineItem(MatrixTimelineItem.Virtual) + val nonEmptyState = awaitItem() + assertThat(nonEmptyState.timelineItems).isNotEmpty() + assertThat(nonEmptyState.timelineItems[0]).isEqualTo(TimelineItem.Virtual("virtual_item_0")) + } + } +} diff --git a/features/onboarding/src/main/kotlin/io/element/android/features/onboarding/OnBoardingScreen.kt b/features/onboarding/src/main/kotlin/io/element/android/features/onboarding/OnBoardingScreen.kt index f0f524cddf..b2699c1816 100644 --- a/features/onboarding/src/main/kotlin/io/element/android/features/onboarding/OnBoardingScreen.kt +++ b/features/onboarding/src/main/kotlin/io/element/android/features/onboarding/OnBoardingScreen.kt @@ -14,8 +14,6 @@ * limitations under the License. */ -@file:OptIn(ExperimentalMaterial3Api::class) - package io.element.android.features.onboarding import androidx.compose.foundation.Image @@ -26,10 +24,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -49,7 +43,8 @@ import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.HorizontalPagerIndicator import com.google.accompanist.pager.rememberPagerState -import io.element.android.libraries.designsystem.components.VectorButton +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import kotlinx.coroutines.delay @@ -64,74 +59,58 @@ fun OnBoardingScreen( onSignUp: () -> Unit = {}, onSignIn: () -> Unit = {}, ) { - val carrouselState = remember { SplashCarouselStateFactory().create() } - val nbOfPages = carrouselState.items.size + val carrouselData = remember { SplashCarouselDataFactory().create() } + val nbOfPages = carrouselData.items.size var key by remember { mutableStateOf(false) } - Surface( - modifier = modifier, - color = MaterialTheme.colorScheme.background, + Box( + modifier = modifier + .fillMaxSize() + .systemBarsPadding() + .padding(vertical = 16.dp) ) { - Box( - modifier = Modifier - .fillMaxSize() - .systemBarsPadding() - .padding(vertical = 16.dp) + Column( + modifier = Modifier.fillMaxSize(), ) { - Column( - modifier = Modifier.fillMaxSize(), + val pagerState = rememberPagerState() + LaunchedEffect(key) { + launch { + delay(3_000) + pagerState.animateScrollToPage((pagerState.currentPage + 1) % nbOfPages) + // https://stackoverflow.com/questions/73714228/accompanist-pager-animatescrolltopage-doesnt-scroll-to-next-page-correctly + key = !key + } + } + LaunchedEffect(pagerState) { + // Collect from the pager state a snapshotFlow reading the currentPage + snapshotFlow { pagerState.currentPage }.collect { page -> + onPageChanged(page) + } + } + HorizontalPager( + modifier = Modifier.weight(1f), + count = nbOfPages, + state = pagerState, + ) { page -> + // Our page content + OnBoardingPage(carrouselData.items[page]) + } + HorizontalPagerIndicator( + pagerState = pagerState, + modifier = Modifier + .align(CenterHorizontally) + .padding(16.dp), + ) + Button( + onClick = { + onSignIn() + }, + enabled = true, + modifier = Modifier + .align(CenterHorizontally) + .testTag(TestTags.onBoardingSignIn) + .padding(top = 16.dp) ) { - val pagerState = rememberPagerState() - LaunchedEffect(key) { - launch { - delay(3_000) - pagerState.animateScrollToPage((pagerState.currentPage + 1) % nbOfPages) - // https://stackoverflow.com/questions/73714228/accompanist-pager-animatescrolltopage-doesnt-scroll-to-next-page-correctly - key = !key - } - } - LaunchedEffect(pagerState) { - // Collect from the pager state a snapshotFlow reading the currentPage - snapshotFlow { pagerState.currentPage }.collect { page -> - onPageChanged(page) - } - } - HorizontalPager( - modifier = Modifier.weight(1f), - count = nbOfPages, - state = pagerState, - ) { page -> - // Our page content - OnBoardingPage(carrouselState.items[page]) - } - HorizontalPagerIndicator( - pagerState = pagerState, - modifier = Modifier - .align(CenterHorizontally) - .padding(16.dp), - ) - /* - VectorButton( - text = "CREATE ACCOUNT", - onClick = { - onSignUp() - }, - enabled = true, - modifier = Modifier - .align(CenterHorizontally) - .padding(top = 16.dp) - ) - */ - VectorButton( - text = stringResource(id = StringR.string.login_splash_submit), - onClick = { - onSignIn() - }, - enabled = true, - modifier = Modifier - .align(CenterHorizontally) - .testTag(TestTags.onBoardingSignIn) - .padding(top = 16.dp) - ) + Text(text = stringResource(id = StringR.string.login_splash_submit)) } } } @@ -139,7 +118,7 @@ fun OnBoardingScreen( @Composable fun OnBoardingPage( - item: SplashCarouselState.Item, + item: SplashCarouselData.Item, modifier: Modifier = Modifier, ) { Box( diff --git a/features/onboarding/src/main/kotlin/io/element/android/features/onboarding/SplashCarouselState.kt b/features/onboarding/src/main/kotlin/io/element/android/features/onboarding/SplashCarouselData.kt similarity index 96% rename from features/onboarding/src/main/kotlin/io/element/android/features/onboarding/SplashCarouselState.kt rename to features/onboarding/src/main/kotlin/io/element/android/features/onboarding/SplashCarouselData.kt index f6523da7a6..58d1b6534c 100644 --- a/features/onboarding/src/main/kotlin/io/element/android/features/onboarding/SplashCarouselState.kt +++ b/features/onboarding/src/main/kotlin/io/element/android/features/onboarding/SplashCarouselData.kt @@ -19,7 +19,7 @@ package io.element.android.features.onboarding import androidx.annotation.DrawableRes import androidx.annotation.StringRes -data class SplashCarouselState( +data class SplashCarouselData( val items: List ) { data class Item( diff --git a/features/onboarding/src/main/kotlin/io/element/android/features/onboarding/SplashCarouselStateFactory.kt b/features/onboarding/src/main/kotlin/io/element/android/features/onboarding/SplashCarouselDataFactory.kt similarity index 90% rename from features/onboarding/src/main/kotlin/io/element/android/features/onboarding/SplashCarouselStateFactory.kt rename to features/onboarding/src/main/kotlin/io/element/android/features/onboarding/SplashCarouselDataFactory.kt index fc06ba49b6..e2848839ce 100644 --- a/features/onboarding/src/main/kotlin/io/element/android/features/onboarding/SplashCarouselStateFactory.kt +++ b/features/onboarding/src/main/kotlin/io/element/android/features/onboarding/SplashCarouselDataFactory.kt @@ -19,8 +19,8 @@ package io.element.android.features.onboarding import androidx.annotation.DrawableRes import io.element.android.libraries.ui.strings.R as StringR -class SplashCarouselStateFactory { - fun create(): SplashCarouselState { +class SplashCarouselDataFactory { + fun create(): SplashCarouselData { val lightTheme = true fun background(@DrawableRes lightDrawable: Int) = @@ -29,9 +29,9 @@ class SplashCarouselStateFactory { fun hero(@DrawableRes lightDrawable: Int, @DrawableRes darkDrawable: Int) = if (lightTheme) lightDrawable else darkDrawable - return SplashCarouselState( + return SplashCarouselData( listOf( - SplashCarouselState.Item( + SplashCarouselData.Item( StringR.string.ftue_auth_carousel_secure_title, StringR.string.ftue_auth_carousel_secure_body, hero( @@ -40,19 +40,19 @@ class SplashCarouselStateFactory { ), background(R.drawable.bg_carousel_page_1) ), - SplashCarouselState.Item( + SplashCarouselData.Item( StringR.string.ftue_auth_carousel_control_title, StringR.string.ftue_auth_carousel_control_body, hero(R.drawable.ic_splash_control, R.drawable.ic_splash_control_dark), background(R.drawable.bg_carousel_page_2) ), - SplashCarouselState.Item( + SplashCarouselData.Item( StringR.string.ftue_auth_carousel_encrypted_title, StringR.string.ftue_auth_carousel_encrypted_body, hero(R.drawable.ic_splash_secure, R.drawable.ic_splash_secure_dark), background(R.drawable.bg_carousel_page_3) ), - SplashCarouselState.Item( + SplashCarouselData.Item( collaborationTitle(), StringR.string.ftue_auth_carousel_workplace_body, hero( diff --git a/features/preferences/build.gradle.kts b/features/preferences/build.gradle.kts index 92dcb5b13d..ba069e6333 100644 --- a/features/preferences/build.gradle.kts +++ b/features/preferences/build.gradle.kts @@ -44,7 +44,14 @@ dependencies { implementation(projects.libraries.uiStrings) implementation(libs.datetime) implementation(libs.accompanist.placeholder) + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(projects.libraries.matrixtest) + androidTestImplementation(libs.test.junitext) ksp(libs.showkase.processor) } diff --git a/features/preferences/src/main/kotlin/io/element/android/features/preferences/root/PreferencesRootView.kt b/features/preferences/src/main/kotlin/io/element/android/features/preferences/root/PreferencesRootView.kt index bab68974fb..bd8bda3e40 100644 --- a/features/preferences/src/main/kotlin/io/element/android/features/preferences/root/PreferencesRootView.kt +++ b/features/preferences/src/main/kotlin/io/element/android/features/preferences/root/PreferencesRootView.kt @@ -27,6 +27,8 @@ import io.element.android.features.rageshake.preferences.RageshakePreferencesSta import io.element.android.features.rageshake.preferences.RageshakePreferencesView import io.element.android.libraries.architecture.Async import io.element.android.libraries.designsystem.components.preferences.PreferenceView +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.ui.strings.R as StringR @Composable @@ -56,7 +58,14 @@ fun PreferencesRootView( @Preview @Composable -fun PreferencesContentPreview() { +fun PreferencesRootViewLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun PreferencesRootViewDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { val state = PreferencesRootState( logoutState = LogoutPreferenceState(), rageshakeState = RageshakePreferencesState(), diff --git a/features/preferences/src/test/kotlin/io/element/android/features/preferences/root/FakeRageShake.kt b/features/preferences/src/test/kotlin/io/element/android/features/preferences/root/FakeRageShake.kt new file mode 100644 index 0000000000..9c1d69828d --- /dev/null +++ b/features/preferences/src/test/kotlin/io/element/android/features/preferences/root/FakeRageShake.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 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 io.element.android.features.preferences.root + +import io.element.android.features.rageshake.rageshake.RageShake + +// TODO Remove this duplicated class when we will rework modules. +class FakeRageShake( + private var isAvailableValue: Boolean = true +) : RageShake { + + private var interceptor: (() -> Unit)? = null + + override fun isAvailable() = isAvailableValue + + override fun start(sensitivity: Float) { + } + + override fun stop() { + } + + override fun setSensitivity(sensitivity: Float) { + } + + override fun setInterceptor(interceptor: (() -> Unit)?) { + this.interceptor = interceptor + } + + fun triggerPhoneRageshake() = interceptor?.invoke() +} diff --git a/features/preferences/src/test/kotlin/io/element/android/features/preferences/root/FakeRageshakeDataStore.kt b/features/preferences/src/test/kotlin/io/element/android/features/preferences/root/FakeRageshakeDataStore.kt new file mode 100644 index 0000000000..517d587bec --- /dev/null +++ b/features/preferences/src/test/kotlin/io/element/android/features/preferences/root/FakeRageshakeDataStore.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 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 io.element.android.features.preferences.root + +import io.element.android.features.rageshake.rageshake.RageshakeDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +const val A_SENSITIVITY = 1f + +// TODO Remove this duplicated class when we will rework modules. +class FakeRageshakeDataStore( + isEnabled: Boolean = true, + sensitivity: Float = A_SENSITIVITY, +) : RageshakeDataStore { + + private val isEnabledFlow = MutableStateFlow(isEnabled) + override fun isEnabled(): Flow = isEnabledFlow + + override suspend fun setIsEnabled(isEnabled: Boolean) { + isEnabledFlow.value = isEnabled + } + + private val sensitivityFlow = MutableStateFlow(sensitivity) + override fun sensitivity(): Flow = sensitivityFlow + + override suspend fun setSensitivity(sensitivity: Float) { + sensitivityFlow.value = sensitivity + } + + override suspend fun reset() = Unit +} diff --git a/features/preferences/src/test/kotlin/io/element/android/features/preferences/root/PreferencesRootPresenterTest.kt b/features/preferences/src/test/kotlin/io/element/android/features/preferences/root/PreferencesRootPresenterTest.kt new file mode 100644 index 0000000000..77d5ff9a07 --- /dev/null +++ b/features/preferences/src/test/kotlin/io/element/android/features/preferences/root/PreferencesRootPresenterTest.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023 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:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.preferences.root + +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.logout.LogoutPreferencePresenter +import io.element.android.features.rageshake.preferences.RageshakePreferencesPresenter +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrixtest.FakeMatrixClient +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class PreferencesRootPresenterTest { + @Test + fun `present - initial state`() = runTest { + val logoutPresenter = LogoutPreferencePresenter(FakeMatrixClient()) + val rageshakePresenter = RageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore()) + val presenter = PreferencesRootPresenter( + logoutPresenter, rageshakePresenter + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.logoutState.logoutAction).isEqualTo(Async.Uninitialized) + assertThat(initialState.rageshakeState.isEnabled).isTrue() + assertThat(initialState.rageshakeState.isSupported).isTrue() + assertThat(initialState.rageshakeState.sensitivity).isEqualTo(1.0f) + assertThat(initialState.myUser).isEqualTo(Async.Uninitialized) + } + } +} diff --git a/features/rageshake/build.gradle.kts b/features/rageshake/build.gradle.kts index ecb333289b..45cc9cdd64 100644 --- a/features/rageshake/build.gradle.kts +++ b/features/rageshake/build.gradle.kts @@ -40,11 +40,19 @@ dependencies { implementation(projects.libraries.designsystem) implementation(projects.libraries.elementresources) implementation(projects.libraries.uiStrings) - implementation(libs.squareup.seismic) + api(libs.squareup.seismic) implementation(libs.androidx.datastore.preferences) implementation(libs.coil) implementation(libs.coil.compose) ksp(libs.showkase.processor) + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(projects.libraries.matrixtest) + testImplementation(libs.test.mockk) + androidTestImplementation(libs.test.junitext) } diff --git a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/bugreport/BugReportPresenter.kt b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/bugreport/BugReportPresenter.kt index cd555e375a..a2e17d737b 100644 --- a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/bugreport/BugReportPresenter.kt +++ b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/bugreport/BugReportPresenter.kt @@ -23,10 +23,10 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable -import androidx.core.net.toUri import io.element.android.features.rageshake.crash.CrashDataStore import io.element.android.features.rageshake.logs.VectorFileLogger import io.element.android.features.rageshake.reporter.BugReporter +import io.element.android.features.rageshake.reporter.BugReporterListener import io.element.android.features.rageshake.reporter.ReportType import io.element.android.features.rageshake.screenshot.ScreenshotHolder import io.element.android.libraries.architecture.Async @@ -45,7 +45,7 @@ class BugReportPresenter @Inject constructor( private class BugReporterUploadListener( private val sendingProgress: MutableState, private val sendingAction: MutableState> - ) : BugReporter.IMXBugReportListener { + ) : BugReporterListener { override fun onUploadCancelled() { sendingProgress.value = 0f @@ -72,7 +72,7 @@ class BugReportPresenter @Inject constructor( override fun present(): BugReportState { val screenshotUri = rememberSaveable { mutableStateOf( - screenshotHolder.getFile()?.toUri()?.toString() + screenshotHolder.getFileUri() ) } val crashInfo: String by crashDataStore @@ -126,7 +126,11 @@ class BugReportPresenter @Inject constructor( formState.value = operation(formState.value) } - private fun CoroutineScope.sendBugReport(formState: BugReportFormState, hasCrashLogs: Boolean, listener: BugReporter.IMXBugReportListener) = launch { + private fun CoroutineScope.sendBugReport( + formState: BugReportFormState, + hasCrashLogs: Boolean, + listener: BugReporterListener, + ) = launch { bugReporter.sendBugReport( coroutineScope = this, reportType = ReportType.BUG_REPORT, @@ -145,6 +149,6 @@ class BugReportPresenter @Inject constructor( private fun CoroutineScope.resetAll() = launch { screenshotHolder.reset() crashDataStore.reset() - VectorFileLogger.getFromTimber().reset() + VectorFileLogger.getFromTimber()?.reset() } } diff --git a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/bugreport/BugReportView.kt b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/bugreport/BugReportView.kt index 8a32f1ac7a..f2fbd7f8f5 100644 --- a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/bugreport/BugReportView.kt +++ b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/bugreport/BugReportView.kt @@ -26,13 +26,7 @@ import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Button -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Surface -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -54,10 +48,15 @@ import io.element.android.libraries.architecture.Async import io.element.android.libraries.designsystem.components.LabelledCheckbox import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.form.textFieldState +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator +import io.element.android.libraries.designsystem.theme.components.OutlinedTextField +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.utils.LogCompositions import io.element.android.libraries.ui.strings.R as StringR -@OptIn(ExperimentalMaterial3Api::class) @Composable fun BugReportView( state: BugReportState, @@ -73,147 +72,151 @@ fun BugReportView( } return } - Surface( - modifier = modifier, - color = MaterialTheme.colorScheme.background, + Box( + modifier = modifier + .fillMaxSize() + .systemBarsPadding() + .imePadding() ) { - Box( + val scrollState = rememberScrollState() + Column( modifier = Modifier - .fillMaxSize() - .systemBarsPadding() - .imePadding() + .verticalScroll( + state = scrollState, + ) + .padding(horizontal = 16.dp), ) { - val scrollState = rememberScrollState() - Column( + val isError = state.sending is Async.Failure + val isFormEnabled = state.sending !is Async.Loading + // Title + Text( + text = stringResource(id = StringR.string.send_bug_report), modifier = Modifier - .verticalScroll( - state = scrollState, - ) - .padding(horizontal = 16.dp), + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 16.dp), + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold, + fontSize = 24.sp, + color = MaterialTheme.colorScheme.primary, + ) + // Form + Text( + text = stringResource(id = StringR.string.send_bug_report_description), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 16.dp), + fontSize = 16.sp, + color = MaterialTheme.colorScheme.primary, + ) + var descriptionFieldState by textFieldState( + stateValue = state.formState.description + ) + Column( + // modifier = Modifier.weight(1f), ) { - val isError = state.sending is Async.Failure - val isFormEnabled = state.sending !is Async.Loading - // Title - Text( - text = stringResource(id = StringR.string.send_bug_report), + OutlinedTextField( + value = descriptionFieldState, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 16.dp), - textAlign = TextAlign.Center, - fontWeight = FontWeight.Bold, - fontSize = 24.sp, - ) - // Form - Text( - text = stringResource(id = StringR.string.send_bug_report_description), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 16.dp), - fontSize = 16.sp, - ) - var descriptionFieldState by textFieldState( - stateValue = state.formState.description - ) - Column( - // modifier = Modifier.weight(1f), - ) { - OutlinedTextField( - value = descriptionFieldState, - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp), - enabled = isFormEnabled, - label = { - Text(text = stringResource(id = StringR.string.send_bug_report_placeholder)) - }, - supportingText = { - Text(text = stringResource(id = StringR.string.send_bug_report_description_in_english)) - }, - onValueChange = { - descriptionFieldState = it - eventSink(BugReportEvents.SetDescription(it)) - }, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Next - ), - // TODO Error text too short - ) - } - LabelledCheckbox( - checked = state.formState.sendLogs, - onCheckedChange = { eventSink(BugReportEvents.SetSendLog(it)) }, + .padding(top = 16.dp), enabled = isFormEnabled, - text = stringResource(id = StringR.string.send_bug_report_include_logs) + label = { + Text(text = stringResource(id = StringR.string.send_bug_report_placeholder)) + }, + supportingText = { + Text(text = stringResource(id = StringR.string.send_bug_report_description_in_english)) + }, + onValueChange = { + descriptionFieldState = it + eventSink(BugReportEvents.SetDescription(it)) + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Next + ), + // TODO Error text too short ) - if (state.hasCrashLogs) { - LabelledCheckbox( - checked = state.formState.sendCrashLogs, - onCheckedChange = { eventSink(BugReportEvents.SetSendCrashLog(it)) }, - enabled = isFormEnabled, - text = stringResource(id = StringR.string.send_bug_report_include_crash_logs) - ) - } + } + LabelledCheckbox( + checked = state.formState.sendLogs, + onCheckedChange = { eventSink(BugReportEvents.SetSendLog(it)) }, + enabled = isFormEnabled, + text = stringResource(id = StringR.string.send_bug_report_include_logs) + ) + if (state.hasCrashLogs) { LabelledCheckbox( - checked = state.formState.canContact, - onCheckedChange = { eventSink(BugReportEvents.SetCanContact(it)) }, + checked = state.formState.sendCrashLogs, + onCheckedChange = { eventSink(BugReportEvents.SetSendCrashLog(it)) }, enabled = isFormEnabled, - text = stringResource(id = StringR.string.you_may_contact_me) + text = stringResource(id = StringR.string.send_bug_report_include_crash_logs) ) - if (state.screenshotUri != null) { - LabelledCheckbox( - checked = state.formState.sendScreenshot, - onCheckedChange = { eventSink(BugReportEvents.SetSendScreenshot(it)) }, - enabled = isFormEnabled, - text = stringResource(id = StringR.string.send_bug_report_include_screenshot) - ) - if (state.formState.sendScreenshot) { - Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.Center - ) { - val context = LocalContext.current - val model = ImageRequest.Builder(context) - .data(state.screenshotUri) - .build() - AsyncImage( - modifier = Modifier.fillMaxWidth(fraction = 0.5f), - model = model, - contentDescription = null - ) - } + } + LabelledCheckbox( + checked = state.formState.canContact, + onCheckedChange = { eventSink(BugReportEvents.SetCanContact(it)) }, + enabled = isFormEnabled, + text = stringResource(id = StringR.string.you_may_contact_me) + ) + if (state.screenshotUri != null) { + LabelledCheckbox( + checked = state.formState.sendScreenshot, + onCheckedChange = { eventSink(BugReportEvents.SetSendScreenshot(it)) }, + enabled = isFormEnabled, + text = stringResource(id = StringR.string.send_bug_report_include_screenshot) + ) + if (state.formState.sendScreenshot) { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + val context = LocalContext.current + val model = ImageRequest.Builder(context) + .data(state.screenshotUri) + .build() + AsyncImage( + modifier = Modifier.fillMaxWidth(fraction = 0.5f), + model = model, + contentDescription = null + ) } } - // Submit - Button( - onClick = { eventSink(BugReportEvents.SendBugReport) }, - enabled = state.submitEnabled, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 32.dp) - ) { - Text(text = stringResource(id = StringR.string.action_send)) - } } - when (state.sending) { - is Async.Loading -> { - CircularProgressIndicator( - progress = state.sendingProgress, - modifier = Modifier.align(Alignment.Center) - ) - } - is Async.Failure -> ErrorDialog( - content = state.sending.error.toString(), + // Submit + Button( + onClick = { eventSink(BugReportEvents.SendBugReport) }, + enabled = state.submitEnabled, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 32.dp) + ) { + Text(text = stringResource(id = StringR.string.action_send)) + } + } + when (state.sending) { + is Async.Loading -> { + CircularProgressIndicator( + progress = state.sendingProgress, + modifier = Modifier.align(Alignment.Center) ) - else -> Unit } + is Async.Failure -> ErrorDialog( + content = state.sending.error.toString(), + ) + else -> Unit } } } -@Composable @Preview -fun BugReportContentPreview() { +@Composable +fun BugReportViewLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun BugReportViewDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { BugReportView( state = BugReportState(), ) diff --git a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/crash/CrashDataStore.kt b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/crash/CrashDataStore.kt index 5038d520b2..d9326841d8 100644 --- a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/crash/CrashDataStore.kt +++ b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/crash/CrashDataStore.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 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. @@ -16,59 +16,14 @@ package io.element.android.features.rageshake.crash -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.booleanPreferencesKey -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore -import io.element.android.libraries.core.bool.orFalse -import io.element.android.libraries.di.ApplicationContext import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.runBlocking -import javax.inject.Inject -private val Context.dataStore: DataStore by preferencesDataStore(name = "elementx_crash") +interface CrashDataStore { + fun setCrashData(crashData: String) -private val appHasCrashedKey = booleanPreferencesKey("appHasCrashed") -private val crashDataKey = stringPreferencesKey("crashData") + suspend fun resetAppHasCrashed() + fun appHasCrashed(): Flow + fun crashInfo(): Flow -class CrashDataStore @Inject constructor( - @ApplicationContext context: Context -) { - private val store = context.dataStore - - fun setCrashData(crashData: String) { - // Must block - runBlocking { - store.edit { prefs -> - prefs[appHasCrashedKey] = true - prefs[crashDataKey] = crashData - } - } - } - - suspend fun resetAppHasCrashed() { - store.edit { prefs -> - prefs[appHasCrashedKey] = false - } - } - - fun appHasCrashed(): Flow { - return store.data.map { prefs -> - prefs[appHasCrashedKey].orFalse() - } - } - - fun crashInfo(): Flow { - return store.data.map { prefs -> - prefs[crashDataKey].orEmpty() - } - } - - suspend fun reset() { - store.edit { it.clear() } - } + suspend fun reset() } diff --git a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/crash/PreferencesCrashDataStore.kt b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/crash/PreferencesCrashDataStore.kt new file mode 100644 index 0000000000..70f258fd02 --- /dev/null +++ b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/crash/PreferencesCrashDataStore.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2022 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 io.element.android.features.rageshake.crash + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.bool.orFalse +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.runBlocking +import javax.inject.Inject + +private val Context.dataStore: DataStore by preferencesDataStore(name = "elementx_crash") + +private val appHasCrashedKey = booleanPreferencesKey("appHasCrashed") +private val crashDataKey = stringPreferencesKey("crashData") + +@ContributesBinding(AppScope::class) +class PreferencesCrashDataStore @Inject constructor( + @ApplicationContext context: Context +) : CrashDataStore { + private val store = context.dataStore + + override fun setCrashData(crashData: String) { + // Must block + runBlocking { + store.edit { prefs -> + prefs[appHasCrashedKey] = true + prefs[crashDataKey] = crashData + } + } + } + + override suspend fun resetAppHasCrashed() { + store.edit { prefs -> + prefs[appHasCrashedKey] = false + } + } + + override fun appHasCrashed(): Flow { + return store.data.map { prefs -> + prefs[appHasCrashedKey].orFalse() + } + } + + override fun crashInfo(): Flow { + return store.data.map { prefs -> + prefs[crashDataKey].orEmpty() + } + } + + override suspend fun reset() { + store.edit { it.clear() } + } +} diff --git a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/crash/VectorUncaughtExceptionHandler.kt b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/crash/VectorUncaughtExceptionHandler.kt index dfd09a203e..942d49d531 100644 --- a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/crash/VectorUncaughtExceptionHandler.kt +++ b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/crash/VectorUncaughtExceptionHandler.kt @@ -26,7 +26,7 @@ import java.io.StringWriter class VectorUncaughtExceptionHandler( context: Context ) : Thread.UncaughtExceptionHandler { - private val crashDataStore = CrashDataStore(context) + private val crashDataStore = PreferencesCrashDataStore(context) private var previousHandler: Thread.UncaughtExceptionHandler? = null /** diff --git a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/crash/ui/CrashDetectionScreen.kt b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/crash/ui/CrashDetectionScreen.kt index e3c58ab515..fb1a6b2d50 100644 --- a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/crash/ui/CrashDetectionScreen.kt +++ b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/crash/ui/CrashDetectionScreen.kt @@ -20,6 +20,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.utils.LogCompositions import io.element.android.libraries.ui.strings.R as StringR @@ -67,7 +69,14 @@ fun CrashDetectionContent( @Preview @Composable -fun CrashDetectionContentPreview() { +fun CrashDetectionContentLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun CrashDetectionContentDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { CrashDetectionContent( state = CrashDetectionState() ) diff --git a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/detection/RageshakeDetectionPresenter.kt b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/detection/RageshakeDetectionPresenter.kt index c91d584021..dc0155877e 100644 --- a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/detection/RageshakeDetectionPresenter.kt +++ b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/detection/RageshakeDetectionPresenter.kt @@ -95,12 +95,12 @@ class RageshakeDetectionPresenter @Inject constructor( private fun handleRageShake(start: Boolean, state: RageshakeDetectionState, takeScreenshot: MutableState) { if (start) { rageShake.start(state.preferenceState.sensitivity) - rageShake.interceptor = { + rageShake.setInterceptor { takeScreenshot.value = true } } else { rageShake.stop() - rageShake.interceptor = null + rageShake.setInterceptor(null) } } diff --git a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/detection/RageshakeDetectionView.kt b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/detection/RageshakeDetectionView.kt index de99427763..bc8002b978 100644 --- a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/detection/RageshakeDetectionView.kt +++ b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/detection/RageshakeDetectionView.kt @@ -27,6 +27,8 @@ import io.element.android.features.rageshake.screenshot.ImageResult import io.element.android.features.rageshake.screenshot.screenshot import io.element.android.libraries.androidutils.hardware.vibrate import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.utils.LogCompositions import io.element.android.libraries.designsystem.utils.OnLifecycleEvent import io.element.android.libraries.ui.strings.R as StringR @@ -99,6 +101,13 @@ fun RageshakeDialogContent( @Preview @Composable -fun RageshakeDialogContentPreview() { +fun RageshakeDialogContentLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun RageshakeDialogContentDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { RageshakeDialogContent() } diff --git a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/logs/VectorFileLogger.kt b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/logs/VectorFileLogger.kt index 1d8d0b349b..5df72e29f7 100644 --- a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/logs/VectorFileLogger.kt +++ b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/logs/VectorFileLogger.kt @@ -43,8 +43,8 @@ class VectorFileLogger( ) : Timber.Tree() { companion object { - fun getFromTimber(): VectorFileLogger { - return Timber.forest().filterIsInstance().first() + fun getFromTimber(): VectorFileLogger? { + return Timber.forest().filterIsInstance().firstOrNull() } private const val SIZE_20MB = 20 * 1024 * 1024 diff --git a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/preferences/RageshakePreferencesView.kt b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/preferences/RageshakePreferencesView.kt index ce5d9bcab8..815340b8b2 100644 --- a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/preferences/RageshakePreferencesView.kt +++ b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/preferences/RageshakePreferencesView.kt @@ -27,6 +27,8 @@ import io.element.android.libraries.designsystem.components.preferences.Preferen import io.element.android.libraries.designsystem.components.preferences.PreferenceSlide import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch import io.element.android.libraries.designsystem.components.preferences.PreferenceText +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.ui.strings.R as StringR @Composable @@ -73,14 +75,28 @@ fun RageshakePreferencesView( } } -@Composable @Preview -fun RageshakePreferencesViewPreview() { +@Composable +fun RageshakePreferencesViewLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun RageshakePreferencesViewDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { RageshakePreferencesView(RageshakePreferencesState(isEnabled = true, isSupported = true, sensitivity = 0.5f)) } -@Composable @Preview -fun RageshakePreferenceNotSupportedPreview() { +@Composable +fun RageshakePreferencesViewNotSupportedLightPreview() = ElementPreviewLight { ContentNotSupportedToPreview() } + +@Preview +@Composable +fun RageshakePreferencesViewNotSupportedDarkPreview() = ElementPreviewDark { ContentNotSupportedToPreview() } + +@Composable +private fun ContentNotSupportedToPreview() { RageshakePreferencesView(RageshakePreferencesState(isEnabled = true, isSupported = false, sensitivity = 0.5f)) } diff --git a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/rageshake/DefaultRageShake.kt b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/rageshake/DefaultRageShake.kt new file mode 100644 index 0000000000..a0963000d8 --- /dev/null +++ b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/rageshake/DefaultRageShake.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2022 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 io.element.android.features.rageshake.rageshake + +import android.content.Context +import android.hardware.Sensor +import android.hardware.SensorManager +import androidx.core.content.getSystemService +import com.squareup.anvil.annotations.ContributesBinding +import com.squareup.seismic.ShakeDetector +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.SingleIn +import javax.inject.Inject + +@SingleIn(AppScope::class) +@ContributesBinding(AppScope::class, RageShake::class) +class DefaultRageShake @Inject constructor( + @ApplicationContext context: Context, +) : ShakeDetector.Listener, RageShake { + + private var sensorManager = context.getSystemService() + private var shakeDetector: ShakeDetector? = null + private var interceptor: (() -> Unit)? = null + + override fun setInterceptor(interceptor: (() -> Unit)?) { + this.interceptor = interceptor + } + + /** + * Check if the feature is available on this device. + */ + override fun isAvailable(): Boolean { + return sensorManager?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null + } + + override fun start(sensitivity: Float) { + sensorManager?.let { + shakeDetector = ShakeDetector(this).apply { + start(it, SensorManager.SENSOR_DELAY_GAME) + } + setSensitivity(sensitivity) + } + } + + override fun stop() { + shakeDetector?.stop() + } + + /** + * sensitivity will be {0, O.25, 0.5, 0.75, 1} and converted to + * [ShakeDetector.SENSITIVITY_LIGHT (=11), ShakeDetector.SENSITIVITY_HARD (=15)]. + */ + override fun setSensitivity(sensitivity: Float) { + shakeDetector?.setSensitivity( + ShakeDetector.SENSITIVITY_LIGHT + (sensitivity * 4).toInt() + ) + } + + override fun hearShake() { + interceptor?.invoke() + } +} diff --git a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/rageshake/PreferencesRageshakeDataStore.kt b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/rageshake/PreferencesRageshakeDataStore.kt new file mode 100644 index 0000000000..4643038536 --- /dev/null +++ b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/rageshake/PreferencesRageshakeDataStore.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2022 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 io.element.android.features.rageshake.rageshake + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.floatPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.bool.orTrue +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +private val Context.dataStore: DataStore by preferencesDataStore(name = "elementx_rageshake") + +private val enabledKey = booleanPreferencesKey("enabled") +private val sensitivityKey = floatPreferencesKey("sensitivity") + +@ContributesBinding(AppScope::class) +class PreferencesRageshakeDataStore @Inject constructor( + @ApplicationContext context: Context +) : RageshakeDataStore { + private val store = context.dataStore + + override fun isEnabled(): Flow { + return store.data.map { prefs -> + prefs[enabledKey].orTrue() + } + } + + override suspend fun setIsEnabled(isEnabled: Boolean) { + store.edit { prefs -> + prefs[enabledKey] = isEnabled + } + } + + override fun sensitivity(): Flow { + return store.data.map { prefs -> + prefs[sensitivityKey] ?: 0.5f + } + } + + override suspend fun setSensitivity(sensitivity: Float) { + store.edit { prefs -> + prefs[sensitivityKey] = sensitivity + } + } + + override suspend fun reset() { + store.edit { it.clear() } + } +} diff --git a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/rageshake/RageShake.kt b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/rageshake/RageShake.kt index 691da5dbe2..d9150b5ecd 100644 --- a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/rageshake/RageShake.kt +++ b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/rageshake/RageShake.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 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. @@ -16,57 +16,21 @@ package io.element.android.features.rageshake.rageshake -import android.content.Context -import android.hardware.Sensor -import android.hardware.SensorManager -import androidx.core.content.getSystemService -import com.squareup.seismic.ShakeDetector -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext -import io.element.android.libraries.di.SingleIn -import javax.inject.Inject - -@SingleIn(AppScope::class) -class RageShake @Inject constructor( - @ApplicationContext context: Context, -) : ShakeDetector.Listener { - - private var sensorManager = context.getSystemService() - private var shakeDetector: ShakeDetector? = null - - var interceptor: (() -> Unit)? = null - +interface RageShake { /** * Check if the feature is available on this device. */ - fun isAvailable(): Boolean { - return sensorManager?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null - } + fun isAvailable(): Boolean - fun start(sensitivity: Float) { - sensorManager?.let { - shakeDetector = ShakeDetector(this).apply { - start(it, SensorManager.SENSOR_DELAY_GAME) - } - setSensitivity(sensitivity) - } - } + fun start(sensitivity: Float) - fun stop() { - shakeDetector?.stop() - } + fun stop() /** * sensitivity will be {0, O.25, 0.5, 0.75, 1} and converted to * [ShakeDetector.SENSITIVITY_LIGHT (=11), ShakeDetector.SENSITIVITY_HARD (=15)]. */ - fun setSensitivity(sensitivity: Float) { - shakeDetector?.setSensitivity( - ShakeDetector.SENSITIVITY_LIGHT + (sensitivity * 4).toInt() - ) - } + fun setSensitivity(sensitivity: Float) - override fun hearShake() { - interceptor?.invoke() - } + fun setInterceptor(interceptor: (() -> Unit)?) } diff --git a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/rageshake/RageshakeDataStore.kt b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/rageshake/RageshakeDataStore.kt index 1bf133d42f..25a7080354 100644 --- a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/rageshake/RageshakeDataStore.kt +++ b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/rageshake/RageshakeDataStore.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 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. @@ -16,54 +16,16 @@ package io.element.android.features.rageshake.rageshake -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.booleanPreferencesKey -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.floatPreferencesKey -import androidx.datastore.preferences.preferencesDataStore -import io.element.android.libraries.core.bool.orTrue -import io.element.android.libraries.di.ApplicationContext import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import javax.inject.Inject -private val Context.dataStore: DataStore by preferencesDataStore(name = "elementx_rageshake") +interface RageshakeDataStore { + fun isEnabled(): Flow -private val enabledKey = booleanPreferencesKey("enabled") -private val sensitivityKey = floatPreferencesKey("sensitivity") + suspend fun setIsEnabled(isEnabled: Boolean) -class RageshakeDataStore @Inject constructor( - @ApplicationContext context: Context -) { - private val store = context.dataStore + fun sensitivity(): Flow - fun isEnabled(): Flow { - return store.data.map { prefs -> - prefs[enabledKey].orTrue() - } - } + suspend fun setSensitivity(sensitivity: Float) - suspend fun setIsEnabled(isEnabled: Boolean) { - store.edit { prefs -> - prefs[enabledKey] = isEnabled - } - } - - fun sensitivity(): Flow { - return store.data.map { prefs -> - prefs[sensitivityKey] ?: 0.5f - } - } - - suspend fun setSensitivity(sensitivity: Float) { - store.edit { prefs -> - prefs[sensitivityKey] = sensitivity - } - } - - suspend fun reset() { - store.edit { it.clear() } - } + suspend fun reset() } diff --git a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/reporter/BugReporter.kt b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/reporter/BugReporter.kt old mode 100755 new mode 100644 index 6cf888a44e..3acd22107d --- a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/reporter/BugReporter.kt +++ b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/reporter/BugReporter.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 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. @@ -16,126 +16,9 @@ package io.element.android.features.rageshake.reporter -import android.content.Context -import android.os.Build -import io.element.android.features.rageshake.R -import io.element.android.features.rageshake.crash.CrashDataStore -import io.element.android.features.rageshake.logs.VectorFileLogger -import io.element.android.features.rageshake.screenshot.ScreenshotHolder -import io.element.android.libraries.androidutils.file.compressFile -import io.element.android.libraries.androidutils.file.safeDelete -import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.core.extensions.toOnOff -import io.element.android.libraries.core.mimetype.MimeTypes -import io.element.android.libraries.di.ApplicationContext import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import okhttp3.Call -import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody.Companion.asRequestBody -import okhttp3.Response -import org.json.JSONException -import org.json.JSONObject -import timber.log.Timber -import java.io.File -import java.io.IOException -import java.io.OutputStreamWriter -import java.net.HttpURLConnection -import java.util.Locale -import javax.inject.Inject - -/** - * BugReporter creates and sends the bug reports. - */ -class BugReporter @Inject constructor( - @ApplicationContext private val context: Context, - private val screenshotHolder: ScreenshotHolder, - private val crashDataStore: CrashDataStore, - private val coroutineDispatchers: CoroutineDispatchers, - /* - private val activeSessionHolder: ActiveSessionHolder, - private val versionProvider: VersionProvider, - private val vectorPreferences: VectorPreferences, - private val vectorFileLogger: VectorFileLogger, - private val systemLocaleProvider: SystemLocaleProvider, - private val matrix: Matrix, - private val buildMeta: BuildMeta, - private val processInfo: ProcessInfo, - private val sdkIntProvider: BuildVersionSdkIntProvider, - private val vectorLocale: VectorLocaleProvider, - */ -) { - var inMultiWindowMode = false - - companion object { - // filenames - private const val LOG_CAT_ERROR_FILENAME = "logcatError.log" - private const val LOG_CAT_FILENAME = "logcat.log" - private const val KEY_REQUESTS_FILENAME = "keyRequests.log" - - private const val BUFFER_SIZE = 1024 * 1024 * 50 - } - - // the http client - private val mOkHttpClient = OkHttpClient() - - // the pending bug report call - private var mBugReportCall: Call? = null - - // boolean to cancel the bug report - private val mIsCancelled = false - - /* - val adapter = MatrixJsonParser.getMoshi() - .adapter(Types.newParameterizedType(Map::class.java, String::class.java, Any::class.java)) - */ - - private val LOGCAT_CMD_ERROR = arrayOf( - "logcat", // /< Run 'logcat' command - "-d", // /< Dump the log rather than continue outputting it - "-v", // formatting - "threadtime", // include timestamps - "AndroidRuntime:E " + // /< Pick all AndroidRuntime errors (such as uncaught exceptions)"communicatorjni:V " + ///< All communicatorjni logging - "libcommunicator:V " + // /< All libcommunicator logging - "DEBUG:V " + // /< All DEBUG logging - which includes native land crashes (seg faults, etc) - "*:S" // /< Everything else silent, so don't pick it.. - ) - - private val LOGCAT_CMD_DEBUG = arrayOf("logcat", "-d", "-v", "threadtime", "*:*") - - /** - * Bug report upload listener. - */ - interface IMXBugReportListener { - /** - * The bug report has been cancelled. - */ - fun onUploadCancelled() - - /** - * The bug report upload failed. - * - * @param reason the failure reason - */ - fun onUploadFailed(reason: String?) - - /** - * The upload progress (in percent). - * - * @param progress the upload progress - */ - fun onProgress(progress: Int) - - /** - * The bug report upload succeeded. - */ - fun onUploadSucceed(reportUrl: String?) - } +interface BugReporter { /** * Send a bug report. * @@ -162,388 +45,6 @@ class BugReporter @Inject constructor( serverVersion: String, canContact: Boolean = false, customFields: Map? = null, - listener: IMXBugReportListener? - ) { - // enumerate files to delete - val mBugReportFiles: MutableList = ArrayList() - - coroutineScope.launch { - var serverError: String? = null - var reportURL: String? = null - withContext(coroutineDispatchers.io) { - var bugDescription = theBugDescription - val crashCallStack = crashDataStore.crashInfo().first() - - if (crashCallStack.isNotEmpty() && withCrashLogs) { - bugDescription += "\n\n\n\n--------------------------------- crash call stack ---------------------------------\n" - bugDescription += crashCallStack - } - - val gzippedFiles = ArrayList() - - val vectorFileLogger = VectorFileLogger.getFromTimber() - if (withDevicesLogs) { - val files = vectorFileLogger.getLogFiles() - files.mapNotNullTo(gzippedFiles) { f -> - if (!mIsCancelled) { - compressFile(f) - } else { - null - } - } - } - - if (!mIsCancelled && (withCrashLogs || withDevicesLogs)) { - val gzippedLogcat = saveLogCat(false) - - if (null != gzippedLogcat) { - if (gzippedFiles.size == 0) { - gzippedFiles.add(gzippedLogcat) - } else { - gzippedFiles.add(0, gzippedLogcat) - } - } - } - - /* - activeSessionHolder.getSafeActiveSession() - ?.takeIf { !mIsCancelled && withKeyRequestHistory } - ?.cryptoService() - ?.getGossipingEvents() - ?.let { GossipingEventsSerializer().serialize(it) } - ?.toByteArray() - ?.let { rawByteArray -> - File(context.cacheDir.absolutePath, KEY_REQUESTS_FILENAME) - .also { - it.outputStream() - .use { os -> os.write(rawByteArray) } - } - } - ?.let { compressFile(it) } - ?.let { gzippedFiles.add(it) } - */ - - var deviceId = "undefined" - var userId = "undefined" - var olmVersion = "undefined" - - /* - activeSessionHolder.getSafeActiveSession()?.let { session -> - userId = session.myUserId - deviceId = session.sessionParams.deviceId ?: "undefined" - olmVersion = session.cryptoService().getCryptoVersion(context, true) - } - */ - - if (!mIsCancelled) { - val text = when (reportType) { - ReportType.BUG_REPORT -> bugDescription - ReportType.SUGGESTION -> "[Suggestion] $bugDescription" - ReportType.SPACE_BETA_FEEDBACK -> "[spaces-feedback] $bugDescription" - ReportType.THREADS_BETA_FEEDBACK -> "[threads-feedback] $bugDescription" - ReportType.AUTO_UISI_SENDER, - ReportType.AUTO_UISI -> bugDescription - } - - // build the multi part request - val builder = BugReporterMultipartBody.Builder() - .addFormDataPart("text", text) - .addFormDataPart("app", rageShakeAppNameForReport(reportType)) - // .addFormDataPart("user_agent", matrix.getUserAgent()) - .addFormDataPart("user_id", userId) - .addFormDataPart("can_contact", canContact.toString()) - .addFormDataPart("device_id", deviceId) - // .addFormDataPart("version", versionProvider.getVersion(longFormat = true)) - // .addFormDataPart("branch_name", buildMeta.gitBranchName) - // .addFormDataPart("matrix_sdk_version", Matrix.getSdkVersion()) - .addFormDataPart("olm_version", olmVersion) - .addFormDataPart("device", Build.MODEL.trim()) - // .addFormDataPart("verbose_log", vectorPreferences.labAllowedExtendedLogging().toOnOff()) - .addFormDataPart("multi_window", inMultiWindowMode.toOnOff()) - // .addFormDataPart( - // "os", Build.VERSION.RELEASE + " (API " + sdkIntProvider.get() + ") " + - // Build.VERSION.INCREMENTAL + "-" + Build.VERSION.CODENAME - // ) - .addFormDataPart("locale", Locale.getDefault().toString()) - // .addFormDataPart("app_language", vectorLocale.applicationLocale.toString()) - // .addFormDataPart("default_app_language", systemLocaleProvider.getSystemLocale().toString()) - // .addFormDataPart("theme", ThemeUtils.getApplicationTheme(context)) - .addFormDataPart("server_version", serverVersion) - .apply { - customFields?.forEach { (name, value) -> - addFormDataPart(name, value) - } - } - - // add the gzipped files - for (file in gzippedFiles) { - builder.addFormDataPart("compressed-log", file.name, file.asRequestBody(MimeTypes.OctetStream.toMediaTypeOrNull())) - } - - mBugReportFiles.addAll(gzippedFiles) - - if (withScreenshot) { - screenshotHolder.getFile()?.let { screenshotFile -> - try { - builder.addFormDataPart( - "file", - screenshotFile.name, screenshotFile.asRequestBody(MimeTypes.OctetStream.toMediaTypeOrNull()) - ) - } catch (e: Exception) { - Timber.e(e, "## sendBugReport() : fail to write screenshot") - } - } - } - - // add some github labels - // builder.addFormDataPart("label", buildMeta.versionName) - // builder.addFormDataPart("label", buildMeta.flavorDescription) - // builder.addFormDataPart("label", buildMeta.gitBranchName) - - // Possible values for BuildConfig.BUILD_TYPE: "debug", "nightly", "release". - // builder.addFormDataPart("label", BuildConfig.BUILD_TYPE) - - when (reportType) { - ReportType.BUG_REPORT -> { - /* nop */ - } - ReportType.SUGGESTION -> builder.addFormDataPart("label", "[Suggestion]") - ReportType.SPACE_BETA_FEEDBACK -> builder.addFormDataPart("label", "spaces-feedback") - ReportType.THREADS_BETA_FEEDBACK -> builder.addFormDataPart("label", "threads-feedback") - ReportType.AUTO_UISI -> { - builder.addFormDataPart("label", "Z-UISI") - builder.addFormDataPart("label", "android") - builder.addFormDataPart("label", "uisi-recipient") - } - ReportType.AUTO_UISI_SENDER -> { - builder.addFormDataPart("label", "Z-UISI") - builder.addFormDataPart("label", "android") - builder.addFormDataPart("label", "uisi-sender") - } - } - - if (crashCallStack.isNotEmpty() && withCrashLogs) { - builder.addFormDataPart("label", "crash") - } - - val requestBody = builder.build() - - // add a progress listener - requestBody.setWriteListener { totalWritten, contentLength -> - val percentage = if (-1L != contentLength) { - if (totalWritten > contentLength) { - 100 - } else { - (totalWritten * 100 / contentLength).toInt() - } - } else { - 0 - } - - if (mIsCancelled && null != mBugReportCall) { - mBugReportCall!!.cancel() - } - - Timber.v("## onWrite() : $percentage%") - try { - listener?.onProgress(percentage) - } catch (e: Exception) { - Timber.e(e, "## onProgress() : failed") - } - } - - // build the request - val request = Request.Builder() - .url(context.getString(R.string.bug_report_url)) - .post(requestBody) - .build() - - var responseCode = HttpURLConnection.HTTP_INTERNAL_ERROR - var response: Response? = null - var errorMessage: String? = null - - // trigger the request - try { - mBugReportCall = mOkHttpClient.newCall(request) - response = mBugReportCall!!.execute() - responseCode = response.code - } catch (e: Exception) { - Timber.e(e, "response") - errorMessage = e.localizedMessage - } - - // if the upload failed, try to retrieve the reason - if (responseCode != HttpURLConnection.HTTP_OK) { - if (null != errorMessage) { - serverError = "Failed with error $errorMessage" - } else if (response?.body == null) { - serverError = "Failed with error $responseCode" - } else { - try { - val inputStream = response.body!!.byteStream() - - serverError = inputStream.use { - buildString { - var ch = it.read() - while (ch != -1) { - append(ch.toChar()) - ch = it.read() - } - } - } - - // check if the error message - serverError?.let { - try { - val responseJSON = JSONObject(it) - serverError = responseJSON.getString("error") - } catch (e: JSONException) { - Timber.e(e, "doInBackground ; Json conversion failed") - } - } - - // should never happen - if (null == serverError) { - serverError = "Failed with error $responseCode" - } - } catch (e: Exception) { - Timber.e(e, "## sendBugReport() : failed to parse error") - } - } - } else { - /* - reportURL = response?.body?.string()?.let { stringBody -> - adapter.fromJson(stringBody)?.get("report_url")?.toString() - } - */ - } - } - } - - withContext(coroutineDispatchers.main) { - mBugReportCall = null - - // delete when the bug report has been successfully sent - for (file in mBugReportFiles) { - file.safeDelete() - } - - if (null != listener) { - try { - if (mIsCancelled) { - listener.onUploadCancelled() - } else if (null == serverError) { - listener.onUploadSucceed(reportURL) - } else { - listener.onUploadFailed(serverError) - } - } catch (e: Exception) { - Timber.e(e, "## onPostExecute() : failed") - } - } - } - } - } - - /** - * Send a bug report either with email or with Vector. - */ - /* TODO Remove - fun openBugReportScreen(activity: FragmentActivity, reportType: ReportType = ReportType.BUG_REPORT) { - screenshot = takeScreenshot(activity) - logDbInfo() - logProcessInfo() - logOtherInfo() - activity.startActivity(BugReportActivity.intent(activity, reportType)) - } - */ - - // private fun logOtherInfo() { - // Timber.i("SyncThread state: " + activeSessionHolder.getSafeActiveSession()?.syncService()?.getSyncState()) - // } - - // private fun logDbInfo() { - // val dbInfo = matrix.debugService().getDbUsageInfo() - // Timber.i(dbInfo) - // } - - // private fun logProcessInfo() { - // val pInfo = processInfo.getInfo() - // Timber.i(pInfo) - // } - - private fun rageShakeAppNameForReport(reportType: ReportType): String { - // As per https://github.com/matrix-org/rageshake - // app: Identifier for the application (eg 'riot-web'). - // Should correspond to a mapping configured in the configuration file for github issue reporting to work. - // (see R.string.bug_report_url for configured RS server) - return context.getString( - when (reportType) { - ReportType.AUTO_UISI_SENDER, - ReportType.AUTO_UISI -> R.string.bug_report_auto_uisi_app_name - else -> R.string.bug_report_app_name - } - ) - } - - // ============================================================================================================== - // Logcat management - // ============================================================================================================== - - /** - * Save the logcat. - * - * @param isErrorLogcat true to save the error logcat - * @return the file if the operation succeeds - */ - private fun saveLogCat(isErrorLogcat: Boolean): File? { - val logCatErrFile = File(context.cacheDir.absolutePath, if (isErrorLogcat) LOG_CAT_ERROR_FILENAME else LOG_CAT_FILENAME) - - if (logCatErrFile.exists()) { - logCatErrFile.safeDelete() - } - - try { - logCatErrFile.writer().use { - getLogCatError(it, isErrorLogcat) - } - - return compressFile(logCatErrFile) - } catch (error: OutOfMemoryError) { - Timber.e(error, "## saveLogCat() : fail to write logcat$error") - } catch (e: Exception) { - Timber.e(e, "## saveLogCat() : fail to write logcat$e") - } - - return null - } - - /** - * Retrieves the logs. - * - * @param streamWriter the stream writer - * @param isErrorLogCat true to save the error logs - */ - private fun getLogCatError(streamWriter: OutputStreamWriter, isErrorLogCat: Boolean) { - val logcatProc: Process - - try { - logcatProc = Runtime.getRuntime().exec(if (isErrorLogCat) LOGCAT_CMD_ERROR else LOGCAT_CMD_DEBUG) - } catch (e1: IOException) { - return - } - - try { - val separator = System.getProperty("line.separator") - logcatProc.inputStream - .reader() - .buffered(BUFFER_SIZE) - .forEachLine { line -> - streamWriter.append(line) - streamWriter.append(separator) - } - } catch (e: IOException) { - Timber.e(e, "getLog fails") - } - } + listener: BugReporterListener? + ) } diff --git a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/reporter/BugReporterListener.kt b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/reporter/BugReporterListener.kt new file mode 100644 index 0000000000..3259034ad7 --- /dev/null +++ b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/reporter/BugReporterListener.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 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 io.element.android.features.rageshake.reporter + +/** + * Bug report upload listener. + */ +interface BugReporterListener { + /** + * The bug report has been cancelled. + */ + fun onUploadCancelled() + + /** + * The bug report upload failed. + * + * @param reason the failure reason + */ + fun onUploadFailed(reason: String?) + + /** + * The upload progress (in percent). + * + * @param progress the upload progress + */ + fun onProgress(progress: Int) + + /** + * The bug report upload succeeded. + */ + fun onUploadSucceed(reportUrl: String?) +} diff --git a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/reporter/DefaultBugReporter.kt b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/reporter/DefaultBugReporter.kt new file mode 100755 index 0000000000..cfc0c68561 --- /dev/null +++ b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/reporter/DefaultBugReporter.kt @@ -0,0 +1,525 @@ +/* + * Copyright (c) 2022 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 io.element.android.features.rageshake.reporter + +import android.content.Context +import android.os.Build +import androidx.core.net.toFile +import androidx.core.net.toUri +import io.element.android.features.rageshake.R +import io.element.android.features.rageshake.crash.CrashDataStore +import io.element.android.features.rageshake.logs.VectorFileLogger +import io.element.android.features.rageshake.screenshot.ScreenshotHolder +import io.element.android.libraries.androidutils.file.compressFile +import io.element.android.libraries.androidutils.file.safeDelete +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.extensions.toOnOff +import io.element.android.libraries.core.mimetype.MimeTypes +import io.element.android.libraries.di.ApplicationContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.Call +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.Response +import org.json.JSONException +import org.json.JSONObject +import timber.log.Timber +import java.io.File +import java.io.IOException +import java.io.OutputStreamWriter +import java.net.HttpURLConnection +import java.util.Locale +import javax.inject.Inject + +/** + * BugReporter creates and sends the bug reports. + */ +class DefaultBugReporter @Inject constructor( + @ApplicationContext private val context: Context, + private val screenshotHolder: ScreenshotHolder, + private val crashDataStore: CrashDataStore, + private val coroutineDispatchers: CoroutineDispatchers, + /* + private val activeSessionHolder: ActiveSessionHolder, + private val versionProvider: VersionProvider, + private val vectorPreferences: VectorPreferences, + private val vectorFileLogger: VectorFileLogger, + private val systemLocaleProvider: SystemLocaleProvider, + private val matrix: Matrix, + private val buildMeta: BuildMeta, + private val processInfo: ProcessInfo, + private val sdkIntProvider: BuildVersionSdkIntProvider, + private val vectorLocale: VectorLocaleProvider, + */ +) : BugReporter { + var inMultiWindowMode = false + + companion object { + // filenames + private const val LOG_CAT_ERROR_FILENAME = "logcatError.log" + private const val LOG_CAT_FILENAME = "logcat.log" + private const val KEY_REQUESTS_FILENAME = "keyRequests.log" + + private const val BUFFER_SIZE = 1024 * 1024 * 50 + } + + // the http client + private val mOkHttpClient = OkHttpClient() + + // the pending bug report call + private var mBugReportCall: Call? = null + + // boolean to cancel the bug report + private val mIsCancelled = false + + /* + val adapter = MatrixJsonParser.getMoshi() + .adapter(Types.newParameterizedType(Map::class.java, String::class.java, Any::class.java)) + */ + + private val LOGCAT_CMD_ERROR = arrayOf( + "logcat", // /< Run 'logcat' command + "-d", // /< Dump the log rather than continue outputting it + "-v", // formatting + "threadtime", // include timestamps + "AndroidRuntime:E " + // /< Pick all AndroidRuntime errors (such as uncaught exceptions)"communicatorjni:V " + ///< All communicatorjni logging + "libcommunicator:V " + // /< All libcommunicator logging + "DEBUG:V " + // /< All DEBUG logging - which includes native land crashes (seg faults, etc) + "*:S" // /< Everything else silent, so don't pick it.. + ) + + private val LOGCAT_CMD_DEBUG = arrayOf("logcat", "-d", "-v", "threadtime", "*:*") + + /** + * Send a bug report. + * + * @param coroutineScope The coroutine scope + * @param reportType The report type (bug, suggestion, feedback) + * @param withDevicesLogs true to include the device log + * @param withCrashLogs true to include the crash logs + * @param withKeyRequestHistory true to include the crash logs + * @param withScreenshot true to include the screenshot + * @param theBugDescription the bug description + * @param serverVersion version of the server + * @param canContact true if the user opt in to be contacted directly + * @param customFields fields which will be sent with the report + * @param listener the listener + */ + override fun sendBugReport( + coroutineScope: CoroutineScope, + reportType: ReportType, + withDevicesLogs: Boolean, + withCrashLogs: Boolean, + withKeyRequestHistory: Boolean, + withScreenshot: Boolean, + theBugDescription: String, + serverVersion: String, + canContact: Boolean, + customFields: Map?, + listener: BugReporterListener? + ) { + // enumerate files to delete + val mBugReportFiles: MutableList = ArrayList() + + coroutineScope.launch { + var serverError: String? = null + var reportURL: String? = null + withContext(coroutineDispatchers.io) { + var bugDescription = theBugDescription + val crashCallStack = crashDataStore.crashInfo().first() + + if (crashCallStack.isNotEmpty() && withCrashLogs) { + bugDescription += "\n\n\n\n--------------------------------- crash call stack ---------------------------------\n" + bugDescription += crashCallStack + } + + val gzippedFiles = ArrayList() + + val vectorFileLogger = VectorFileLogger.getFromTimber() + if (withDevicesLogs && vectorFileLogger != null) { + val files = vectorFileLogger.getLogFiles() + files.mapNotNullTo(gzippedFiles) { f -> + if (!mIsCancelled) { + compressFile(f) + } else { + null + } + } + } + + if (!mIsCancelled && (withCrashLogs || withDevicesLogs)) { + val gzippedLogcat = saveLogCat(false) + + if (null != gzippedLogcat) { + if (gzippedFiles.size == 0) { + gzippedFiles.add(gzippedLogcat) + } else { + gzippedFiles.add(0, gzippedLogcat) + } + } + } + + /* + activeSessionHolder.getSafeActiveSession() + ?.takeIf { !mIsCancelled && withKeyRequestHistory } + ?.cryptoService() + ?.getGossipingEvents() + ?.let { GossipingEventsSerializer().serialize(it) } + ?.toByteArray() + ?.let { rawByteArray -> + File(context.cacheDir.absolutePath, KEY_REQUESTS_FILENAME) + .also { + it.outputStream() + .use { os -> os.write(rawByteArray) } + } + } + ?.let { compressFile(it) } + ?.let { gzippedFiles.add(it) } + */ + + var deviceId = "undefined" + var userId = "undefined" + var olmVersion = "undefined" + + /* + activeSessionHolder.getSafeActiveSession()?.let { session -> + userId = session.myUserId + deviceId = session.sessionParams.deviceId ?: "undefined" + olmVersion = session.cryptoService().getCryptoVersion(context, true) + } + */ + + if (!mIsCancelled) { + val text = when (reportType) { + ReportType.BUG_REPORT -> bugDescription + ReportType.SUGGESTION -> "[Suggestion] $bugDescription" + ReportType.SPACE_BETA_FEEDBACK -> "[spaces-feedback] $bugDescription" + ReportType.THREADS_BETA_FEEDBACK -> "[threads-feedback] $bugDescription" + ReportType.AUTO_UISI_SENDER, + ReportType.AUTO_UISI -> bugDescription + } + + // build the multi part request + val builder = BugReporterMultipartBody.Builder() + .addFormDataPart("text", text) + .addFormDataPart("app", rageShakeAppNameForReport(reportType)) + // .addFormDataPart("user_agent", matrix.getUserAgent()) + .addFormDataPart("user_id", userId) + .addFormDataPart("can_contact", canContact.toString()) + .addFormDataPart("device_id", deviceId) + // .addFormDataPart("version", versionProvider.getVersion(longFormat = true)) + // .addFormDataPart("branch_name", buildMeta.gitBranchName) + // .addFormDataPart("matrix_sdk_version", Matrix.getSdkVersion()) + .addFormDataPart("olm_version", olmVersion) + .addFormDataPart("device", Build.MODEL.trim()) + // .addFormDataPart("verbose_log", vectorPreferences.labAllowedExtendedLogging().toOnOff()) + .addFormDataPart("multi_window", inMultiWindowMode.toOnOff()) + // .addFormDataPart( + // "os", Build.VERSION.RELEASE + " (API " + sdkIntProvider.get() + ") " + + // Build.VERSION.INCREMENTAL + "-" + Build.VERSION.CODENAME + // ) + .addFormDataPart("locale", Locale.getDefault().toString()) + // .addFormDataPart("app_language", vectorLocale.applicationLocale.toString()) + // .addFormDataPart("default_app_language", systemLocaleProvider.getSystemLocale().toString()) + // .addFormDataPart("theme", ThemeUtils.getApplicationTheme(context)) + .addFormDataPart("server_version", serverVersion) + .apply { + customFields?.forEach { (name, value) -> + addFormDataPart(name, value) + } + } + + // add the gzipped files + for (file in gzippedFiles) { + builder.addFormDataPart("compressed-log", file.name, file.asRequestBody(MimeTypes.OctetStream.toMediaTypeOrNull())) + } + + mBugReportFiles.addAll(gzippedFiles) + + if (withScreenshot) { + screenshotHolder.getFileUri() + ?.toUri() + ?.toFile() + ?.let { screenshotFile -> + try { + builder.addFormDataPart( + "file", + screenshotFile.name, screenshotFile.asRequestBody(MimeTypes.OctetStream.toMediaTypeOrNull()) + ) + } catch (e: Exception) { + Timber.e(e, "## sendBugReport() : fail to write screenshot") + } + } + } + + // add some github labels + // builder.addFormDataPart("label", buildMeta.versionName) + // builder.addFormDataPart("label", buildMeta.flavorDescription) + // builder.addFormDataPart("label", buildMeta.gitBranchName) + + // Possible values for BuildConfig.BUILD_TYPE: "debug", "nightly", "release". + // builder.addFormDataPart("label", BuildConfig.BUILD_TYPE) + + when (reportType) { + ReportType.BUG_REPORT -> { + /* nop */ + } + ReportType.SUGGESTION -> builder.addFormDataPart("label", "[Suggestion]") + ReportType.SPACE_BETA_FEEDBACK -> builder.addFormDataPart("label", "spaces-feedback") + ReportType.THREADS_BETA_FEEDBACK -> builder.addFormDataPart("label", "threads-feedback") + ReportType.AUTO_UISI -> { + builder.addFormDataPart("label", "Z-UISI") + builder.addFormDataPart("label", "android") + builder.addFormDataPart("label", "uisi-recipient") + } + ReportType.AUTO_UISI_SENDER -> { + builder.addFormDataPart("label", "Z-UISI") + builder.addFormDataPart("label", "android") + builder.addFormDataPart("label", "uisi-sender") + } + } + + if (crashCallStack.isNotEmpty() && withCrashLogs) { + builder.addFormDataPart("label", "crash") + } + + val requestBody = builder.build() + + // add a progress listener + requestBody.setWriteListener { totalWritten, contentLength -> + val percentage = if (-1L != contentLength) { + if (totalWritten > contentLength) { + 100 + } else { + (totalWritten * 100 / contentLength).toInt() + } + } else { + 0 + } + + if (mIsCancelled && null != mBugReportCall) { + mBugReportCall!!.cancel() + } + + Timber.v("## onWrite() : $percentage%") + try { + listener?.onProgress(percentage) + } catch (e: Exception) { + Timber.e(e, "## onProgress() : failed") + } + } + + // build the request + val request = Request.Builder() + .url(context.getString(R.string.bug_report_url)) + .post(requestBody) + .build() + + var responseCode = HttpURLConnection.HTTP_INTERNAL_ERROR + var response: Response? = null + var errorMessage: String? = null + + // trigger the request + try { + mBugReportCall = mOkHttpClient.newCall(request) + response = mBugReportCall!!.execute() + responseCode = response.code + } catch (e: Exception) { + Timber.e(e, "response") + errorMessage = e.localizedMessage + } + + // if the upload failed, try to retrieve the reason + if (responseCode != HttpURLConnection.HTTP_OK) { + if (null != errorMessage) { + serverError = "Failed with error $errorMessage" + } else if (response?.body == null) { + serverError = "Failed with error $responseCode" + } else { + try { + val inputStream = response.body!!.byteStream() + + serverError = inputStream.use { + buildString { + var ch = it.read() + while (ch != -1) { + append(ch.toChar()) + ch = it.read() + } + } + } + + // check if the error message + serverError?.let { + try { + val responseJSON = JSONObject(it) + serverError = responseJSON.getString("error") + } catch (e: JSONException) { + Timber.e(e, "doInBackground ; Json conversion failed") + } + } + + // should never happen + if (null == serverError) { + serverError = "Failed with error $responseCode" + } + } catch (e: Exception) { + Timber.e(e, "## sendBugReport() : failed to parse error") + } + } + } else { + /* + reportURL = response?.body?.string()?.let { stringBody -> + adapter.fromJson(stringBody)?.get("report_url")?.toString() + } + */ + } + } + } + + withContext(coroutineDispatchers.main) { + mBugReportCall = null + + // delete when the bug report has been successfully sent + for (file in mBugReportFiles) { + file.safeDelete() + } + + if (null != listener) { + try { + if (mIsCancelled) { + listener.onUploadCancelled() + } else if (null == serverError) { + listener.onUploadSucceed(reportURL) + } else { + listener.onUploadFailed(serverError) + } + } catch (e: Exception) { + Timber.e(e, "## onPostExecute() : failed") + } + } + } + } + } + + /** + * Send a bug report either with email or with Vector. + */ + /* TODO Remove + fun openBugReportScreen(activity: FragmentActivity, reportType: ReportType = ReportType.BUG_REPORT) { + screenshot = takeScreenshot(activity) + logDbInfo() + logProcessInfo() + logOtherInfo() + activity.startActivity(BugReportActivity.intent(activity, reportType)) + } + */ + + // private fun logOtherInfo() { + // Timber.i("SyncThread state: " + activeSessionHolder.getSafeActiveSession()?.syncService()?.getSyncState()) + // } + + // private fun logDbInfo() { + // val dbInfo = matrix.debugService().getDbUsageInfo() + // Timber.i(dbInfo) + // } + + // private fun logProcessInfo() { + // val pInfo = processInfo.getInfo() + // Timber.i(pInfo) + // } + + private fun rageShakeAppNameForReport(reportType: ReportType): String { + // As per https://github.com/matrix-org/rageshake + // app: Identifier for the application (eg 'riot-web'). + // Should correspond to a mapping configured in the configuration file for github issue reporting to work. + // (see R.string.bug_report_url for configured RS server) + return context.getString( + when (reportType) { + ReportType.AUTO_UISI_SENDER, + ReportType.AUTO_UISI -> R.string.bug_report_auto_uisi_app_name + else -> R.string.bug_report_app_name + } + ) + } + + // ============================================================================================================== + // Logcat management + // ============================================================================================================== + + /** + * Save the logcat. + * + * @param isErrorLogcat true to save the error logcat + * @return the file if the operation succeeds + */ + private fun saveLogCat(isErrorLogcat: Boolean): File? { + val logCatErrFile = File(context.cacheDir.absolutePath, if (isErrorLogcat) LOG_CAT_ERROR_FILENAME else LOG_CAT_FILENAME) + + if (logCatErrFile.exists()) { + logCatErrFile.safeDelete() + } + + try { + logCatErrFile.writer().use { + getLogCatError(it, isErrorLogcat) + } + + return compressFile(logCatErrFile) + } catch (error: OutOfMemoryError) { + Timber.e(error, "## saveLogCat() : fail to write logcat$error") + } catch (e: Exception) { + Timber.e(e, "## saveLogCat() : fail to write logcat$e") + } + + return null + } + + /** + * Retrieves the logs. + * + * @param streamWriter the stream writer + * @param isErrorLogCat true to save the error logs + */ + private fun getLogCatError(streamWriter: OutputStreamWriter, isErrorLogCat: Boolean) { + val logcatProc: Process + + try { + logcatProc = Runtime.getRuntime().exec(if (isErrorLogCat) LOGCAT_CMD_ERROR else LOGCAT_CMD_DEBUG) + } catch (e1: IOException) { + return + } + + try { + val separator = System.getProperty("line.separator") + logcatProc.inputStream + .reader() + .buffered(BUFFER_SIZE) + .forEachLine { line -> + streamWriter.append(line) + streamWriter.append(separator) + } + } catch (e: IOException) { + Timber.e(e, "getLog fails") + } + } +} diff --git a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/screenshot/DefaultScreenshotHolder.kt b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/screenshot/DefaultScreenshotHolder.kt new file mode 100644 index 0000000000..31eeac39a6 --- /dev/null +++ b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/screenshot/DefaultScreenshotHolder.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2022 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 io.element.android.features.rageshake.screenshot + +import android.content.Context +import android.graphics.Bitmap +import androidx.core.net.toUri +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.androidutils.bitmap.writeBitmap +import io.element.android.libraries.androidutils.file.safeDelete +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.SingleIn +import java.io.File +import javax.inject.Inject + +@SingleIn(AppScope::class) +@ContributesBinding(AppScope::class) +class DefaultScreenshotHolder @Inject constructor( + @ApplicationContext private val context: Context, +) : ScreenshotHolder { + private val file = File(context.filesDir, "screenshot.png") + + override fun writeBitmap(data: Bitmap) { + file.writeBitmap(data, Bitmap.CompressFormat.PNG, 85) + } + + override fun getFileUri(): String? { + return file + .takeIf { it.exists() && it.length() > 0 } + ?.toUri() + ?.toString() + } + + override fun reset() { + file.safeDelete() + } +} diff --git a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/screenshot/ScreenshotHolder.kt b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/screenshot/ScreenshotHolder.kt index 33674c07fb..dfe31ae2fe 100644 --- a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/screenshot/ScreenshotHolder.kt +++ b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/screenshot/ScreenshotHolder.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 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. @@ -16,29 +16,10 @@ package io.element.android.features.rageshake.screenshot -import android.content.Context import android.graphics.Bitmap -import io.element.android.libraries.androidutils.bitmap.writeBitmap -import io.element.android.libraries.androidutils.file.safeDelete -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext -import io.element.android.libraries.di.SingleIn -import java.io.File -import javax.inject.Inject -@SingleIn(AppScope::class) -class ScreenshotHolder @Inject constructor( - @ApplicationContext private val context: Context, -) { - private val file = File(context.filesDir, "screenshot.png") - - fun writeBitmap(data: Bitmap) { - file.writeBitmap(data, Bitmap.CompressFormat.PNG, 85) - } - - fun getFile() = file.takeIf { it.exists() && it.length() > 0 } - - fun reset() { - file.safeDelete() - } +interface ScreenshotHolder { + fun writeBitmap(data: Bitmap) + fun getFileUri(): String? + fun reset() } diff --git a/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/bugreport/BugReportPresenterTest.kt b/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/bugreport/BugReportPresenterTest.kt new file mode 100644 index 0000000000..484c3bd1b0 --- /dev/null +++ b/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/bugreport/BugReportPresenterTest.kt @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2023 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:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.rageshake.bugreport + +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.rageshake.crash.ui.A_CRASH_DATA +import io.element.android.features.rageshake.crash.ui.FakeCrashDataStore +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrixtest.A_FAILURE_REASON +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +const val A_SHORT_DESCRIPTION = "bug!" +const val A_LONG_DESCRIPTION = "I have seen a bug!" + +class BugReportPresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = BugReportPresenter( + FakeBugReporter(), + FakeCrashDataStore(), + FakeScreenshotHolder(), + this, + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.hasCrashLogs).isFalse() + assertThat(initialState.formState).isEqualTo(BugReportFormState.Default) + assertThat(initialState.sending).isEqualTo(Async.Uninitialized) + assertThat(initialState.screenshotUri).isNull() + assertThat(initialState.sendingProgress).isEqualTo(0f) + assertThat(initialState.submitEnabled).isFalse() + } + } + + @Test + fun `present - set description`() = runTest { + val presenter = BugReportPresenter( + FakeBugReporter(), + FakeCrashDataStore(), + FakeScreenshotHolder(), + this, + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(BugReportEvents.SetDescription(A_SHORT_DESCRIPTION)) + assertThat(awaitItem().submitEnabled).isFalse() + initialState.eventSink.invoke(BugReportEvents.SetDescription(A_LONG_DESCRIPTION)) + assertThat(awaitItem().submitEnabled).isTrue() + } + } + + @Test + fun `present - can contact`() = runTest { + val presenter = BugReportPresenter( + FakeBugReporter(), + FakeCrashDataStore(), + FakeScreenshotHolder(), + this, + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(BugReportEvents.SetCanContact(true)) + assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(canContact = true)) + initialState.eventSink.invoke(BugReportEvents.SetCanContact(false)) + assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(canContact = false)) + } + } + + @Test + fun `present - send crash logs`() = runTest { + val presenter = BugReportPresenter( + FakeBugReporter(), + FakeCrashDataStore(), + FakeScreenshotHolder(), + this, + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + // Since this is true by default, start by disabling + initialState.eventSink.invoke(BugReportEvents.SetSendCrashLog(false)) + assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(sendCrashLogs = false)) + initialState.eventSink.invoke(BugReportEvents.SetSendCrashLog(true)) + assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(sendCrashLogs = true)) + } + } + + @Test + fun `present - send logs`() = runTest { + val presenter = BugReportPresenter( + FakeBugReporter(), + FakeCrashDataStore(), + FakeScreenshotHolder(), + this, + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + // Since this is true by default, start by disabling + initialState.eventSink.invoke(BugReportEvents.SetSendLog(false)) + assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(sendLogs = false)) + initialState.eventSink.invoke(BugReportEvents.SetSendLog(true)) + assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(sendLogs = true)) + } + } + + @Test + fun `present - send screenshot`() = runTest { + val presenter = BugReportPresenter( + FakeBugReporter(), + FakeCrashDataStore(), + FakeScreenshotHolder(), + this, + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(BugReportEvents.SetSendScreenshot(true)) + assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(sendScreenshot = true)) + initialState.eventSink.invoke(BugReportEvents.SetSendScreenshot(false)) + assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(sendScreenshot = false)) + } + } + + @Test + fun `present - reset all`() = runTest { + val presenter = BugReportPresenter( + FakeBugReporter(), + FakeCrashDataStore(crashData = A_CRASH_DATA, appHasCrashed = true), + FakeScreenshotHolder(screenshotUri = A_SCREENSHOT_URI), + this, + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.hasCrashLogs).isTrue() + assertThat(initialState.screenshotUri).isEqualTo(A_SCREENSHOT_URI) + initialState.eventSink.invoke(BugReportEvents.ResetAll) + val resetState = awaitItem() + assertThat(resetState.hasCrashLogs).isFalse() + // TODO Make it live assertThat(resetState.screenshotUri).isNull() + } + } + + @Test + fun `present - send success`() = runTest { + val presenter = BugReportPresenter( + FakeBugReporter(mode = FakeBugReporterMode.Success), + FakeCrashDataStore(crashData = A_CRASH_DATA, appHasCrashed = true), + FakeScreenshotHolder(screenshotUri = A_SCREENSHOT_URI), + this, + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(BugReportEvents.SendBugReport) + skipItems(1) + val progressState = awaitItem() + assertThat(progressState.sending).isEqualTo(Async.Loading(null)) + assertThat(progressState.sendingProgress).isEqualTo(0f) + assertThat(progressState.submitEnabled).isFalse() + assertThat(awaitItem().sendingProgress).isEqualTo(0.5f) + assertThat(awaitItem().sendingProgress).isEqualTo(1f) + skipItems(1) + assertThat(awaitItem().sending).isEqualTo(Async.Success(Unit)) + } + } + + @Test + fun `present - send failure`() = runTest { + val presenter = BugReportPresenter( + FakeBugReporter(mode = FakeBugReporterMode.Failure), + FakeCrashDataStore(crashData = A_CRASH_DATA, appHasCrashed = true), + FakeScreenshotHolder(screenshotUri = A_SCREENSHOT_URI), + this, + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(BugReportEvents.SendBugReport) + skipItems(1) + val progressState = awaitItem() + assertThat(progressState.sending).isEqualTo(Async.Loading(null)) + assertThat(progressState.sendingProgress).isEqualTo(0f) + assertThat(awaitItem().sendingProgress).isEqualTo(0.5f) + // Failure + assertThat(awaitItem().sendingProgress).isEqualTo(0f) + assertThat((awaitItem().sending as Async.Failure).error.message).isEqualTo(A_FAILURE_REASON) + } + } + + @Test + fun `present - send cancel`() = runTest { + val presenter = BugReportPresenter( + FakeBugReporter(mode = FakeBugReporterMode.Cancel), + FakeCrashDataStore(crashData = A_CRASH_DATA, appHasCrashed = true), + FakeScreenshotHolder(screenshotUri = A_SCREENSHOT_URI), + this, + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(BugReportEvents.SendBugReport) + skipItems(1) + val progressState = awaitItem() + assertThat(progressState.sending).isEqualTo(Async.Loading(null)) + assertThat(progressState.sendingProgress).isEqualTo(0f) + assertThat(awaitItem().sendingProgress).isEqualTo(0.5f) + // Cancelled + assertThat(awaitItem().sendingProgress).isEqualTo(0f) + assertThat(awaitItem().sending).isEqualTo(Async.Uninitialized) + } + } +} diff --git a/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/bugreport/FakeBugReporter.kt b/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/bugreport/FakeBugReporter.kt new file mode 100644 index 0000000000..29977d7a95 --- /dev/null +++ b/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/bugreport/FakeBugReporter.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2023 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 io.element.android.features.rageshake.bugreport + +import io.element.android.features.rageshake.reporter.BugReporter +import io.element.android.features.rageshake.reporter.BugReporterListener +import io.element.android.features.rageshake.reporter.ReportType +import io.element.android.libraries.matrixtest.A_FAILURE_REASON +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +class FakeBugReporter(val mode: FakeBugReporterMode = FakeBugReporterMode.Success) : BugReporter { + override fun sendBugReport( + coroutineScope: CoroutineScope, + reportType: ReportType, + withDevicesLogs: Boolean, + withCrashLogs: Boolean, + withKeyRequestHistory: Boolean, + withScreenshot: Boolean, + theBugDescription: String, + serverVersion: String, + canContact: Boolean, + customFields: Map?, + listener: BugReporterListener?, + ) { + coroutineScope.launch { + delay(100) + listener?.onProgress(0) + delay(100) + listener?.onProgress(50) + delay(100) + when (mode) { + FakeBugReporterMode.Success -> Unit + FakeBugReporterMode.Failure -> { + listener?.onUploadFailed(A_FAILURE_REASON) + return@launch + } + FakeBugReporterMode.Cancel -> { + listener?.onUploadCancelled() + return@launch + } + } + listener?.onProgress(100) + delay(100) + listener?.onUploadSucceed(null) + } + } +} + +enum class FakeBugReporterMode { + Success, + Failure, + Cancel +} diff --git a/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/bugreport/FakeScreenshotHolder.kt b/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/bugreport/FakeScreenshotHolder.kt new file mode 100644 index 0000000000..14ece36a14 --- /dev/null +++ b/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/bugreport/FakeScreenshotHolder.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 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 io.element.android.features.rageshake.bugreport + +import android.graphics.Bitmap +import io.element.android.features.rageshake.screenshot.ScreenshotHolder + +const val A_SCREENSHOT_URI = "file://content/uri" + +class FakeScreenshotHolder(private val screenshotUri: String? = null) : ScreenshotHolder { + override fun writeBitmap(data: Bitmap) = Unit + + override fun getFileUri() = screenshotUri + + override fun reset() = Unit +} diff --git a/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/crash/ui/CrashDetectionPresenterTest.kt b/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/crash/ui/CrashDetectionPresenterTest.kt new file mode 100644 index 0000000000..16a03eca86 --- /dev/null +++ b/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/crash/ui/CrashDetectionPresenterTest.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2023 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:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.rageshake.crash.ui + +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class CrashDetectionPresenterTest { + @Test + fun `present - initial state no crash`() = runTest { + val presenter = CrashDetectionPresenter( + FakeCrashDataStore() + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.crashDetected).isFalse() + } + } + + @Test + fun `present - initial state crash`() = runTest { + val presenter = CrashDetectionPresenter( + FakeCrashDataStore(appHasCrashed = true) + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.crashDetected).isTrue() + + } + } + + @Test + fun `present - reset app has crashed`() = runTest { + val presenter = CrashDetectionPresenter( + FakeCrashDataStore(appHasCrashed = true) + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.crashDetected).isTrue() + initialState.eventSink.invoke(CrashDetectionEvents.ResetAppHasCrashed) + assertThat(awaitItem().crashDetected).isFalse() + } + } + + @Test + fun `present - reset all crash data`() = runTest { + val presenter = CrashDetectionPresenter( + FakeCrashDataStore(appHasCrashed = true, crashData = A_CRASH_DATA) + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.crashDetected).isTrue() + initialState.eventSink.invoke(CrashDetectionEvents.ResetAllCrashData) + assertThat(awaitItem().crashDetected).isFalse() + } + } +} diff --git a/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/crash/ui/FakeCrashDataStore.kt b/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/crash/ui/FakeCrashDataStore.kt new file mode 100644 index 0000000000..a757931d53 --- /dev/null +++ b/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/crash/ui/FakeCrashDataStore.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 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 io.element.android.features.rageshake.crash.ui + +import io.element.android.features.rageshake.crash.CrashDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +const val A_CRASH_DATA = "Some crash data" + +class FakeCrashDataStore( + crashData: String = "", + appHasCrashed: Boolean = false, +) : CrashDataStore { + private val appHasCrashedFlow = MutableStateFlow(appHasCrashed) + private val crashDataFlow = MutableStateFlow(crashData) + + override fun setCrashData(crashData: String) { + crashDataFlow.value = crashData + } + + override suspend fun resetAppHasCrashed() { + appHasCrashedFlow.value = false + } + + override fun appHasCrashed(): Flow = appHasCrashedFlow + + override fun crashInfo(): Flow = crashDataFlow + + override suspend fun reset() { + appHasCrashedFlow.value = false + crashDataFlow.value = "" + } +} diff --git a/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/detection/RageshakeDetectionPresenterTest.kt b/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/detection/RageshakeDetectionPresenterTest.kt new file mode 100644 index 0000000000..1ef8941bff --- /dev/null +++ b/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/detection/RageshakeDetectionPresenterTest.kt @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2023 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:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.rageshake.detection + +import android.graphics.Bitmap +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.rageshake.bugreport.FakeScreenshotHolder +import io.element.android.features.rageshake.preferences.FakeRageShake +import io.element.android.features.rageshake.preferences.FakeRageshakeDataStore +import io.element.android.features.rageshake.preferences.RageshakePreferencesPresenter +import io.element.android.features.rageshake.screenshot.ImageResult +import io.element.android.libraries.matrixtest.AN_EXCEPTION +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class RageshakeDetectionPresenterTest { + @Test + fun `present - initial state`() = runTest { + val screenshotHolder = FakeScreenshotHolder(screenshotUri = null) + val rageshake = FakeRageShake(isAvailableValue = true) + val rageshakeDataStore = FakeRageshakeDataStore(isEnabled = true) + val presenter = RageshakeDetectionPresenter( + screenshotHolder = screenshotHolder, + rageShake = rageshake, + preferencesPresenter = RageshakePreferencesPresenter( + rageshake = rageshake, + rageshakeDataStore = rageshakeDataStore, + ) + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.takeScreenshot).isFalse() + assertThat(initialState.showDialog).isFalse() + assertThat(initialState.isStarted).isFalse() + } + } + + @Test + fun `present - start and stop detection`() = runTest { + val screenshotHolder = FakeScreenshotHolder(screenshotUri = null) + val rageshake = FakeRageShake(isAvailableValue = true) + val rageshakeDataStore = FakeRageshakeDataStore(isEnabled = true) + val presenter = RageshakeDetectionPresenter( + screenshotHolder = screenshotHolder, + rageShake = rageshake, + preferencesPresenter = RageshakePreferencesPresenter( + rageshake = rageshake, + rageshakeDataStore = rageshakeDataStore, + ) + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + initialState.eventSink.invoke(RageshakeDetectionEvents.StartDetection) + assertThat(awaitItem().isStarted).isTrue() + initialState.eventSink.invoke(RageshakeDetectionEvents.StopDetection) + assertThat(awaitItem().isStarted).isFalse() + } + } + + @Test + fun `present - screenshot with success then dismiss`() = runTest { + val screenshotHolder = FakeScreenshotHolder(screenshotUri = null) + val rageshake = FakeRageShake(isAvailableValue = true) + val rageshakeDataStore = FakeRageshakeDataStore(isEnabled = true) + val presenter = RageshakeDetectionPresenter( + screenshotHolder = screenshotHolder, + rageShake = rageshake, + preferencesPresenter = RageshakePreferencesPresenter( + rageshake = rageshake, + rageshakeDataStore = rageshakeDataStore, + ) + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.isStarted).isFalse() + initialState.eventSink.invoke(RageshakeDetectionEvents.StartDetection) + assertThat(awaitItem().isStarted).isTrue() + rageshake.triggerPhoneRageshake() + assertThat(awaitItem().takeScreenshot).isTrue() + initialState.eventSink.invoke( + RageshakeDetectionEvents.ProcessScreenshot(ImageResult.Success(aBitmap())) + ) + assertThat(awaitItem().showDialog).isTrue() + initialState.eventSink.invoke(RageshakeDetectionEvents.Dismiss) + val finalState = awaitItem() + assertThat(finalState.showDialog).isFalse() + assertThat(rageshakeDataStore.isEnabled().first()).isTrue() + } + } + + @Test + fun `present - screenshot with error then dismiss`() = runTest { + val screenshotHolder = FakeScreenshotHolder(screenshotUri = null) + val rageshake = FakeRageShake(isAvailableValue = true) + val rageshakeDataStore = FakeRageshakeDataStore(isEnabled = true) + val presenter = RageshakeDetectionPresenter( + screenshotHolder = screenshotHolder, + rageShake = rageshake, + preferencesPresenter = RageshakePreferencesPresenter( + rageshake = rageshake, + rageshakeDataStore = rageshakeDataStore, + ) + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.isStarted).isFalse() + initialState.eventSink.invoke(RageshakeDetectionEvents.StartDetection) + assertThat(awaitItem().isStarted).isTrue() + rageshake.triggerPhoneRageshake() + assertThat(awaitItem().takeScreenshot).isTrue() + initialState.eventSink.invoke( + RageshakeDetectionEvents.ProcessScreenshot(ImageResult.Error(AN_EXCEPTION)) + ) + assertThat(awaitItem().showDialog).isTrue() + initialState.eventSink.invoke(RageshakeDetectionEvents.Dismiss) + val finalState = awaitItem() + assertThat(finalState.showDialog).isFalse() + assertThat(rageshakeDataStore.isEnabled().first()).isTrue() + } + } + + @Test + fun `present - screenshot then disable`() = runTest { + val screenshotHolder = FakeScreenshotHolder(screenshotUri = null) + val rageshake = FakeRageShake(isAvailableValue = true) + val rageshakeDataStore = FakeRageshakeDataStore(isEnabled = true) + val presenter = RageshakeDetectionPresenter( + screenshotHolder = screenshotHolder, + rageShake = rageshake, + preferencesPresenter = RageshakePreferencesPresenter( + rageshake = rageshake, + rageshakeDataStore = rageshakeDataStore, + ) + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.isStarted).isFalse() + initialState.eventSink.invoke(RageshakeDetectionEvents.StartDetection) + assertThat(awaitItem().isStarted).isTrue() + rageshake.triggerPhoneRageshake() + assertThat(awaitItem().takeScreenshot).isTrue() + initialState.eventSink.invoke( + RageshakeDetectionEvents.ProcessScreenshot(ImageResult.Success(aBitmap())) + ) + assertThat(awaitItem().showDialog).isTrue() + initialState.eventSink.invoke(RageshakeDetectionEvents.Disable) + skipItems(1) + assertThat(awaitItem().showDialog).isFalse() + assertThat(rageshakeDataStore.isEnabled().first()).isFalse() + } + } +} + +private fun aBitmap(): Bitmap = mockk() + diff --git a/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/preferences/FakeRageShake.kt b/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/preferences/FakeRageShake.kt new file mode 100644 index 0000000000..fbabdaac5d --- /dev/null +++ b/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/preferences/FakeRageShake.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 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 io.element.android.features.rageshake.preferences + +import io.element.android.features.rageshake.rageshake.RageShake + +const val A_SENSITIVITY = 1f + +class FakeRageShake( + private var isAvailableValue: Boolean = true +) : RageShake { + + private var interceptor: (() -> Unit)? = null + + override fun isAvailable() = isAvailableValue + + override fun start(sensitivity: Float) { + } + + override fun stop() { + } + + override fun setSensitivity(sensitivity: Float) { + } + + override fun setInterceptor(interceptor: (() -> Unit)?) { + this.interceptor = interceptor + } + + fun triggerPhoneRageshake() = interceptor?.invoke() +} diff --git a/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/preferences/FakeRageshakeDataStore.kt b/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/preferences/FakeRageshakeDataStore.kt new file mode 100644 index 0000000000..22c4ae4d4d --- /dev/null +++ b/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/preferences/FakeRageshakeDataStore.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023 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 io.element.android.features.rageshake.preferences + +import io.element.android.features.rageshake.rageshake.RageshakeDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeRageshakeDataStore( + isEnabled: Boolean = true, + sensitivity: Float = A_SENSITIVITY, +) : RageshakeDataStore { + + private val isEnabledFlow = MutableStateFlow(isEnabled) + override fun isEnabled(): Flow = isEnabledFlow + + override suspend fun setIsEnabled(isEnabled: Boolean) { + isEnabledFlow.value = isEnabled + } + + private val sensitivityFlow = MutableStateFlow(sensitivity) + override fun sensitivity(): Flow = sensitivityFlow + + override suspend fun setSensitivity(sensitivity: Float) { + sensitivityFlow.value = sensitivity + } + + override suspend fun reset() = Unit +} diff --git a/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/preferences/RageshakePreferencesPresenterTest.kt b/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/preferences/RageshakePreferencesPresenterTest.kt new file mode 100644 index 0000000000..17a46e8da6 --- /dev/null +++ b/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/preferences/RageshakePreferencesPresenterTest.kt @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2023 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:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.rageshake.preferences + +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class RageshakePreferencesPresenterTest { + @Test + fun `present - initial state available`() = runTest { + val presenter = RageshakePreferencesPresenter( + FakeRageShake(isAvailableValue = true), + FakeRageshakeDataStore(isEnabled = true) + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.isSupported).isTrue() + assertThat(initialState.isEnabled).isTrue() + } + } + + @Test + fun `present - initial state not available`() = runTest { + val presenter = RageshakePreferencesPresenter( + FakeRageShake(isAvailableValue = false), + FakeRageshakeDataStore(isEnabled = true) + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.isSupported).isFalse() + assertThat(initialState.isEnabled).isTrue() + } + } + + @Test + fun `present - enable and disable`() = runTest { + val presenter = RageshakePreferencesPresenter( + FakeRageShake(isAvailableValue = true), + FakeRageshakeDataStore(isEnabled = true) + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.isEnabled).isTrue() + initialState.eventSink.invoke(RageshakePreferencesEvents.SetIsEnabled(false)) + assertThat(awaitItem().isEnabled).isFalse() + initialState.eventSink.invoke(RageshakePreferencesEvents.SetIsEnabled(true)) + assertThat(awaitItem().isEnabled).isTrue() + } + } + + @Test + fun `present - set sensitivity`() = runTest { + val presenter = RageshakePreferencesPresenter( + FakeRageShake(isAvailableValue = true), + FakeRageshakeDataStore(isEnabled = true) + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.sensitivity).isEqualTo(A_SENSITIVITY) + initialState.eventSink.invoke(RageshakePreferencesEvents.SetSensitivity(A_SENSITIVITY + 1f)) + assertThat(awaitItem().sensitivity).isEqualTo(A_SENSITIVITY + 1f) + } + } +} + diff --git a/features/roomlist/build.gradle.kts b/features/roomlist/build.gradle.kts index fc0402eafc..fe8112fb81 100644 --- a/features/roomlist/build.gradle.kts +++ b/features/roomlist/build.gradle.kts @@ -41,7 +41,7 @@ dependencies { implementation(projects.libraries.designsystem) implementation(projects.libraries.elementresources) implementation(projects.libraries.uiStrings) - implementation(libs.datetime) + implementation(projects.libraries.dateformatter) implementation(libs.accompanist.placeholder) testImplementation(libs.test.junit) @@ -51,6 +51,8 @@ dependencies { testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrixtest) + testImplementation(testFixtures(projects.libraries.matrix)) + androidTestImplementation(libs.test.junitext) ksp(libs.showkase.processor) diff --git a/features/roomlist/src/main/kotlin/io/element/android/features/roomlist/RoomListPresenter.kt b/features/roomlist/src/main/kotlin/io/element/android/features/roomlist/RoomListPresenter.kt index b28504eee1..26d3bfd4ae 100644 --- a/features/roomlist/src/main/kotlin/io/element/android/features/roomlist/RoomListPresenter.kt +++ b/features/roomlist/src/main/kotlin/io/element/android/features/roomlist/RoomListPresenter.kt @@ -31,6 +31,7 @@ import io.element.android.features.roomlist.model.RoomListRoomSummaryPlaceholder import io.element.android.features.roomlist.model.RoomListState import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.parallelMap +import io.element.android.libraries.dateformatter.LastMessageFormatter import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.MatrixClient @@ -57,7 +58,6 @@ class RoomListPresenter @Inject constructor( mutableStateOf(null) } var filter by rememberSaveable { mutableStateOf("") } - val isLoginOut = rememberSaveable { mutableStateOf(false) } val roomSummaries by client .roomSummaryDataSource() .roomSummaries() @@ -86,7 +86,6 @@ class RoomListPresenter @Inject constructor( matrixUser = matrixUser.value, roomList = filteredRoomSummaries.value, filter = filter, - isLoginOut = isLoginOut.value, eventSink = ::handleEvents ) } diff --git a/features/roomlist/src/main/kotlin/io/element/android/features/roomlist/RoomListView.kt b/features/roomlist/src/main/kotlin/io/element/android/features/roomlist/RoomListView.kt index 8a67b649e3..3ed5edcf63 100644 --- a/features/roomlist/src/main/kotlin/io/element/android/features/roomlist/RoomListView.kt +++ b/features/roomlist/src/main/kotlin/io/element/android/features/roomlist/RoomListView.kt @@ -14,8 +14,6 @@ * limitations under the License. */ -@file:OptIn(ExperimentalMaterial3Api::class) - package io.element.android.features.roomlist import androidx.compose.foundation.layout.Column @@ -24,7 +22,6 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Scaffold import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable @@ -43,6 +40,9 @@ import io.element.android.features.roomlist.model.RoomListRoomSummary import io.element.android.features.roomlist.model.RoomListState import io.element.android.features.roomlist.model.stubbedRoomSummaries import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.utils.LogCompositions import io.element.android.libraries.matrix.core.RoomId import io.element.android.libraries.matrix.core.UserId @@ -76,6 +76,7 @@ fun RoomListView( ) } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun RoomListView( roomSummaries: ImmutableList, @@ -125,11 +126,15 @@ fun RoomListView( filter = filter, onFilterChanged = onFilterChanged, onOpenSettings = onOpenSettings, - scrollBehavior = scrollBehavior + scrollBehavior = scrollBehavior, + modifier = Modifier, ) }, content = { padding -> - Column(modifier = Modifier.padding(padding)) { + Column( + modifier = Modifier + .padding(padding) + ) { LazyColumn( modifier = Modifier .weight(1f) @@ -152,7 +157,14 @@ private fun RoomListRoomSummary.contentType() = isPlaceholder @Preview @Composable -fun RoomListViewPreview() { +fun RoomListViewLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun RoomListViewDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { RoomListView( roomSummaries = stubbedRoomSummaries(), matrixUser = MatrixUser(id = UserId("@id"), username = "User#1", avatarData = AvatarData("U")), diff --git a/features/roomlist/src/main/kotlin/io/element/android/features/roomlist/components/RoomListTopBar.kt b/features/roomlist/src/main/kotlin/io/element/android/features/roomlist/components/RoomListTopBar.kt index c7f077374f..f622164af9 100644 --- a/features/roomlist/src/main/kotlin/io/element/android/features/roomlist/components/RoomListTopBar.kt +++ b/features/roomlist/src/main/kotlin/io/element/android/features/roomlist/components/RoomListTopBar.kt @@ -27,14 +27,7 @@ import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.MediumTopAppBar -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults -import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -46,7 +39,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle @@ -54,6 +46,12 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.form.textFieldState +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.theme.components.MediumTopAppBar +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextField +import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.utils.LogCompositions import io.element.android.libraries.matrix.ui.model.MatrixUser import io.element.android.libraries.ui.strings.R as StringR @@ -64,7 +62,8 @@ fun RoomListTopBar( filter: String, onFilterChanged: (String) -> Unit, onOpenSettings: () -> Unit, - scrollBehavior: TopAppBarScrollBehavior + scrollBehavior: TopAppBarScrollBehavior, + modifier: Modifier = Modifier, ) { LogCompositions( tag = "RoomListScreen", @@ -87,6 +86,7 @@ fun RoomListTopBar( onFilterChanged = onFilterChanged, onCloseClicked = ::closeFilter, scrollBehavior = scrollBehavior, + modifier = modifier, ) } else { DefaultRoomListTopBar( @@ -96,6 +96,7 @@ fun RoomListTopBar( searchWidgetStateIsOpened = true }, scrollBehavior = scrollBehavior, + modifier = modifier, ) } } @@ -148,14 +149,6 @@ fun SearchRoomListTopBar( } } }, - colors = TextFieldDefaults.textFieldColors( - textColor = MaterialTheme.colorScheme.onBackground, - containerColor = Color.Transparent, - cursorColor = MaterialTheme.colorScheme.onBackground.copy(alpha = ContentAlpha.medium), - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent - ) ) }, navigationIcon = { @@ -182,10 +175,11 @@ private fun DefaultRoomListTopBar( matrixUser: MatrixUser?, onOpenSettings: () -> Unit, onSearchClicked: () -> Unit, - scrollBehavior: TopAppBarScrollBehavior + scrollBehavior: TopAppBarScrollBehavior, + modifier: Modifier = Modifier, ) { MediumTopAppBar( - modifier = Modifier + modifier = modifier .nestedScroll(scrollBehavior.nestedScrollConnection), title = { Text( diff --git a/features/roomlist/src/main/kotlin/io/element/android/features/roomlist/components/RoomSummaryRow.kt b/features/roomlist/src/main/kotlin/io/element/android/features/roomlist/components/RoomSummaryRow.kt index 7b561d9054..53ca8b5d1f 100644 --- a/features/roomlist/src/main/kotlin/io/element/android/features/roomlist/components/RoomSummaryRow.kt +++ b/features/roomlist/src/main/kotlin/io/element/android/features/roomlist/components/RoomSummaryRow.kt @@ -32,7 +32,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -54,6 +53,13 @@ import androidx.compose.ui.unit.sp import com.google.accompanist.placeholder.material.placeholder import io.element.android.features.roomlist.model.RoomListRoomSummary import io.element.android.libraries.designsystem.components.avatar.Avatar +import io.element.android.libraries.designsystem.theme.ElementTheme +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.roomListPlaceHolder +import io.element.android.libraries.designsystem.theme.roomListRoomMessage +import io.element.android.libraries.designsystem.theme.roomListRoomMessageDate +import io.element.android.libraries.designsystem.theme.roomListRoomName +import io.element.android.libraries.designsystem.theme.roomListUnreadIndicator private val minHeight = 72.dp @@ -95,7 +101,11 @@ internal fun DefaultRoomSummaryRow( ) { Avatar( room.avatarData, - modifier = Modifier.placeholder(room.isPlaceholder, shape = CircleShape) + modifier = Modifier.placeholder( + visible = room.isPlaceholder, + shape = CircleShape, + color = ElementTheme.colors.roomListPlaceHolder(), + ) ) Column( modifier = Modifier @@ -105,19 +115,27 @@ internal fun DefaultRoomSummaryRow( ) { // Name Text( - modifier = Modifier - .placeholder(room.isPlaceholder, shape = TextPlaceholderShape), + modifier = Modifier.placeholder( + visible = room.isPlaceholder, + shape = TextPlaceholderShape, + color = ElementTheme.colors.roomListPlaceHolder(), + ), fontSize = 16.sp, fontWeight = FontWeight.SemiBold, text = room.name, + color = MaterialTheme.roomListRoomName(), maxLines = 1, overflow = TextOverflow.Ellipsis ) // Last Message Text( - modifier = Modifier.placeholder(room.isPlaceholder, shape = TextPlaceholderShape), + modifier = Modifier.placeholder( + visible = room.isPlaceholder, + shape = TextPlaceholderShape, + color = ElementTheme.colors.roomListPlaceHolder(), + ), text = room.lastMessage?.toString().orEmpty(), - color = MaterialTheme.colorScheme.secondary, + color = MaterialTheme.roomListRoomMessage(), fontSize = 14.sp, maxLines = 1, overflow = TextOverflow.Ellipsis @@ -129,14 +147,18 @@ internal fun DefaultRoomSummaryRow( .alignByBaseline(), ) { Text( - modifier = Modifier.placeholder(room.isPlaceholder, shape = TextPlaceholderShape), + modifier = Modifier.placeholder( + visible = room.isPlaceholder, + shape = TextPlaceholderShape, + color = ElementTheme.colors.roomListPlaceHolder(), + ), fontSize = 12.sp, text = room.timestamp ?: "", - color = MaterialTheme.colorScheme.secondary, + color = MaterialTheme.roomListRoomMessageDate(), ) Spacer(Modifier.size(4.dp)) val unreadIndicatorColor = - if (room.hasUnread) MaterialTheme.colorScheme.primary else Color.Transparent + if (room.hasUnread) MaterialTheme.roomListUnreadIndicator() else Color.Transparent Box( modifier = Modifier .size(12.dp) diff --git a/features/roomlist/src/main/kotlin/io/element/android/features/roomlist/model/RoomListRoomSummaryPlaceholders.kt b/features/roomlist/src/main/kotlin/io/element/android/features/roomlist/model/RoomListRoomSummaryPlaceholders.kt index 5e6176bcb2..5fb3221093 100644 --- a/features/roomlist/src/main/kotlin/io/element/android/features/roomlist/model/RoomListRoomSummaryPlaceholders.kt +++ b/features/roomlist/src/main/kotlin/io/element/android/features/roomlist/model/RoomListRoomSummaryPlaceholders.kt @@ -33,8 +33,8 @@ object RoomListRoomSummaryPlaceholders { fun createFakeList(size: Int): List { return mutableListOf().apply { - for (i in 0..size) { - add(create("\$fakeRoom$i")) + repeat(size) { + add(create("\$fakeRoom$it")) } } } diff --git a/features/roomlist/src/main/kotlin/io/element/android/features/roomlist/model/RoomListState.kt b/features/roomlist/src/main/kotlin/io/element/android/features/roomlist/model/RoomListState.kt index f2d873654b..e9a48a7249 100644 --- a/features/roomlist/src/main/kotlin/io/element/android/features/roomlist/model/RoomListState.kt +++ b/features/roomlist/src/main/kotlin/io/element/android/features/roomlist/model/RoomListState.kt @@ -25,6 +25,5 @@ data class RoomListState( val matrixUser: MatrixUser?, val roomList: ImmutableList, val filter: String, - val isLoginOut: Boolean, val eventSink: (RoomListEvents) -> Unit ) diff --git a/features/messages/src/test/kotlin/io/element/android/features/messages/ExampleUnitTest.kt b/features/roomlist/src/test/kotlin/io/element/android/features/roomlist/FakeLastMessageFormatter.kt similarity index 59% rename from features/messages/src/test/kotlin/io/element/android/features/messages/ExampleUnitTest.kt rename to features/roomlist/src/test/kotlin/io/element/android/features/roomlist/FakeLastMessageFormatter.kt index 83296930a7..997846056a 100644 --- a/features/messages/src/test/kotlin/io/element/android/features/messages/ExampleUnitTest.kt +++ b/features/roomlist/src/test/kotlin/io/element/android/features/roomlist/FakeLastMessageFormatter.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 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. @@ -14,19 +14,17 @@ * limitations under the License. */ -package io.element.android.features.messages +package io.element.android.features.roomlist -import org.junit.Assert.assertEquals -import org.junit.Test +import io.element.android.libraries.dateformatter.LastMessageFormatter -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) +class FakeLastMessageFormatter : LastMessageFormatter { + private var format = "" + fun givenFormat(format: String) { + this.format = format + } + + override fun format(timestamp: Long?): String { + return format } } diff --git a/features/roomlist/src/test/kotlin/io/element/android/features/roomlist/RoomListPresenterTests.kt b/features/roomlist/src/test/kotlin/io/element/android/features/roomlist/RoomListPresenterTests.kt index 615353b7e8..94256c8888 100644 --- a/features/roomlist/src/test/kotlin/io/element/android/features/roomlist/RoomListPresenterTests.kt +++ b/features/roomlist/src/test/kotlin/io/element/android/features/roomlist/RoomListPresenterTests.kt @@ -14,14 +14,30 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package io.element.android.features.roomlist import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.features.roomlist.model.RoomListEvents +import io.element.android.features.roomlist.model.RoomListRoomSummary +import io.element.android.libraries.dateformatter.LastMessageFormatter +import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.core.SessionId +import io.element.android.libraries.matrixtest.AN_AVATAR_URL +import io.element.android.libraries.matrixtest.AN_EXCEPTION +import io.element.android.libraries.matrixtest.A_MESSAGE +import io.element.android.libraries.matrixtest.A_ROOM_ID +import io.element.android.libraries.matrixtest.A_ROOM_NAME +import io.element.android.libraries.matrixtest.A_USER_ID +import io.element.android.libraries.matrixtest.A_USER_NAME import io.element.android.libraries.matrixtest.FakeMatrixClient +import io.element.android.libraries.matrixtest.room.FakeRoomSummaryDataSource +import io.element.android.libraries.matrixtest.room.aRoomSummaryFilled +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Test @@ -29,11 +45,11 @@ class RoomListPresenterTests { @Test fun `present - should start with no user and then load user with success`() = runTest { - val presenter = RoomListPresenter( FakeMatrixClient( SessionId("sessionId") - ), LastMessageFormatter() + ), + createDateFormatter() ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -41,7 +57,166 @@ class RoomListPresenterTests { val initialState = awaitItem() assertThat(initialState.matrixUser).isNull() val withUserState = awaitItem() - assertThat(withUserState).isNotNull() + assertThat(withUserState.matrixUser).isNotNull() + assertThat(withUserState.matrixUser!!.id).isEqualTo(A_USER_ID) + assertThat(withUserState.matrixUser!!.username).isEqualTo(A_USER_NAME) + assertThat(withUserState.matrixUser!!.avatarData.name).isEqualTo(A_USER_NAME) + assertThat(withUserState.matrixUser!!.avatarData.url).isEqualTo(AN_AVATAR_URL) + } + } + + @Test + fun `present - should start with no user and then load user with error`() = runTest { + val presenter = RoomListPresenter( + FakeMatrixClient( + SessionId("sessionId"), + userDisplayName = Result.failure(AN_EXCEPTION), + userAvatarURLString = Result.failure(AN_EXCEPTION), + ), + createDateFormatter() + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.matrixUser).isNull() + val withUserState = awaitItem() + assertThat(withUserState.matrixUser).isNotNull() + // username fallback to user id value + assertThat(withUserState.matrixUser!!.username).isEqualTo(A_USER_ID.value) + } + } + + @Test + fun `present - should filter room with success`() = runTest { + val presenter = RoomListPresenter( + FakeMatrixClient( + SessionId("sessionId") + ), + createDateFormatter() + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val withUserState = awaitItem() + assertThat(withUserState.filter).isEqualTo("") + withUserState.eventSink.invoke(RoomListEvents.UpdateFilter("t")) + val withFilterState = awaitItem() + assertThat(withFilterState.filter).isEqualTo("t") + } + } + + @Test + fun `present - load 1 room with success`() = runTest { + val roomSummaryDataSource = FakeRoomSummaryDataSource() + val presenter = RoomListPresenter( + FakeMatrixClient( + sessionId = SessionId("sessionId"), + roomSummaryDataSource = roomSummaryDataSource + ), + createDateFormatter() + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val withUserState = awaitItem() + // Room list is loaded with 16 placeholders + assertThat(withUserState.roomList.size).isEqualTo(16) + assertThat(withUserState.roomList.all { it.isPlaceholder }).isTrue() + roomSummaryDataSource.postRoomSummary(listOf(aRoomSummaryFilled())) + skipItems(1) + val withRoomState = awaitItem() + assertThat(withRoomState.roomList.size).isEqualTo(1) + assertThat(withRoomState.roomList.first()).isEqualTo(aRoomListRoomSummary) + } + } + + @Test + fun `present - load 1 room with success and filter rooms`() = runTest { + val roomSummaryDataSource = FakeRoomSummaryDataSource() + val presenter = RoomListPresenter( + FakeMatrixClient( + sessionId = SessionId("sessionId"), + roomSummaryDataSource = roomSummaryDataSource + ), + createDateFormatter() + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + roomSummaryDataSource.postRoomSummary(listOf(aRoomSummaryFilled())) + skipItems(3) + val loadedState = awaitItem() + // Test filtering with result + loadedState.eventSink.invoke(RoomListEvents.UpdateFilter(A_ROOM_NAME.substring(0, 3))) + val withNotFilteredRoomState = awaitItem() + assertThat(withNotFilteredRoomState.filter).isEqualTo(A_ROOM_NAME.substring(0, 3)) + assertThat(withNotFilteredRoomState.roomList.size).isEqualTo(1) + assertThat(withNotFilteredRoomState.roomList.first()).isEqualTo(aRoomListRoomSummary) + // Test filtering without result + withNotFilteredRoomState.eventSink.invoke(RoomListEvents.UpdateFilter("tada")) + skipItems(1) // Filter update + val withFilteredRoomState = awaitItem() + assertThat(withFilteredRoomState.filter).isEqualTo("tada") + assertThat(withFilteredRoomState.roomList).isEmpty() + } + } + + @Test + fun `present - update visible range`() = runTest { + val roomSummaryDataSource = FakeRoomSummaryDataSource() + val presenter = RoomListPresenter( + FakeMatrixClient( + sessionId = SessionId("sessionId"), + roomSummaryDataSource = roomSummaryDataSource + ), + createDateFormatter() + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + roomSummaryDataSource.postRoomSummary(listOf(aRoomSummaryFilled())) + skipItems(3) + val loadedState = awaitItem() + // check initial value + assertThat(roomSummaryDataSource.latestSlidingSyncRange).isNull() + // Test empty range + loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(1, 0))) + assertThat(roomSummaryDataSource.latestSlidingSyncRange).isNull() + // Update visible range and check that range is transmitted to the SDK after computation + loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(0, 0))) + assertThat(roomSummaryDataSource.latestSlidingSyncRange).isEqualTo(IntRange(0, 20)) + loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(0, 1))) + assertThat(roomSummaryDataSource.latestSlidingSyncRange).isEqualTo(IntRange(0, 21)) + loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(19, 29))) + assertThat(roomSummaryDataSource.latestSlidingSyncRange).isEqualTo(IntRange(0, 49)) + loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(49, 59))) + assertThat(roomSummaryDataSource.latestSlidingSyncRange).isEqualTo(IntRange(29, 79)) + loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(149, 159))) + assertThat(roomSummaryDataSource.latestSlidingSyncRange).isEqualTo(IntRange(129, 179)) + loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(149, 259))) + assertThat(roomSummaryDataSource.latestSlidingSyncRange).isEqualTo(IntRange(129, 279)) + } + } + + private fun createDateFormatter(): LastMessageFormatter { + return FakeLastMessageFormatter().apply { + givenFormat(A_FORMATTED_DATE) } } } + +private const val A_FORMATTED_DATE = "formatted_date" + +private val aRoomListRoomSummary = RoomListRoomSummary( + id = A_ROOM_ID.value, + roomId = A_ROOM_ID, + name = A_ROOM_NAME, + hasUnread = true, + timestamp = A_FORMATTED_DATE, + lastMessage = A_MESSAGE, + avatarData = AvatarData(name = A_ROOM_NAME), + isPlaceholder = false, +) diff --git a/features/template/src/main/kotlin/io/element/android/features/template/TemplateView.kt b/features/template/src/main/kotlin/io/element/android/features/template/TemplateView.kt index 0de5a4df96..8069b3a991 100644 --- a/features/template/src/main/kotlin/io/element/android/features/template/TemplateView.kt +++ b/features/template/src/main/kotlin/io/element/android/features/template/TemplateView.kt @@ -17,11 +17,14 @@ package io.element.android.features.template import androidx.compose.foundation.layout.Box -import androidx.compose.material3.Text +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Text @Composable fun TemplateView( @@ -29,13 +32,23 @@ fun TemplateView( modifier: Modifier = Modifier, ) { Box(modifier, contentAlignment = Alignment.Center) { - Text("Template feature view") + Text( + "Template feature view", + color = MaterialTheme.colorScheme.primary, + ) } } -@Composable @Preview -fun TemplateViewPreview() { +@Composable +fun TemplateViewLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun TemplateViewDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { TemplateView( state = TemplateState(), ) diff --git a/features/template/src/test/kotlin/io/element/android/features/template/TemplatePresenterTests.kt b/features/template/src/test/kotlin/io/element/android/features/template/TemplatePresenterTests.kt index 39b7e32ea8..a14cd2761e 100644 --- a/features/template/src/test/kotlin/io/element/android/features/template/TemplatePresenterTests.kt +++ b/features/template/src/test/kotlin/io/element/android/features/template/TemplatePresenterTests.kt @@ -14,27 +14,39 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package io.element.android.features.template import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test -import com.google.common.truth.Truth +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Test class TemplatePresenterTests { @Test - fun `present - `() = runTest { - + fun `present - initial state`() = runTest { val presenter = TemplatePresenter() moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { val initialState = awaitItem() - Truth.assertThat(initialState) + assertThat(initialState) } + } + @Test + fun `present - send event`() = runTest { + val presenter = TemplatePresenter() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(TemplateEvents.MyEvent) + } } } diff --git a/gradle.properties b/gradle.properties index 6902acf2bd..df832c13ba 100644 --- a/gradle.properties +++ b/gradle.properties @@ -47,3 +47,6 @@ signing.element.nightly.keyPassword=Secret # Customise the Lint version to use a more recent version than the one bundled with AGP # https://googlesamples.github.io/android-custom-lint-rules/usage/newer-lint.md.html android.experimental.lint.version=8.0.0-alpha10 + +# Enable test fixture for all modules by default +android.experimental.enableTestFixtures=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c3b00a39a9..8ed886b47f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ ksp = "1.8.0-1.0.8" molecule = "0.7.0" # AndroidX -material = "1.6.1" +material = "1.8.0" corektx = "1.9.0" datastore = "1.0.0" constraintlayout = "2.1.4" @@ -97,7 +97,7 @@ test_junit = "junit:junit:4.13.2" test_runner = "androidx.test:runner:1.4.0" test_uiautomator = "androidx.test.uiautomator:uiautomator:2.2.0" test_junitext = "androidx.test.ext:junit:1.1.3" -test_mockk = "io.mockk:mockk:1.13.2" +test_mockk = "io.mockk:mockk:1.13.4" test_barista = "com.adevinta.android:barista:4.2.0" test_hamcrest = "org.hamcrest:hamcrest:2.2" test_orchestrator = "androidx.test:orchestrator:1.4.1" diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt index 94d81a28e2..d3ed18bee2 100644 --- a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt +++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt @@ -39,7 +39,8 @@ sealed interface Async { suspend fun (suspend () -> T).execute(state: MutableState>) { try { state.value = Async.Loading() - state.value = Async.Success(this()) + val result = this() + state.value = Async.Success(result) } catch (error: Throwable) { state.value = Async.Failure(error) } diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/data/StableCharSequence.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/data/StableCharSequence.kt index 25f68f2fea..e4ffe2dcaa 100644 --- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/data/StableCharSequence.kt +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/data/StableCharSequence.kt @@ -24,6 +24,8 @@ class StableCharSequence(val charSequence: CharSequence) { override fun hashCode() = hash override fun equals(other: Any?) = other is StableCharSequence && other.hash == hash + + override fun toString(): String = "StableCharSequence(\"$charSequence\")" } fun CharSequence.toStableCharSequence() = StableCharSequence(this) diff --git a/libraries/dateformatter/.gitignore b/libraries/dateformatter/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/libraries/dateformatter/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/libraries/dateformatter/build.gradle.kts b/libraries/dateformatter/build.gradle.kts new file mode 100644 index 0000000000..60ecd95052 --- /dev/null +++ b/libraries/dateformatter/build.gradle.kts @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022 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. + */ + +// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + id("io.element.android-library") + alias(libs.plugins.ksp) + alias(libs.plugins.anvil) +} + +anvil { + generateDaggerFactories.set(true) +} + +android { + namespace = "io.element.android.libraries.dateformatter" + + dependencies { + anvil(projects.anvilcodegen) + implementation(libs.dagger) + implementation(projects.libraries.di) + implementation(projects.anvilannotations) + api(libs.datetime) + ksp(libs.showkase.processor) + + testImplementation(libs.test.junit) + testImplementation(libs.test.truth) + } +} diff --git a/libraries/dateformatter/consumer-rules.pro b/libraries/dateformatter/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libraries/dateformatter/proguard-rules.pro b/libraries/dateformatter/proguard-rules.pro new file mode 100644 index 0000000000..ff59496d81 --- /dev/null +++ b/libraries/dateformatter/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle.kts. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/libraries/dateformatter/src/main/AndroidManifest.xml b/libraries/dateformatter/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..cf0e6386de --- /dev/null +++ b/libraries/dateformatter/src/main/AndroidManifest.xml @@ -0,0 +1,16 @@ + + diff --git a/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/LastMessageFormatter.kt b/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/LastMessageFormatter.kt new file mode 100644 index 0000000000..caa5886cf9 --- /dev/null +++ b/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/LastMessageFormatter.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.dateformatter + +interface LastMessageFormatter { + fun format(timestamp: Long?): String +} diff --git a/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/di/DateFormatterModule.kt b/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/di/DateFormatterModule.kt new file mode 100644 index 0000000000..feab851a8b --- /dev/null +++ b/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/di/DateFormatterModule.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.dateformatter.di + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Module +import dagger.Provides +import io.element.android.libraries.di.AppScope +import kotlinx.datetime.Clock +import kotlinx.datetime.TimeZone +import java.util.* + +@Module +@ContributesTo(AppScope::class) +object DateFormatterModule { + @Provides + fun providesClock(): Clock = Clock.System + + @Provides + fun providesLocale(): Locale = Locale.getDefault() + + @Provides + fun providesTimezone(): TimeZone = TimeZone.currentSystemDefault() +} diff --git a/features/roomlist/src/main/kotlin/io/element/android/features/roomlist/LastMessageFormatter.kt b/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageFormatter.kt similarity index 79% rename from features/roomlist/src/main/kotlin/io/element/android/features/roomlist/LastMessageFormatter.kt rename to libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageFormatter.kt index 037ba5200d..a466491766 100644 --- a/features/roomlist/src/main/kotlin/io/element/android/features/roomlist/LastMessageFormatter.kt +++ b/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageFormatter.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 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. @@ -14,10 +14,13 @@ * limitations under the License. */ -package io.element.android.features.roomlist +package io.element.android.libraries.dateformatter.impl import android.text.format.DateFormat import android.text.format.DateUtils +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.dateformatter.LastMessageFormatter +import io.element.android.libraries.di.AppScope import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlinx.datetime.LocalDateTime @@ -32,32 +35,33 @@ import java.util.Locale import javax.inject.Inject import kotlin.math.absoluteValue -class LastMessageFormatter @Inject constructor() { - - private val clock: Clock = Clock.System - private val locale: Locale = Locale.getDefault() - +@ContributesBinding(AppScope::class) +class DefaultLastMessageFormatter @Inject constructor( + private val clock: Clock, + private val locale: Locale, + private val timezone: TimeZone, +) : LastMessageFormatter { private val onlyTimeFormatter: DateTimeFormatter by lazy { - val pattern = DateFormat.getBestDateTimePattern(locale, "HH:mm") + val pattern = DateFormat.getBestDateTimePattern(locale, "HH:mm") ?: "HH:mm" DateTimeFormatter.ofPattern(pattern) } private val dateWithMonthFormatter: DateTimeFormatter by lazy { - val pattern = DateFormat.getBestDateTimePattern(locale, "d MMM") + val pattern = DateFormat.getBestDateTimePattern(locale, "d MMM") ?: "d MMM" DateTimeFormatter.ofPattern(pattern) } private val dateWithYearFormatter: DateTimeFormatter by lazy { - val pattern = DateFormat.getBestDateTimePattern(locale, "dd.MM.yyyy") + val pattern = DateFormat.getBestDateTimePattern(locale, "dd.MM.yyyy") ?: "dd.MM.yyyy" DateTimeFormatter.ofPattern(pattern) } - fun format(timestamp: Long?): String { + override fun format(timestamp: Long?): String { if (timestamp == null) return "" val now: Instant = clock.now() val tsInstant = Instant.fromEpochMilliseconds(timestamp) - val nowDateTime = now.toLocalDateTime(TimeZone.currentSystemDefault()) - val tsDateTime = tsInstant.toLocalDateTime(TimeZone.currentSystemDefault()) + val nowDateTime = now.toLocalDateTime(timezone) + val tsDateTime = tsInstant.toLocalDateTime(timezone) val isSameDay = nowDateTime.date == tsDateTime.date return when { isSameDay -> { @@ -77,7 +81,7 @@ class LastMessageFormatter @Inject constructor() { return if (period.years.absoluteValue >= 1) { formatDateWithYear(date) } else if (period.days.absoluteValue < 2 && period.months.absoluteValue < 1) { - getRelativeDay(date.toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds()) + getRelativeDay(date.toInstant(timezone).toEpochMilliseconds()) } else { formatDateWithMonth(date) } @@ -97,6 +101,6 @@ class LastMessageFormatter @Inject constructor() { clock.now().toEpochMilliseconds(), DateUtils.DAY_IN_MILLIS, DateUtils.FORMAT_SHOW_WEEKDAY - ).toString() + )?.toString() ?: "" } } diff --git a/libraries/dateformatter/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageFormatterTest.kt b/libraries/dateformatter/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageFormatterTest.kt new file mode 100644 index 0000000000..c21dcf4230 --- /dev/null +++ b/libraries/dateformatter/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageFormatterTest.kt @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.dateformatter.impl + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.dateformatter.LastMessageFormatter +import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone +import org.junit.Test +import java.util.Locale + +class DefaultLastMessageFormatterTest { + + @Test + fun `test null`() { + val now = "1980-04-06T18:35:24.00Z" + val formatter = createFormatter(now) + assertThat(formatter.format(null)).isEmpty() + } + + @Test + fun `test epoch`() { + val now = "1980-04-06T18:35:24.00Z" + val formatter = createFormatter(now) + assertThat(formatter.format(0)).isEqualTo("01.01.1970") + } + + @Test + fun `test now`() { + val now = "1980-04-06T18:35:24.00Z" + val dat = "1980-04-06T18:35:24.00Z" + val formatter = createFormatter(now) + assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("18:35") + } + + @Test + fun `test one second before`() { + val now = "1980-04-06T18:35:24.00Z" + val dat = "1980-04-06T18:35:23.00Z" + val formatter = createFormatter(now) + assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("18:35") + } + + @Test + fun `test one minute before`() { + val now = "1980-04-06T18:35:24.00Z" + val dat = "1980-04-06T18:34:24.00Z" + val formatter = createFormatter(now) + assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("18:34") + } + + @Test + fun `test one hour before`() { + val now = "1980-04-06T18:35:24.00Z" + val dat = "1980-04-06T17:35:24.00Z" + val formatter = createFormatter(now) + assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("17:35") + } + + @Test + fun `test one day before same time`() { + val now = "1980-04-06T18:35:24.00Z" + val dat = "1980-04-05T18:35:24.00Z" + val formatter = createFormatter(now) + // TODO DateUtils.getRelativeTimeSpanString returns null. + assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("") + } + + @Test + fun `test one month before same time`() { + val now = "1980-04-06T18:35:24.00Z" + val dat = "1980-03-06T18:35:24.00Z" + val formatter = createFormatter(now) + assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("6 Mar") + } + + @Test + fun `test one year before same time`() { + val now = "1980-04-06T18:35:24.00Z" + val dat = "1979-04-06T18:35:24.00Z" + val formatter = createFormatter(now) + assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("06.04.1979") + } + + /** + * Create DefaultLastMessageFormatter and set current time to the provided date. + */ + private fun createFormatter(@Suppress("SameParameterValue") currentDate: String): LastMessageFormatter { + val clock = FakeClock().also { it.givenInstant(Instant.parse(currentDate)) } + return DefaultLastMessageFormatter(clock, Locale.US, TimeZone.UTC) + } +} diff --git a/features/preferences/src/test/kotlin/io/element/android/features/preferences/ExampleUnitTest.kt b/libraries/dateformatter/src/test/kotlin/io/element/android/libraries/dateformatter/impl/FakeClock.kt similarity index 58% rename from features/preferences/src/test/kotlin/io/element/android/features/preferences/ExampleUnitTest.kt rename to libraries/dateformatter/src/test/kotlin/io/element/android/libraries/dateformatter/impl/FakeClock.kt index 3b615c83e9..58a5495218 100644 --- a/features/preferences/src/test/kotlin/io/element/android/features/preferences/ExampleUnitTest.kt +++ b/libraries/dateformatter/src/test/kotlin/io/element/android/libraries/dateformatter/impl/FakeClock.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 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. @@ -14,19 +14,17 @@ * limitations under the License. */ -package io.element.android.features.preferences +package io.element.android.libraries.dateformatter.impl -import org.junit.Assert.assertEquals -import org.junit.Test +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) +class FakeClock : Clock { + private var instant: Instant = Instant.fromEpochMilliseconds(0) + + fun givenInstant(instant: Instant) { + this.instant = instant } + + override fun now(): Instant = instant } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/Color.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/Color.kt index 936f102a85..279b38fdd1 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/Color.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/Color.kt @@ -52,10 +52,4 @@ val ElementGreen = Color(0xFF0DBD8B) val ElementOrange = Color(0xFFD9B072) val Vermilion = Color(0xFFFF5B55) -// TODO Update color -val MessageHighlightLight = Azure - -// TODO Update color -val MessageHighlightDark = Azure - val LinkColor = Color(0xFF054F6E) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/ColorUtil.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/ColorUtil.kt index b15bfc49db..469c70913a 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/ColorUtil.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/ColorUtil.kt @@ -25,6 +25,6 @@ fun Boolean.toEnabledColor(): Color { return if (this) { MaterialTheme.colorScheme.primary } else { - MaterialTheme.colorScheme.secondary + MaterialTheme.colorScheme.primary.copy(alpha = 0.40f) } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/Type.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/ElementTextStyles.kt similarity index 83% rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/Type.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/ElementTextStyles.kt index 6e97bfa4fb..4e0e3bd854 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/Type.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/ElementTextStyles.kt @@ -16,55 +16,13 @@ package io.element.android.libraries.designsystem -import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.sp -import com.airbnb.android.showkase.annotation.ShowkaseTypography - -@ShowkaseTypography(name = "Body Large", group = "Element") -val bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp -) - -@ShowkaseTypography(name = "Headline Small", group = "Element") -val headlineSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Bold, - fontSize = 32.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp -) - -// Set of Material typography styles to start with -val Typography = Typography( - bodyLarge = bodyLarge, - headlineSmall = headlineSmall, - /* Other default text styles to override - titleLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), - labelSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ) - */ -) +// TODO Remove object ElementTextStyles { object Bold { diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt index 591baf1152..54a8be0334 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt @@ -21,7 +21,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.text.InlineTextContent import androidx.compose.material3.LocalTextStyle -import androidx.compose.material3.Text +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -32,6 +32,7 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextStyle +import io.element.android.libraries.designsystem.theme.components.Text import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.persistentMapOf @@ -83,6 +84,7 @@ fun ClickableLinkText( onTextLayout = { layoutResult.value = it }, - inlineContent = inlineContent + inlineContent = inlineContent, + color = MaterialTheme.colorScheme.primary, ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledCheckbox.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledCheckbox.kt index f1ad5738ac..698d2fb6ee 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledCheckbox.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledCheckbox.kt @@ -18,12 +18,15 @@ package io.element.android.libraries.designsystem.components import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.Checkbox -import androidx.compose.material3.Text +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Checkbox +import io.element.android.libraries.designsystem.theme.components.Text @Composable fun LabelledCheckbox( @@ -42,13 +45,23 @@ fun LabelledCheckbox( onCheckedChange = onCheckedChange, enabled = enabled, ) - Text(text = text) + Text( + text = text, + color = MaterialTheme.colorScheme.primary, + ) } } @Preview @Composable -fun LabelledCheckboxPreview() { +fun LabelledCheckboxLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun LabelledCheckboxDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { LabelledCheckbox( checked = true, text = "Some text", diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ProgressDialog.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ProgressDialog.kt index 8bcc77226b..5028bcc030 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ProgressDialog.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ProgressDialog.kt @@ -22,9 +22,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -32,6 +30,10 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator +import io.element.android.libraries.designsystem.theme.components.Text @Composable fun ProgressDialog( @@ -48,19 +50,19 @@ fun ProgressDialog( modifier = modifier .fillMaxWidth() .background( - color = MaterialTheme.colorScheme.onBackground, + color = MaterialTheme.colorScheme.surfaceVariant, shape = RoundedCornerShape(8.dp) ) ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { CircularProgressIndicator( modifier = Modifier.padding(16.dp), - color = MaterialTheme.colorScheme.background + color = MaterialTheme.colorScheme.onSurfaceVariant ) if (!text.isNullOrBlank()) { Text( text = text, - color = MaterialTheme.colorScheme.background, + color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(16.dp) ) } @@ -69,8 +71,15 @@ fun ProgressDialog( } } -@Composable @Preview -fun ProgressDialogPreview() { +@Composable +fun ProgressDialogLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun ProgressDialogDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { ProgressDialog(text = "test dialog content") } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/VectorIcon.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/VectorIcon.kt index 60e7c644ad..d576b1dacc 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/VectorIcon.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/VectorIcon.kt @@ -16,7 +16,6 @@ package io.element.android.libraries.designsystem.components -import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -29,7 +28,7 @@ fun VectorIcon( modifier: Modifier = Modifier, tint: Color = LocalContentColor.current, ) { - Icon( + androidx.compose.material3.Icon( painter = painterResource(id = resourceId), contentDescription = null, modifier = modifier, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt index 1df00a7c50..b9a6d19bc8 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt @@ -20,7 +20,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -34,6 +33,9 @@ import androidx.compose.ui.unit.sp import coil.compose.AsyncImage import io.element.android.libraries.designsystem.AvatarGradientEnd import io.element.android.libraries.designsystem.AvatarGradientStart +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Text import timber.log.Timber @Composable @@ -97,6 +99,13 @@ private fun InitialsAvatar( @Preview @Composable -fun InitialsAvatarPreview() { +fun AvatarLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun AvatarDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { Avatar(AvatarData(name = "A")) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ConfirmationDialog.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ConfirmationDialog.kt index 6059b6b998..aee26ba7f2 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ConfirmationDialog.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ConfirmationDialog.kt @@ -22,13 +22,20 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.Text +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.ui.strings.R as StringR @Composable @@ -43,6 +50,12 @@ fun ConfirmationDialog( onCancelClicked: () -> Unit = {}, onThirdButtonClicked: () -> Unit = {}, onDismiss: () -> Unit = {}, + shape: Shape = AlertDialogDefaults.shape, + containerColor: Color = MaterialTheme.colorScheme.surfaceVariant, + iconContentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + titleContentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + textContentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + tonalElevation: Dp = AlertDialogDefaults.TonalElevation, ) { AlertDialog( modifier = modifier, @@ -93,12 +106,25 @@ fun ConfirmationDialog( } } }, + shape = shape, + containerColor = containerColor, + iconContentColor = iconContentColor, + titleContentColor = titleContentColor, + textContentColor = textContentColor, + tonalElevation = tonalElevation, ) } -@Composable @Preview -fun ConfirmationDialogPreview() { +@Composable +fun ConfirmationDialogLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun ConfirmationDialogDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { ConfirmationDialog( title = "Title", content = "Content", diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ErrorDialog.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ErrorDialog.kt index 99e473fdbd..fc4aefe6f1 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ErrorDialog.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ErrorDialog.kt @@ -21,13 +21,20 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.Text +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.ui.strings.R as StringR @Composable @@ -37,6 +44,12 @@ fun ErrorDialog( title: String = stringResource(id = StringR.string.dialog_title_error), submitText: String = stringResource(id = StringR.string.ok), onDismiss: () -> Unit = {}, + shape: Shape = AlertDialogDefaults.shape, + containerColor: Color = MaterialTheme.colorScheme.surfaceVariant, + iconContentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + titleContentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + textContentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + tonalElevation: Dp = AlertDialogDefaults.TonalElevation, ) { AlertDialog( modifier = modifier, @@ -62,12 +75,25 @@ fun ErrorDialog( } } }, + shape = shape, + containerColor = containerColor, + iconContentColor = iconContentColor, + titleContentColor = titleContentColor, + textContentColor = textContentColor, + tonalElevation = tonalElevation, ) } -@Composable @Preview -fun ErrorDialogPreview() { +@Composable +fun ErrorDialogLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun ErrorDialogDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { ErrorDialog( content = "Content", ) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceCategory.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceCategory.kt index c9e6ac6740..5129d58e16 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceCategory.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceCategory.kt @@ -20,13 +20,18 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Divider +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Announcement +import androidx.compose.material.icons.filled.BugReport import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Divider +import io.element.android.libraries.designsystem.theme.components.Text @Composable fun PreferenceCategory( @@ -46,20 +51,39 @@ fun PreferenceCategory( Text( modifier = Modifier.padding(top = 8.dp, start = 56.dp), style = MaterialTheme.typography.titleSmall, - text = title + color = MaterialTheme.colorScheme.primary, + text = title, ) content() } } -@Composable @Preview -fun PreferenceCategoryPreview() { +@Composable +fun PreferenceCategoryLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun PreferenceCategoryDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { PreferenceCategory( title = "Category title", ) { - PreferenceTextPreview() - PreferenceSwitchPreview() - PreferenceSlidePreview() + PreferenceText( + title = "Title", + icon = Icons.Default.BugReport, + ) + PreferenceSwitch( + title = "Switch", + icon = Icons.Default.Announcement, + isChecked = true + ) + PreferenceSlide( + title = "Slide", + summary = "Summary", + value = 0.75F + ) } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceScreen.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceScreen.kt index 4b5f007506..08d5a377a6 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceScreen.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceScreen.kt @@ -28,13 +28,10 @@ import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Announcement import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.BugReport import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -42,6 +39,13 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TopAppBar @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -110,12 +114,36 @@ fun PreferenceTopAppBar( ) } -@Composable @Preview -fun PreferenceScreenPreview() { +@Composable +fun PreferenceViewLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun PreferenceViewDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { PreferenceView( title = "Preference screen" ) { - PreferenceCategoryPreview() + PreferenceCategory( + title = "Category title", + ) { + PreferenceText( + title = "Title", + icon = Icons.Default.BugReport, + ) + PreferenceSwitch( + title = "Switch", + icon = Icons.Default.Announcement, + isChecked = true + ) + PreferenceSlide( + title = "Slide", + summary = "Summary", + value = 0.75F + ) + } } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceSlide.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceSlide.kt index aadb0ab236..c16eaab45b 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceSlide.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceSlide.kt @@ -24,14 +24,16 @@ import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Slider -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.tooling.preview.Preview import io.element.android.libraries.designsystem.components.preferences.components.PreferenceIcon +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Slider +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.toEnabledColor @Composable @@ -84,9 +86,16 @@ fun PreferenceSlide( } } -@Composable @Preview -fun PreferenceSlidePreview() { +@Composable +fun PreferenceSlideLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun PreferenceSlideDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { PreferenceSlide( title = "Slide", summary = "Summary", diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceSwitch.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceSwitch.kt index 68a402296c..53e4e0a29c 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceSwitch.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceSwitch.kt @@ -24,15 +24,17 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Announcement -import androidx.compose.material3.Checkbox import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.tooling.preview.Preview import io.element.android.libraries.designsystem.components.preferences.components.PreferenceIcon +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Checkbox +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.toEnabledColor @Composable @@ -75,12 +77,20 @@ fun PreferenceSwitch( } } -@Composable @Preview -fun PreferenceSwitchPreview() { +@Composable +fun PreferenceSwitchLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun PreferenceSwitchDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { PreferenceSwitch( title = "Switch", icon = Icons.Default.Announcement, + enabled = true, isChecked = true ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt index bef45830e2..1c10d6259a 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt @@ -25,13 +25,15 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.BugReport import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.tooling.preview.Preview import io.element.android.libraries.designsystem.components.preferences.components.PreferenceIcon +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Text @Composable fun PreferenceText( @@ -57,15 +59,23 @@ fun PreferenceText( .weight(1f) .padding(end = preferencePaddingEnd), style = MaterialTheme.typography.bodyLarge, - text = title + text = title, + color = MaterialTheme.colorScheme.primary, ) } } } -@Composable @Preview -fun PreferenceTextPreview() { +@Composable +fun PreferenceTextLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun PreferenceTextDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { PreferenceText( title = "Title", icon = Icons.Default.BugReport, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/components/PreferenceIcon.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/components/PreferenceIcon.kt index 6a821333cc..81c8146c57 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/components/PreferenceIcon.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/components/PreferenceIcon.kt @@ -19,11 +19,11 @@ package io.element.android.libraries.designsystem.components.preferences.compone import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.toEnabledColor @Composable diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreview.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreview.kt new file mode 100644 index 0000000000..f79cfed70b --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreview.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.designsystem.preview + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.element.android.libraries.designsystem.theme.ElementTheme + +@Composable +fun ElementPreviewLight( + showBackground: Boolean = true, + content: @Composable () -> Unit +) { + ElementPreview( + darkTheme = false, + showBackground = showBackground, + content = content + ) +} + +@Composable +fun ElementPreviewDark( + showBackground: Boolean = true, + content: @Composable () -> Unit +) { + ElementPreview( + darkTheme = true, + showBackground = showBackground, + content = content + ) +} + +@Composable +private fun ElementPreview( + darkTheme: Boolean, + showBackground: Boolean, + content: @Composable () -> Unit +) { + ElementTheme(darkTheme = darkTheme) { + if (showBackground) { + Box(modifier = Modifier.background(MaterialTheme.colorScheme.background)) { + content() + } + } else { + content() + } + } +} + diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt new file mode 100644 index 0000000000..8dcf4a9dfe --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.designsystem.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import io.element.android.libraries.designsystem.SystemGrey4Dark +import io.element.android.libraries.designsystem.SystemGrey6Light +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.previews.ColorListPreview +import kotlinx.collections.immutable.persistentMapOf + +/** + * Room list. + */ +@Composable +fun MaterialTheme.roomListRoomName() = colorScheme.primary + +@Composable +fun MaterialTheme.roomListRoomMessage() = colorScheme.secondary + +@Composable +fun MaterialTheme.roomListRoomMessageDate() = colorScheme.secondary + +@Composable +fun MaterialTheme.roomListUnreadIndicator() = colorScheme.primary + +@Composable +fun ElementColors.roomListPlaceHolder() = if (isLight) SystemGrey6Light else SystemGrey4Dark + +@Preview +@Composable +fun ColorAliasesLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun ColorAliasesDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + ColorListPreview( + backgroundColor = Color.Black, + foregroundColor = Color.White, + colors = persistentMapOf( + "roomListRoomName" to MaterialTheme.roomListRoomName(), + "roomListRoomMessage" to MaterialTheme.roomListRoomMessage(), + "roomListRoomMessageDate" to MaterialTheme.roomListRoomMessageDate(), + "roomListUnreadIndicator" to MaterialTheme.roomListUnreadIndicator(), + "roomListPlaceHolder" to ElementTheme.colors.roomListPlaceHolder(), + ) + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorsDark.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorsDark.kt new file mode 100644 index 0000000000..774158856e --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorsDark.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.designsystem.theme + +import androidx.compose.material3.darkColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import io.element.android.libraries.designsystem.Azure +import io.element.android.libraries.designsystem.DarkGrey +import io.element.android.libraries.designsystem.SystemGrey5Dark +import io.element.android.libraries.designsystem.SystemGrey6Dark +import io.element.android.libraries.designsystem.theme.previews.ColorsSchemePreview + +fun elementColorsDark() = ElementColors( + messageFromMeBackground = SystemGrey5Dark, + messageFromOtherBackground = SystemGrey6Dark, + messageHighlightedBackground = Azure, + isLight = false, +) + +// TODO Lots of colors are missing +val materialColorSchemeDark = darkColorScheme( + primary = Color.White, + // TODO onPrimary = ColorDarkTokens.OnPrimary, + // TODO primaryContainer = ColorDarkTokens.PrimaryContainer, + // TODO onPrimaryContainer = ColorDarkTokens.OnPrimaryContainer, + // TODO inversePrimary = ColorDarkTokens.InversePrimary, + secondary = DarkGrey, + // TODO onSecondary = ColorDarkTokens.OnSecondary, + // TODO secondaryContainer = ColorDarkTokens.SecondaryContainer, + // TODO onSecondaryContainer = ColorDarkTokens.OnSecondaryContainer, + tertiary = Color.White, + // TODO onTertiary = ColorDarkTokens.OnTertiary, + // TODO tertiaryContainer = ColorDarkTokens.TertiaryContainer, + // TODO onTertiaryContainer = ColorDarkTokens.OnTertiaryContainer, + background = Color.Black, + onBackground = Color.White, + surface = Color.Black, + onSurface = Color.White, + surfaceVariant = SystemGrey5Dark, + // TODO onSurfaceVariant = ColorDarkTokens.OnSurfaceVariant, + // TODO surfaceTint = primary, + // TODO inverseSurface = ColorDarkTokens.InverseSurface, + // TODO inverseOnSurface = ColorDarkTokens.InverseOnSurface, + // TODO error = ColorDarkTokens.Error, + // TODO onError = ColorDarkTokens.OnError, + // TODO errorContainer = ColorDarkTokens.ErrorContainer, + // TODO onErrorContainer = ColorDarkTokens.OnErrorContainer, + // TODO outline = ColorDarkTokens.Outline, + // TODO outlineVariant = ColorDarkTokens.OutlineVariant, + // TODO scrim = ColorDarkTokens.Scrim, +) + +@Preview +@Composable +fun ColorsSchemePreviewDark() = ColorsSchemePreview( + Color.White, + Color.Black, + materialColorSchemeDark, +) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorsLight.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorsLight.kt new file mode 100644 index 0000000000..dc13718360 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorsLight.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.designsystem.theme + +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import io.element.android.libraries.designsystem.Azure +import io.element.android.libraries.designsystem.LightGrey +import io.element.android.libraries.designsystem.SystemGrey5Light +import io.element.android.libraries.designsystem.SystemGrey6Light +import io.element.android.libraries.designsystem.theme.previews.ColorsSchemePreview + +fun elementColorsLight() = ElementColors( + messageFromMeBackground = SystemGrey5Light, + messageFromOtherBackground = SystemGrey6Light, + messageHighlightedBackground = Azure, + isLight = true, +) + +// TODO Lots of colors are missing +val materialColorSchemeLight = lightColorScheme( + primary = Color.Black, + // TODO onPrimary = ColorLightTokens.OnPrimary, + // TODO primaryContainer = ColorLightTokens.PrimaryContainer, + // TODO onPrimaryContainer = ColorLightTokens.OnPrimaryContainer, + // TODO inversePrimary = ColorLightTokens.InversePrimary, + secondary = LightGrey, + // TODO onSecondary = ColorLightTokens.OnSecondary, + // TODO secondaryContainer = ColorLightTokens.SecondaryContainer, + // TODO onSecondaryContainer = ColorLightTokens.OnSecondaryContainer, + tertiary = Color.Black, + // TODO onTertiary = ColorLightTokens.OnTertiary, + // TODO tertiaryContainer = ColorLightTokens.TertiaryContainer, + // TODO onTertiaryContainer = ColorLightTokens.OnTertiaryContainer, + background = Color.White, + onBackground = Color.Black, + surface = Color.White, + onSurface = Color.Black, + surfaceVariant = SystemGrey5Light, + onSurfaceVariant = Color.Black, + // TODO surfaceTint = primary, + // TODO inverseSurface = ColorLightTokens.InverseSurface, + // TODO inverseOnSurface = ColorLightTokens.InverseOnSurface, + // TODO error = ColorLightTokens.Error, + // TODO onError = ColorLightTokens.OnError, + // TODO errorContainer = ColorLightTokens.ErrorContainer, + // TODO onErrorContainer = ColorLightTokens.OnErrorContainer, + // TODO outline = ColorLightTokens.Outline, + // TODO outlineVariant = ColorLightTokens.OutlineVariant, + // TODO scrim = ColorLightTokens.Scrim, +) + +@Preview +@Composable +fun ColorsSchemePreviewLight() = ColorsSchemePreview( + Color.Black, + Color.White, + materialColorSchemeLight, +) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementColors.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementColors.kt new file mode 100644 index 0000000000..b39c31d157 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementColors.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.designsystem.theme + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color + +class ElementColors( + messageFromMeBackground: Color, + messageFromOtherBackground: Color, + messageHighlightedBackground: Color, + isLight: Boolean, +) { + var messageFromMeBackground by mutableStateOf(messageFromMeBackground) + private set + var messageFromOtherBackground by mutableStateOf(messageFromOtherBackground) + private set + var messageHighlightedBackground by mutableStateOf(messageHighlightedBackground) + private set + + var isLight by mutableStateOf(isLight) + private set + + fun copy( + messageFromMeBackground: Color = this.messageFromMeBackground, + messageFromOtherBackground: Color = this.messageFromOtherBackground, + messageHighlightedBackground: Color = this.messageHighlightedBackground, + isLight: Boolean = this.isLight, + ) = ElementColors( + messageFromMeBackground = messageFromMeBackground, + messageFromOtherBackground = messageFromOtherBackground, + messageHighlightedBackground = messageHighlightedBackground, + isLight = isLight, + ) + + fun updateColorsFrom(other: ElementColors) { + messageFromMeBackground = other.messageFromMeBackground + messageFromOtherBackground = other.messageFromOtherBackground + messageHighlightedBackground = other.messageHighlightedBackground + isLight = other.isLight + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/Theme.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementTheme.kt similarity index 55% rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/Theme.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementTheme.kt index d2168f644e..cbeb84badb 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/Theme.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementTheme.kt @@ -14,77 +14,58 @@ * limitations under the License. */ -package io.element.android.libraries.designsystem +package io.element.android.libraries.designsystem.theme import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.ColorScheme import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import com.google.accompanist.systemuicontroller.rememberSystemUiController -private val DarkColorScheme = darkColorScheme( - primary = Color.White, - secondary = DarkGrey, - tertiary = Color.White, - background = Color.Black, - onBackground = Color.White, - surface = Color.Black, - surfaceVariant = SystemGrey5Dark, - onSurface = Color.White, - onSurfaceVariant = Color.White, -) +/** + * Inspired from https://medium.com/@lucasyujideveloper/54cbcbde1ace + */ +object ElementTheme { + val colors: ElementColors + @Composable + @ReadOnlyComposable + get() = LocalColors.current +} -private val LightColorScheme = lightColorScheme( - primary = Color.Black, - secondary = LightGrey, - tertiary = Color.Black, - background = Color.White, - onBackground = Color.Black, - surface = Color.White, - surfaceVariant = SystemGrey5Light, - onSurface = Color.Black, - onSurfaceVariant = Color.Black, - - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ -) -@Suppress("CompositionLocalAllowlist") -val LocalIsDarkTheme = compositionLocalOf { error("Not defined") } +/* Global variables (application level) */ +val LocalColors = staticCompositionLocalOf { elementColorsLight() } @Composable -fun ElementXTheme( +fun ElementTheme( darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ - dynamicColor: Boolean = false, - content: @Composable () -> Unit + dynamicColor: Boolean = false, /* true to enable MaterialYou */ + lightColors: ElementColors = elementColorsLight(), + darkColors: ElementColors = elementColorsDark(), + materialLightColors: ColorScheme = materialColorSchemeLight, + materialDarkColors: ColorScheme = materialColorSchemeDark, + content: @Composable () -> Unit, ) { + val systemUiController = rememberSystemUiController() + val useDarkIcons = !darkTheme + val currentColor = remember { if (darkTheme) darkColors else lightColors } val colorScheme = when { dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { val context = LocalContext.current if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) } - darkTheme -> DarkColorScheme - else -> LightColorScheme + darkTheme -> materialDarkColors + else -> materialLightColors } - val systemUiController = rememberSystemUiController() - val useDarkIcons = !darkTheme - SideEffect { systemUiController.setStatusBarColor( color = colorScheme.background @@ -94,11 +75,13 @@ fun ElementXTheme( darkIcons = useDarkIcons ) } - - CompositionLocalProvider(LocalIsDarkTheme provides darkTheme) { + val rememberedColors = remember { currentColor.copy() }.apply { updateColorsFrom(currentColor) } + CompositionLocalProvider( + LocalColors provides rememberedColors, + ) { MaterialTheme( colorScheme = colorScheme, - typography = Typography, + // TODO typography = content = content ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementTypography.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementTypography.kt new file mode 100644 index 0000000000..b2159522c8 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementTypography.kt @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.designsystem.theme + +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp +import com.airbnb.android.showkase.annotation.ShowkaseTypography + +/** + * TODO Provide the typo to Material3 theme. + */ +@ShowkaseTypography(name = "H1", group = "Element") +val h1Default: TextStyle = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Bold, + fontSize = 24.sp +) + +@ShowkaseTypography(name = "Body1", group = "Element") +val body1Default: TextStyle = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Normal, + fontSize = 16.sp +) + +@ShowkaseTypography(name = "BodySmall", group = "Element") +val bodySmallDefault: TextStyle = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Normal, + fontSize = 14.sp +) + +@ShowkaseTypography(name = "bodyMedium", group = "Element") +val bodyMediumDefault: TextStyle = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Normal, + fontSize = 18.sp +) + +@ShowkaseTypography(name = "Body Large", group = "Element") +val bodyLargeDefault: TextStyle = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp +) + +@ShowkaseTypography(name = "Headline Small", group = "Element") +val headlineSmallDefault: TextStyle = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Bold, + fontSize = 24.sp, + lineHeight = 30.sp, + letterSpacing = 1.sp +) + +@ShowkaseTypography(name = "Headline Medium", group = "Element") +val headlineMediumDefault: TextStyle = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Bold, + fontSize = 28.sp, + lineHeight = 34.sp, + letterSpacing = 1.sp +) + +@ShowkaseTypography(name = "Headline Large", group = "Element") +val headlineLargeDefault: TextStyle = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Bold, + fontSize = 32.sp, + lineHeight = 38.sp, + letterSpacing = 1.sp +) + +@ShowkaseTypography(name = "titleSmall", group = "Element") +val titleSmallDefault: TextStyle = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.5.sp +) + +@ShowkaseTypography(name = "titleMedium", group = "Element") +val titleMediumDefault: TextStyle = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 18.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp +) + diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt new file mode 100644 index 0000000000..d20e5f9651 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.designsystem.theme.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ButtonElevation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.tooling.preview.Preview +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight + +@Composable +fun Button( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + shape: Shape = ButtonDefaults.shape, + colors: ButtonColors = ButtonDefaults.buttonColors(), + elevation: ButtonElevation? = ButtonDefaults.buttonElevation(), + border: BorderStroke? = null, + contentPadding: PaddingValues = ButtonDefaults.ContentPadding, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + content: @Composable RowScope.() -> Unit +) { + androidx.compose.material3.Button( + onClick = onClick, + modifier = modifier, + enabled = enabled, + shape = shape, + colors = colors, + elevation = elevation, + border = border, + contentPadding = contentPadding, + interactionSource = interactionSource, + content = content, + ) +} + +@Preview +@Composable +fun ButtonsLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun ButtonsDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + Column { + Button(onClick = {}, enabled = true) { + Text(text = "Click me! - Enabled") + } + Button(onClick = {}, enabled = false) { + Text(text = "Click me! - Disabled") + } + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Checkbox.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Checkbox.kt new file mode 100644 index 0000000000..2c8a7411fb --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Checkbox.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.designsystem.theme.components + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.CheckboxColors +import androidx.compose.material3.CheckboxDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight + +@Composable +fun Checkbox( + checked: Boolean, + onCheckedChange: ((Boolean) -> Unit)?, + modifier: Modifier = Modifier, + enabled: Boolean = true, + colors: CheckboxColors = CheckboxDefaults.colors(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } +) { + androidx.compose.material3.Checkbox( + checked = checked, + onCheckedChange = onCheckedChange, + modifier = modifier, + enabled = enabled, + colors = colors, + interactionSource = interactionSource, + ) +} + +@Preview +@Composable +fun CheckboxesLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun CheckboxesDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + Column { + Checkbox(onCheckedChange = {}, enabled = true, checked = true) + Checkbox(onCheckedChange = {}, enabled = true, checked = false) + Checkbox(onCheckedChange = {}, enabled = false, checked = true) + Checkbox(onCheckedChange = {}, enabled = false, checked = false) + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/CircularProgressIndicator.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/CircularProgressIndicator.kt new file mode 100644 index 0000000000..00a0f8c0de --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/CircularProgressIndicator.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.designsystem.theme.components + +import androidx.compose.material3.ProgressIndicatorDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp + +@Composable +fun CircularProgressIndicator( + progress: Float, + modifier: Modifier = Modifier, + color: Color = ProgressIndicatorDefaults.circularColor, + strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth +) { + androidx.compose.material3.CircularProgressIndicator( + modifier = modifier, + progress = progress, + color = color, + strokeWidth = strokeWidth, + ) +} + +@Composable +fun CircularProgressIndicator( + modifier: Modifier = Modifier, + color: Color = ProgressIndicatorDefaults.circularColor, + strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth, +) { + androidx.compose.material3.CircularProgressIndicator( + modifier = modifier, + color = color, + strokeWidth = strokeWidth, + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Divider.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Divider.kt new file mode 100644 index 0000000000..f15b81129e --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Divider.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.designsystem.theme.components + +import androidx.compose.material3.DividerDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight + +@Composable +fun Divider( + modifier: Modifier = Modifier, + thickness: Dp = DividerDefaults.Thickness, + color: Color = DividerDefaults.color, +) { + androidx.compose.material3.Divider( + modifier = modifier, + thickness = thickness, + color = color, + ) +} + +@Preview +@Composable +fun DividerLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun DividerDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + Divider() +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/FloatingActionButton.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/FloatingActionButton.kt new file mode 100644 index 0000000000..e27f2e7b25 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/FloatingActionButton.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.designsystem.theme.components + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material3.FloatingActionButtonDefaults +import androidx.compose.material3.FloatingActionButtonElevation +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape + +@Composable +fun FloatingActionButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + shape: Shape = FloatingActionButtonDefaults.shape, + containerColor: Color = FloatingActionButtonDefaults.containerColor, + contentColor: Color = contentColorFor(containerColor), + elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + content: @Composable () -> Unit, +) { + androidx.compose.material3.FloatingActionButton( + onClick = onClick, + modifier = modifier, + shape = shape, + containerColor = containerColor, + contentColor = contentColor, + elevation = elevation, + interactionSource = interactionSource, + content = content, + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Icon.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Icon.kt new file mode 100644 index 0000000000..6006a6c60a --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Icon.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.designsystem.theme.components + +import androidx.compose.material3.LocalContentColor +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.vector.ImageVector + +@Composable +fun Icon( + imageVector: ImageVector, + contentDescription: String?, + modifier: Modifier = Modifier, + tint: Color = LocalContentColor.current +) { + androidx.compose.material3.Icon( + imageVector = imageVector, + contentDescription = contentDescription, + modifier = modifier, + tint = tint, + ) +} + +@Composable +fun Icon( + bitmap: ImageBitmap, + contentDescription: String?, + modifier: Modifier = Modifier, + tint: Color = LocalContentColor.current +) { + androidx.compose.material3.Icon( + bitmap = bitmap, + contentDescription = contentDescription, + modifier = modifier, + tint = tint, + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/VectorButton.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/IconButton.kt similarity index 50% rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/VectorButton.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/IconButton.kt index 928e83cfa4..5af819c1f0 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/VectorButton.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/IconButton.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 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. @@ -14,20 +14,28 @@ * limitations under the License. */ -package io.element.android.libraries.designsystem.components +package io.element.android.libraries.designsystem.theme.components -import androidx.compose.material3.Button -import androidx.compose.material3.Text +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material3.IconButtonDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @Composable -fun VectorButton(text: String, enabled: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { - Button( +fun IconButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + content: @Composable () -> Unit +) { + androidx.compose.material3.IconButton( onClick = onClick, + modifier = modifier, enabled = enabled, - modifier = modifier - ) { - Text(text = text) - } + colors = IconButtonDefaults.iconButtonColors(), + interactionSource = interactionSource, + content = content, + ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/MediumTopAppBar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/MediumTopAppBar.kt new file mode 100644 index 0000000000..cbc215717d --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/MediumTopAppBar.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.designsystem.theme.components + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarColors +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MediumTopAppBar( + title: @Composable () -> Unit, + modifier: Modifier = Modifier, + navigationIcon: @Composable () -> Unit = {}, + actions: @Composable RowScope.() -> Unit = {}, + windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, + colors: TopAppBarColors = TopAppBarDefaults.mediumTopAppBarColors(), + scrollBehavior: TopAppBarScrollBehavior? = null +) { + androidx.compose.material3.MediumTopAppBar( + title = title, + modifier = modifier, + navigationIcon = navigationIcon, + actions = actions, + windowInsets = windowInsets, + colors = colors, + scrollBehavior = scrollBehavior, + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheetLayout.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheetLayout.kt new file mode 100644 index 0000000000..6b558e3418 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheetLayout.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.designsystem.theme.components + +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.MaterialTheme +import androidx.compose.material.ModalBottomSheetDefaults +import androidx.compose.material.ModalBottomSheetState +import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material.contentColorFor +import androidx.compose.material.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun ModalBottomSheetLayout( + sheetContent: @Composable ColumnScope.() -> Unit, + modifier: Modifier = Modifier, + sheetState: ModalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden), + sheetShape: Shape = MaterialTheme.shapes.large, + sheetElevation: Dp = ModalBottomSheetDefaults.Elevation, + sheetBackgroundColor: Color = MaterialTheme.colors.surface, + sheetContentColor: Color = contentColorFor(sheetBackgroundColor), + scrimColor: Color = ModalBottomSheetDefaults.scrimColor, + content: @Composable () -> Unit = {} +) { + androidx.compose.material.ModalBottomSheetLayout( + sheetContent = sheetContent, + modifier = modifier, + sheetState = sheetState, + sheetShape = sheetShape, + sheetElevation = sheetElevation, + sheetBackgroundColor = sheetBackgroundColor, + sheetContentColor = sheetContentColor, + scrimColor = scrimColor, + content = content, + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/OutlinedTextField.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/OutlinedTextField.kt new file mode 100644 index 0000000000..6e0cc157fe --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/OutlinedTextField.kt @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.designsystem.theme.components + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.TextFieldColors +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun OutlinedTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + readOnly: Boolean = false, + textStyle: TextStyle = LocalTextStyle.current, + label: @Composable (() -> Unit)? = null, + placeholder: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + supportingText: @Composable (() -> Unit)? = null, + isError: Boolean = false, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + singleLine: Boolean = false, + maxLines: Int = Int.MAX_VALUE, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + shape: Shape = TextFieldDefaults.outlinedShape, + colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors() +) { + androidx.compose.material3.OutlinedTextField( + value = value, + onValueChange = onValueChange, + modifier = modifier, + enabled = enabled, + readOnly = readOnly, + textStyle = textStyle, + label = label, + placeholder = placeholder, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + supportingText = supportingText, + isError = isError, + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + singleLine = singleLine, + maxLines = maxLines, + interactionSource = interactionSource, + shape = shape, + colors = colors, + ) +} + +@Preview +@Composable +fun OutlinedTextFieldsLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun OutlinedTextFieldsDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + Column { + OutlinedTextField(onValueChange = {}, value = "Content", isError = false, enabled = true, readOnly = true) + OutlinedTextField(onValueChange = {}, value = "Content", isError = false, enabled = true, readOnly = false) + OutlinedTextField(onValueChange = {}, value = "Content", isError = false, enabled = false, readOnly = true) + OutlinedTextField(onValueChange = {}, value = "Content", isError = false, enabled = false, readOnly = false) + OutlinedTextField(onValueChange = {}, value = "Content", isError = true, enabled = true, readOnly = true) + OutlinedTextField(onValueChange = {}, value = "Content", isError = true, enabled = true, readOnly = false) + OutlinedTextField(onValueChange = {}, value = "Content", isError = true, enabled = false, readOnly = true) + OutlinedTextField(onValueChange = {}, value = "Content", isError = true, enabled = false, readOnly = false) + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Scaffold.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Scaffold.kt new file mode 100644 index 0000000000..99a69c0553 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Scaffold.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.designsystem.theme.components + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FabPosition +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ScaffoldDefaults +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun Scaffold( + modifier: Modifier = Modifier, + topBar: @Composable () -> Unit = {}, + bottomBar: @Composable () -> Unit = {}, + snackbarHost: @Composable () -> Unit = {}, + floatingActionButton: @Composable () -> Unit = {}, + floatingActionButtonPosition: FabPosition = FabPosition.End, + containerColor: Color = MaterialTheme.colorScheme.background, + contentColor: Color = contentColorFor(containerColor), + contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets, + content: @Composable (PaddingValues) -> Unit +) { + androidx.compose.material3.Scaffold( + modifier = modifier, + topBar = topBar, + bottomBar = bottomBar, + snackbarHost = snackbarHost, + floatingActionButton = floatingActionButton, + floatingActionButtonPosition = floatingActionButtonPosition, + containerColor = containerColor, + contentColor = contentColor, + contentWindowInsets = contentWindowInsets, + content = content, + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Slider.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Slider.kt new file mode 100644 index 0000000000..a303e1ba77 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Slider.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.designsystem.theme.components + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.SliderColors +import androidx.compose.material3.SliderDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight + +@Composable +fun Slider( + value: Float, + onValueChange: (Float) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + valueRange: ClosedFloatingPointRange = 0f..1f, + /*@IntRange(from = 0)*/ + steps: Int = 0, + onValueChangeFinished: (() -> Unit)? = null, + colors: SliderColors = SliderDefaults.colors(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } +) { + androidx.compose.material3.Slider( + value = value, + onValueChange = onValueChange, + modifier = modifier, + enabled = enabled, + valueRange = valueRange, + steps = steps, + onValueChangeFinished = onValueChangeFinished, + colors = colors, + interactionSource = interactionSource, + ) +} + +@Preview +@Composable +fun SlidersLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun SlidersDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + Column { + Slider(onValueChange = {}, value = 0.33f, enabled = true) + Slider(onValueChange = {}, value = 0.33f, enabled = false) + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Surface.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Surface.kt new file mode 100644 index 0000000000..8518d8a715 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Surface.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.designsystem.theme.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Composable +fun Surface( + modifier: Modifier = Modifier, + shape: Shape = RectangleShape, + color: Color = MaterialTheme.colorScheme.surface, + contentColor: Color = contentColorFor(color), + tonalElevation: Dp = 0.dp, + shadowElevation: Dp = 0.dp, + border: BorderStroke? = null, + content: @Composable () -> Unit +) { + androidx.compose.material3.Surface( + modifier = modifier, + shape = shape, + color = color, + contentColor = contentColor, + tonalElevation = tonalElevation, + shadowElevation = shadowElevation, + border = border, + content = content, + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Text.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Text.kt new file mode 100644 index 0000000000..2e05f2a793 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Text.kt @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.designsystem.theme.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.utils.toHrf +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.persistentMapOf + +@Composable +fun Text( + text: String, + modifier: Modifier = Modifier, + color: Color = Color.Unspecified, + fontSize: TextUnit = TextUnit.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeight? = null, + fontFamily: FontFamily? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + onTextLayout: (TextLayoutResult) -> Unit = {}, + style: TextStyle = LocalTextStyle.current +) { + androidx.compose.material3.Text( + text = text, + modifier = modifier, + color = color, + fontSize = fontSize, + fontStyle = fontStyle, + fontWeight = fontWeight, + fontFamily = fontFamily, + letterSpacing = letterSpacing, + textDecoration = textDecoration, + textAlign = textAlign, + lineHeight = lineHeight, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines, + onTextLayout = onTextLayout, + style = style, + ) +} + +@Composable +fun Text( + text: AnnotatedString, + modifier: Modifier = Modifier, + color: Color = Color.Unspecified, + fontSize: TextUnit = TextUnit.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeight? = null, + fontFamily: FontFamily? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + inlineContent: ImmutableMap = persistentMapOf(), + onTextLayout: (TextLayoutResult) -> Unit = {}, + style: TextStyle = LocalTextStyle.current +) { + androidx.compose.material3.Text( + text = text, + modifier = modifier, + color = color, + fontSize = fontSize, + fontStyle = fontStyle, + fontWeight = fontWeight, + fontFamily = fontFamily, + letterSpacing = letterSpacing, + textDecoration = textDecoration, + textAlign = textAlign, + lineHeight = lineHeight, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines, + inlineContent = inlineContent, + onTextLayout = onTextLayout, + style = style, + ) +} + +@Preview +@Composable +fun TextLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun TextDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + val colors = mapOf( + "primary" to MaterialTheme.colorScheme.primary, + "secondary" to MaterialTheme.colorScheme.secondary, + "tertiary" to MaterialTheme.colorScheme.tertiary, + "background" to MaterialTheme.colorScheme.background, + "error" to MaterialTheme.colorScheme.error, + "surface" to MaterialTheme.colorScheme.surface, + "surfaceVariant" to MaterialTheme.colorScheme.surfaceVariant, + "primaryContainer" to MaterialTheme.colorScheme.primaryContainer, + "secondaryContainer" to MaterialTheme.colorScheme.secondaryContainer, + "tertiaryContainer" to MaterialTheme.colorScheme.tertiaryContainer, + // "inversePrimary" to MaterialTheme.colorScheme.inversePrimary, + "errorContainer" to MaterialTheme.colorScheme.errorContainer, + "inverseSurface" to MaterialTheme.colorScheme.inverseSurface, + ) + Column( + modifier = Modifier.width(IntrinsicSize.Max) + ) { + colors.keys.forEach { name -> + val color = colors[name]!! + val textColor = contentColorFor(backgroundColor = color) + Box( + modifier = Modifier + .background(color = color) + .fillMaxWidth() + .padding(2.dp) + ) { + Text( + text = "Text on $name\n${textColor.toHrf()} on ${color.toHrf()}", + color = textColor, + ) + } + Spacer(modifier = Modifier.height(2.dp)) + } + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt new file mode 100644 index 0000000000..27995b46af --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.designsystem.theme.components + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.TextFieldColors +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.VisualTransformation + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + readOnly: Boolean = false, + textStyle: TextStyle = LocalTextStyle.current, + label: @Composable (() -> Unit)? = null, + placeholder: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + supportingText: @Composable (() -> Unit)? = null, + isError: Boolean = false, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + singleLine: Boolean = false, + maxLines: Int = Int.MAX_VALUE, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + shape: Shape = TextFieldDefaults.filledShape, + colors: TextFieldColors = TextFieldDefaults.textFieldColors() +) { + androidx.compose.material3.TextField( + value = value, + onValueChange = onValueChange, + modifier = modifier, + enabled = enabled, + readOnly = readOnly, + textStyle = textStyle, + label = label, + placeholder = placeholder, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + supportingText = supportingText, + isError = isError, + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + singleLine = singleLine, + maxLines = maxLines, + interactionSource = interactionSource, + shape = shape, + colors = colors, + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TopAppBar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TopAppBar.kt new file mode 100644 index 0000000000..76c388cefe --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TopAppBar.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.designsystem.theme.components + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarColors +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TopAppBar( + title: @Composable () -> Unit, + modifier: Modifier = Modifier, + navigationIcon: @Composable () -> Unit = {}, + actions: @Composable RowScope.() -> Unit = {}, + windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, + colors: TopAppBarColors = TopAppBarDefaults.smallTopAppBarColors(), + scrollBehavior: TopAppBarScrollBehavior? = null +) { + androidx.compose.material3.TopAppBar( + title = title, + modifier = modifier, + navigationIcon = navigationIcon, + actions = actions, + windowInsets = windowInsets, + colors = colors, + scrollBehavior = scrollBehavior, + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/previews/ColorListPreview.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/previews/ColorListPreview.kt new file mode 100644 index 0000000000..f511d619d0 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/previews/ColorListPreview.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.designsystem.theme.previews + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import kotlinx.collections.immutable.ImmutableMap + +@Composable +internal fun ColorListPreview( + backgroundColor: Color, + foregroundColor: Color, + colors: ImmutableMap, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background(color = backgroundColor) + .fillMaxWidth() + ) { + colors.keys.forEach { name -> + val color = colors[name]!! + ColorPreview(backgroundColor = backgroundColor, foregroundColor = foregroundColor, name = name, color = color) + } + Spacer(modifier = Modifier.height(2.dp)) + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/previews/ColorPreview.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/previews/ColorPreview.kt new file mode 100644 index 0000000000..6cfcfec8ff --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/previews/ColorPreview.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.designsystem.theme.previews + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.utils.toHrf + +@Composable +internal fun ColorPreview( + backgroundColor: Color, + foregroundColor: Color, + name: String, color: Color, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier.fillMaxWidth()) { + Text(text = name + " " + color.toHrf(), fontSize = 6.sp, color = foregroundColor) + val backgroundBrush = Brush.linearGradient( + listOf( + backgroundColor, + foregroundColor, + ) + ) + Row( + modifier = Modifier.background(backgroundBrush) + ) { + repeat(2) { + Box( + modifier = Modifier + .padding(1.dp) + .background(color = color) + .height(10.dp) + .weight(1f) + ) + } + } + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/previews/ColorsSchemePreview.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/previews/ColorsSchemePreview.kt new file mode 100644 index 0000000000..6a764e9907 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/previews/ColorsSchemePreview.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.designsystem.theme.previews + +import androidx.compose.material3.ColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import kotlinx.collections.immutable.persistentMapOf + +@Composable +internal fun ColorsSchemePreview( + backgroundColor: Color, + foregroundColor: Color, + colorScheme: ColorScheme, + modifier: Modifier = Modifier, +) { + val colors = persistentMapOf( + "primary" to colorScheme.primary, + "onPrimary" to colorScheme.onPrimary, + "primaryContainer" to colorScheme.primaryContainer, + "onPrimaryContainer" to colorScheme.onPrimaryContainer, + "inversePrimary" to colorScheme.inversePrimary, + "secondary" to colorScheme.secondary, + "onSecondary" to colorScheme.onSecondary, + "secondaryContainer" to colorScheme.secondaryContainer, + "onSecondaryContainer" to colorScheme.onSecondaryContainer, + "tertiary" to colorScheme.tertiary, + "onTertiary" to colorScheme.onTertiary, + "tertiaryContainer" to colorScheme.tertiaryContainer, + "onTertiaryContainer" to colorScheme.onTertiaryContainer, + "background" to colorScheme.background, + "onBackground" to colorScheme.onBackground, + "surface" to colorScheme.surface, + "onSurface" to colorScheme.onSurface, + "surfaceVariant" to colorScheme.surfaceVariant, + "onSurfaceVariant" to colorScheme.onSurfaceVariant, + "surfaceTint" to colorScheme.surfaceTint, + "inverseSurface" to colorScheme.inverseSurface, + "inverseOnSurface" to colorScheme.inverseOnSurface, + "error" to colorScheme.error, + "onError" to colorScheme.onError, + "errorContainer" to colorScheme.errorContainer, + "onErrorContainer" to colorScheme.onErrorContainer, + "outline" to colorScheme.outline, + "outlineVariant" to colorScheme.outlineVariant, + "scrim" to colorScheme.scrim, + ) + ColorListPreview( + backgroundColor = backgroundColor, + foregroundColor = foregroundColor, + colors = colors, + modifier = modifier, + ) +} diff --git a/features/rageshake/src/test/kotlin/io/element/android/features/login/ExampleUnitTest.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Colors.kt similarity index 59% rename from features/rageshake/src/test/kotlin/io/element/android/features/login/ExampleUnitTest.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Colors.kt index ee6363e624..42723cbb54 100644 --- a/features/rageshake/src/test/kotlin/io/element/android/features/login/ExampleUnitTest.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Colors.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 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. @@ -14,19 +14,13 @@ * limitations under the License. */ -package io.element.android.features.login +package io.element.android.libraries.designsystem.utils -import org.junit.Assert.assertEquals -import org.junit.Test +import androidx.compose.ui.graphics.Color /** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). + * Convert color to Human Readable Format. */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } +internal fun Color.toHrf(): String { + return "0x" + value.toString(16).take(8).uppercase() } diff --git a/libraries/elementresources/build.gradle.kts b/libraries/elementresources/build.gradle.kts index 3451be5c38..a33e0bc471 100644 --- a/libraries/elementresources/build.gradle.kts +++ b/libraries/elementresources/build.gradle.kts @@ -23,5 +23,5 @@ android { } dependencies { - implementation("com.google.android.material:material:1.7.0") + implementation(libs.androidx.material) } diff --git a/libraries/matrix/build.gradle.kts b/libraries/matrix/build.gradle.kts index 6db92e02f3..c331eab4ee 100644 --- a/libraries/matrix/build.gradle.kts +++ b/libraries/matrix/build.gradle.kts @@ -19,7 +19,7 @@ plugins { id("io.element.android-library") alias(libs.plugins.anvil) - kotlin("plugin.serialization") version "1.8.0" + kotlin("plugin.serialization") version "1.8.10" } android { diff --git a/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/FakeMatrixClient.kt b/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/FakeMatrixClient.kt index 4593fe252a..aedc0fd1a2 100644 --- a/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/FakeMatrixClient.kt +++ b/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/FakeMatrixClient.kt @@ -25,10 +25,18 @@ import io.element.android.libraries.matrix.room.MatrixRoom import io.element.android.libraries.matrix.room.RoomSummaryDataSource import io.element.android.libraries.matrixtest.media.FakeMediaResolver import io.element.android.libraries.matrixtest.room.FakeMatrixRoom -import io.element.android.libraries.matrixtest.room.InMemoryRoomSummaryDataSource +import io.element.android.libraries.matrixtest.room.FakeRoomSummaryDataSource +import kotlinx.coroutines.delay import org.matrix.rustcomponents.sdk.MediaSource -class FakeMatrixClient(override val sessionId: SessionId) : MatrixClient { +class FakeMatrixClient( + override val sessionId: SessionId = SessionId(A_SESSION_ID), + private val userDisplayName: Result = Result.success(A_USER_NAME), + private val userAvatarURLString: Result = Result.success(AN_AVATAR_URL), + val roomSummaryDataSource: RoomSummaryDataSource = FakeRoomSummaryDataSource() +) : MatrixClient { + + private var logoutFailure: Throwable? = null override fun getRoom(roomId: RoomId): MatrixRoom? { return FakeMatrixRoom(roomId) @@ -39,23 +47,30 @@ class FakeMatrixClient(override val sessionId: SessionId) : MatrixClient { override fun stopSync() = Unit override fun roomSummaryDataSource(): RoomSummaryDataSource { - return InMemoryRoomSummaryDataSource() + return roomSummaryDataSource } override fun mediaResolver(): MediaResolver { return FakeMediaResolver() } - override suspend fun logout() = Unit + fun givenLogoutError(failure: Throwable) { + logoutFailure = failure + } - override fun userId(): UserId = UserId("") + override suspend fun logout() { + delay(100) + logoutFailure?.let { throw it } + } + + override fun userId(): UserId = A_USER_ID override suspend fun loadUserDisplayName(): Result { - return Result.success("") + return userDisplayName } override suspend fun loadUserAvatarURLString(): Result { - return Result.success("") + return userAvatarURLString } override suspend fun loadMediaContentForSource(source: MediaSource): Result { diff --git a/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/TestData.kt b/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/TestData.kt new file mode 100644 index 0000000000..970a3882ab --- /dev/null +++ b/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/TestData.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.matrixtest + +import io.element.android.libraries.matrix.core.EventId +import io.element.android.libraries.matrix.core.RoomId +import io.element.android.libraries.matrix.core.UserId + +const val A_USER_NAME = "alice" +const val A_PASSWORD = "password" + +val A_USER_ID = UserId("@alice:server.org") +val A_ROOM_ID = RoomId("!aRoomId") +val AN_EVENT_ID = EventId("\$anEventId") + +const val A_ROOM_NAME = "A room name" +const val A_MESSAGE = "Hello world!" +const val A_REPLY = "OK, I'll be there!" +const val ANOTHER_MESSAGE = "Hello universe!" + +const val A_HOMESERVER = "matrix.org" +const val A_HOMESERVER_2 = "matrix-client.org" +const val A_SESSION_ID = "sessionId" + +const val AN_AVATAR_URL = "mxc://data" + +const val A_FAILURE_REASON = "There has been a failure" +val A_THROWABLE = Throwable(A_FAILURE_REASON) +val AN_EXCEPTION = Exception(A_FAILURE_REASON) + diff --git a/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/auth/FakeAuthenticationService.kt b/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/auth/FakeAuthenticationService.kt new file mode 100644 index 0000000000..ab4935ede3 --- /dev/null +++ b/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/auth/FakeAuthenticationService.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.matrixtest.auth + +import io.element.android.libraries.matrix.MatrixClient +import io.element.android.libraries.matrix.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.core.SessionId +import io.element.android.libraries.matrixtest.A_HOMESERVER +import io.element.android.libraries.matrixtest.A_SESSION_ID +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +class FakeAuthenticationService : MatrixAuthenticationService { + private var homeserver: String = A_HOMESERVER + private var loginError: Throwable? = null + + override fun isLoggedIn(): Flow { + return flowOf(false) + } + + override suspend fun getLatestSessionId(): SessionId? { + return null + } + + override suspend fun restoreSession(sessionId: SessionId): MatrixClient? { + return null + } + + override fun getHomeserver(): String? { + return null + } + + fun givenHomeserver(homeserver: String) { + this.homeserver = homeserver + } + + override fun getHomeserverOrDefault(): String { + return homeserver + } + + override suspend fun setHomeserver(homeserver: String) { + delay(100) + } + + override suspend fun login(username: String, password: String): SessionId { + delay(100) + loginError?.let { throw it } + return SessionId(A_SESSION_ID) + } + + fun givenLoginError(throwable: Throwable?) { + loginError = throwable + } +} diff --git a/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/room/FakeMatrixRoom.kt b/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/room/FakeMatrixRoom.kt index cb1ce7538b..3f5028291c 100644 --- a/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/room/FakeMatrixRoom.kt +++ b/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/room/FakeMatrixRoom.kt @@ -20,17 +20,20 @@ import io.element.android.libraries.matrix.core.EventId import io.element.android.libraries.matrix.core.RoomId import io.element.android.libraries.matrix.room.MatrixRoom import io.element.android.libraries.matrix.timeline.MatrixTimeline +import io.element.android.libraries.matrixtest.A_ROOM_ID import io.element.android.libraries.matrixtest.timeline.FakeMatrixTimeline +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow class FakeMatrixRoom( - override val roomId: RoomId, + override val roomId: RoomId = A_ROOM_ID, override val name: String? = null, override val bestName: String = "", override val displayName: String = "", override val topic: String? = null, - override val avatarUrl: String? = null + override val avatarUrl: String? = null, + private val matrixTimeline: MatrixTimeline = FakeMatrixTimeline(), ) : MatrixRoom { override fun syncUpdateFlow(): Flow { @@ -38,7 +41,7 @@ class FakeMatrixRoom( } override fun timeline(): MatrixTimeline { - return FakeMatrixTimeline() + return matrixTimeline } override suspend fun fetchMembers(): Result { @@ -54,18 +57,34 @@ class FakeMatrixRoom( } override suspend fun sendMessage(message: String): Result { - TODO("Not yet implemented") + delay(100) + return Result.success(Unit) } + var editMessageParameter: String? = null + private set + override suspend fun editMessage(originalEventId: EventId, message: String): Result { - TODO("Not yet implemented") + editMessageParameter = message + delay(100) + return Result.success(Unit) } + var replyMessageParameter: String? = null + private set + override suspend fun replyMessage(eventId: EventId, message: String): Result { - TODO("Not yet implemented") + replyMessageParameter = message + delay(100) + return Result.success(Unit) } + var redactEventEventIdParam: EventId? = null + private set + override suspend fun redactEvent(eventId: EventId, reason: String?): Result { - TODO("Not yet implemented") + redactEventEventIdParam = eventId + delay(100) + return Result.success(Unit) } } diff --git a/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/room/InMemoryRoomSummaryDataSource.kt b/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/room/FakeRoomSummaryDataSource.kt similarity index 66% rename from libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/room/InMemoryRoomSummaryDataSource.kt rename to libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/room/FakeRoomSummaryDataSource.kt index 5179e911ab..9d7cb3e377 100644 --- a/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/room/InMemoryRoomSummaryDataSource.kt +++ b/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/room/FakeRoomSummaryDataSource.kt @@ -21,11 +21,22 @@ import io.element.android.libraries.matrix.room.RoomSummaryDataSource import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -class InMemoryRoomSummaryDataSource : RoomSummaryDataSource { +class FakeRoomSummaryDataSource : RoomSummaryDataSource { - override fun roomSummaries(): StateFlow> { - return MutableStateFlow(emptyList()) + private val roomSummariesFlow = MutableStateFlow>(emptyList()) + + suspend fun postRoomSummary(roomSummaries: List) { + roomSummariesFlow.emit(roomSummaries) } - override fun setSlidingSyncRange(range: IntRange) = Unit + override fun roomSummaries(): StateFlow> { + return roomSummariesFlow + } + + var latestSlidingSyncRange: IntRange? = null + private set + + override fun setSlidingSyncRange(range: IntRange) { + latestSlidingSyncRange = range + } } diff --git a/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/room/RoomSummaryFixture.kt b/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/room/RoomSummaryFixture.kt new file mode 100644 index 0000000000..41d9f6d524 --- /dev/null +++ b/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/room/RoomSummaryFixture.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.matrixtest.room + +import io.element.android.libraries.matrix.core.RoomId +import io.element.android.libraries.matrix.room.RoomSummary +import io.element.android.libraries.matrix.room.RoomSummaryDetails +import io.element.android.libraries.matrixtest.A_MESSAGE +import io.element.android.libraries.matrixtest.A_ROOM_ID +import io.element.android.libraries.matrixtest.A_ROOM_NAME + +fun aRoomSummaryFilled( + roomId: RoomId = A_ROOM_ID, + name: String = A_ROOM_NAME, + isDirect: Boolean = false, + avatarURLString: String? = null, + lastMessage: CharSequence? = A_MESSAGE, + lastMessageTimestamp: Long? = null, + unreadNotificationCount: Int = 2, +) = RoomSummary.Filled( + aRoomSummaryDetail( + roomId = roomId, + name = name, + isDirect = isDirect, + avatarURLString = avatarURLString, + lastMessage = lastMessage, + lastMessageTimestamp = lastMessageTimestamp, + unreadNotificationCount = unreadNotificationCount, + ) +) + +fun aRoomSummaryDetail( + roomId: RoomId = A_ROOM_ID, + name: String = A_ROOM_NAME, + isDirect: Boolean = false, + avatarURLString: String? = null, + lastMessage: CharSequence? = A_MESSAGE, + lastMessageTimestamp: Long? = null, + unreadNotificationCount: Int = 2, +) = RoomSummaryDetails( + roomId = roomId, + name = name, + isDirect = isDirect, + avatarURLString = avatarURLString, + lastMessage = lastMessage, + lastMessageTimestamp = lastMessageTimestamp, + unreadNotificationCount = unreadNotificationCount, +) diff --git a/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/timeline/FakeMatrixTimeline.kt b/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/timeline/FakeMatrixTimeline.kt index a4e00897ec..18127dcd2d 100644 --- a/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/timeline/FakeMatrixTimeline.kt +++ b/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/timeline/FakeMatrixTimeline.kt @@ -19,21 +19,20 @@ package io.element.android.libraries.matrixtest.timeline import io.element.android.libraries.matrix.core.EventId import io.element.android.libraries.matrix.timeline.MatrixTimeline import io.element.android.libraries.matrix.timeline.MatrixTimelineItem +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import org.matrix.rustcomponents.sdk.TimelineListener class FakeMatrixTimeline : MatrixTimeline { - - override var callback: MatrixTimeline.Callback? - get() = null - set(value) {} + override var callback: MatrixTimeline.Callback? = null override fun timelineItems(): Flow> { return emptyFlow() } override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result { + delay(100) return Result.success(Unit) } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserHeader.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserHeader.kt index 3fe339180e..e83c486476 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserHeader.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserHeader.kt @@ -24,7 +24,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -36,6 +35,9 @@ import androidx.compose.ui.unit.sp import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.core.UserId import io.element.android.libraries.matrix.ui.model.MatrixUser import io.element.android.libraries.matrix.ui.model.getBestName @@ -64,8 +66,9 @@ fun MatrixUserHeader( fontWeight = FontWeight.SemiBold, text = matrixUser.getBestName(), maxLines = 1, - overflow = TextOverflow.Ellipsis - ) + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.colorScheme.primary, + ) // Id if (matrixUser.username.isNullOrEmpty().not()) { Spacer(modifier = Modifier.height(4.dp)) @@ -82,7 +85,14 @@ fun MatrixUserHeader( @Preview @Composable -fun MatrixUserHeaderPreview() { +fun MatrixUserHeaderLightPreview() = ElementPreviewLight { ContentToPreview1() } + +@Preview +@Composable +fun MatrixUserHeaderDarkPreview() = ElementPreviewDark { ContentToPreview1() } + +@Composable +private fun ContentToPreview1() { MatrixUserHeader( MatrixUser( id = UserId("@alice:server.org"), @@ -94,7 +104,14 @@ fun MatrixUserHeaderPreview() { @Preview @Composable -fun MatrixUserHeaderNoUsernamePreview() { +fun MatrixUserHeaderNoUserNameLightPreview() = ElementPreviewLight { ContentToPreview2() } + +@Preview +@Composable +fun MatrixUserHeaderNoUserNameDarkPreview() = ElementPreviewDark { ContentToPreview2() } + +@Composable +private fun ContentToPreview2() { MatrixUserHeader( MatrixUser( id = UserId("@alice:server.org"), diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserRow.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserRow.kt index 498a8d17a9..50bb7d377f 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserRow.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserRow.kt @@ -24,7 +24,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -35,6 +34,9 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.core.UserId import io.element.android.libraries.matrix.ui.model.MatrixUser import io.element.android.libraries.matrix.ui.model.getBestName @@ -68,7 +70,8 @@ fun MatrixUserRow( fontWeight = FontWeight.SemiBold, text = matrixUser.getBestName(), maxLines = 1, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.colorScheme.primary, ) // Id if (matrixUser.username.isNullOrEmpty().not()) { @@ -86,7 +89,14 @@ fun MatrixUserRow( @Preview @Composable -fun MatrixUserRowPreview() { +fun MatrixUserRowLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun MatrixUserRowDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { MatrixUserRow( MatrixUser( id = UserId("@alice:server.org"), diff --git a/libraries/textcomposer/build.gradle.kts b/libraries/textcomposer/build.gradle.kts index 4b5f82f747..6f15fefa42 100644 --- a/libraries/textcomposer/build.gradle.kts +++ b/libraries/textcomposer/build.gradle.kts @@ -35,8 +35,9 @@ dependencies { implementation(projects.libraries.androidutils) implementation(projects.libraries.core) implementation(projects.libraries.matrix) + implementation(projects.libraries.designsystem) implementation(libs.wysiwyg) implementation(libs.androidx.constraintlayout) - implementation("com.google.android.material:material:1.7.0") + implementation(libs.androidx.material) ksp(libs.showkase.processor) } diff --git a/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index 930b5d6f77..2c5cc26798 100644 --- a/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -21,7 +21,7 @@ import android.net.Uri import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.material3.Text +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -33,6 +33,9 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.view.isInvisible import androidx.core.view.isVisible +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.ui.strings.R as StringR @Composable @@ -123,7 +126,8 @@ private fun FakeComposer( .align(Alignment.Center), textAlign = TextAlign.Center, text = "Composer Preview", - fontSize = 20.sp + fontSize = 20.sp, + color = MaterialTheme.colorScheme.secondary, ) } } @@ -145,7 +149,14 @@ private fun MessageComposerView.setup(isDarkMode: Boolean, composerMode: Message @Preview @Composable -fun TextComposerPreview() { +fun TextComposerLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun TextComposerDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { TextComposer( onSendMessage = {}, fullscreen = false, diff --git a/libraries/ui-strings/src/main/res/values-cs/strings.xml b/libraries/ui-strings/src/main/res/values-cs/strings.xml index c122de7798..b1f7df9bb4 100644 --- a/libraries/ui-strings/src/main/res/values-cs/strings.xml +++ b/libraries/ui-strings/src/main/res/values-cs/strings.xml @@ -2979,4 +2979,5 @@ Hlasovou zprávu nelze spustit, protože právě nahráváte živé vysílání. Ukončete prosím živé vysílání, abyste mohli začít nahrávat hlasovou zprávu Nelze spustit hlasovou zprávu Chyba připojení - nahrávání pozastaveno + Použít formát inline kódu \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-de/strings.xml b/libraries/ui-strings/src/main/res/values-de/strings.xml index 17fa9b6e44..06c477b410 100644 --- a/libraries/ui-strings/src/main/res/values-de/strings.xml +++ b/libraries/ui-strings/src/main/res/values-de/strings.xml @@ -2918,4 +2918,5 @@ Du kannst keine Sprachnachricht beginnen, da du im Moment eine Echtzeitübertragung aufzeichnest. Bitte beende deine Sprachübertragung, um ein Gespräch zu beginnen Kann Sprachnachricht nicht beginnen Verbindungsfehler − Aufnahme pausiert + Als Inline-Code formatieren \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-et/strings.xml b/libraries/ui-strings/src/main/res/values-et/strings.xml index f33ade2a7e..0f005fe04e 100644 --- a/libraries/ui-strings/src/main/res/values-et/strings.xml +++ b/libraries/ui-strings/src/main/res/values-et/strings.xml @@ -2256,7 +2256,7 @@ Koosta valikud Küsimus või teema Küsitluse küsimus või teema - Koosta üks küsitlus + Loo selline küsitlus Küsitlus Saada e-posti aadressid ja telefoninumbrid %s serverisse Sinu kontaktid on vaid sinu teada. Kui tahad nende hulgast leida Matrix\'i kasutajaid, siis me vajame sinu luba nende andmete saatmiseks räsitud kujul isikutuvastusserverisse. @@ -2330,9 +2330,9 @@ Asukoht Jaga asukohta Tulemusi kuvame vaid siis, kui küsitlus on lõppenud - Küsitlus on lõppenud + Suletud valikutega küsitlus Osalejad näevad tulemusi peale oma valiku salvestamist - Ava küsitlus + Avatud valikutega küsitlus Küsitluse tüüp Muuda küsitlust Hääletanuid ei ole @@ -2910,4 +2910,5 @@ Häälsõnumi esitamine ei õnnestu Kuna sa hetkel salvestad ringhäälingukõnet, siis häälsõnumi salvestamine või esitamine ei õnnestu. Selleks palun lõpeta ringhäälingukõne Viga võrguühenduses - salvestamine on peatatud + Kasuta lõimitud koodi vormingut \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-fa/strings.xml b/libraries/ui-strings/src/main/res/values-fa/strings.xml index d498f4a51b..9b1d367dde 100644 --- a/libraries/ui-strings/src/main/res/values-fa/strings.xml +++ b/libraries/ui-strings/src/main/res/values-fa/strings.xml @@ -2919,4 +2919,5 @@ از آن‌جا که در حال ضبط پخشی زنده‌اید، نمی‌توانید پیامی صوتی را آغاز کنید. لطفاً برای آغاز ضبط یک پیام صوتی، پخش زنده‌تان را پایان دهید نمی‌توان پخش صوتی را آغاز کرد خطای اتّصال - ضبط مکث شد + اعمال قالب کد درون‌خط \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-fi/strings.xml b/libraries/ui-strings/src/main/res/values-fi/strings.xml index c1cc5da2c8..66f333845f 100644 --- a/libraries/ui-strings/src/main/res/values-fi/strings.xml +++ b/libraries/ui-strings/src/main/res/values-fi/strings.xml @@ -252,7 +252,7 @@ Saapuva videopuhelu Saapuva puhelu Puhelu käynnissä… - Toinen puoli ei vastannut. + Toinen osapuoli ei vastannut. Huomio ${app_name} tarvitsee käyttöluvan mikrofoniin suorittakseen puheluita. ${app_name} tarvitsee käyttöluvan kameraan ja mikrofoniin suorittakseen videopuheluita. @@ -887,7 +887,7 @@ Jaat sähköpostiosoitteita tai puhelinnumeroita identiteettipalvelimella %1$s. Sinun täytyy yhdistää uudelleen palvelimeen %2$s, jotta voit lopettaa niiden jakamisen. Hyväksy identiteettipalvelimen (%s) käyttöehdot salliaksesi, että sinut voi löytää sähköpostiosoitteen tai puhelinnumeron perusteella. Yhteyden katkaiseminen identiteettipalvelimeesi tarkoittaa, että muut käyttäjät eivät voi etsiä sinua etkä voi kutsua muita sähköpostin tai puhelinnumeron perusteella. - Lähetimme sinulle vahvistussähköpostin osoitteeseen %s, tarkista sähköpostisi ja klikkaa vahvistuslinkkiä + Lähetimme sinulle sähköpostia osoitteeseen %s. Tarkista sähköpostisi ja klikkaa vahvistuslinkkiä. Ota yksityiskohtaiset lokit käyttöön. Yritä uudelleen, kun olet hyväksynyt kotipalvelimesi käyttöehdot. Palvelimen vastaus näyttäisi olevan liian hidas. Tämä voi johtua kehnosta yhteydestä tai palvelimella olevasta ongelmasta. Yritä hetken kuluttua uudelleen. @@ -1026,7 +1026,7 @@ Kirjaudu sisään palvelimeen %1$s Rekisteröidy Kirjaudu sisään - Jatka kertakirjautumiseen + Jatka kertakirjautumisella Element Matrix Services in osoite Osoite Korkealuokkaista isännöintiä organisaatioille @@ -1097,8 +1097,8 @@ Syöttämäsi koodi ei ole kelvollinen. Tarkista se. Vanhentunut kotipalvelin - Liian monta pyyntöä lähetettiin. Voit yrittää uudelleen 1 sekunnissa… - Liian monta pyyntöä lähetettiin. Voit yrittää uudelleen %1$d sekunnissa… + Liian monta pyyntöä lähetettiin. Voit yrittää uudelleen sekunnin kuluttua… + Liian monta pyyntöä lähetettiin. Voit yrittää uudelleen %1$d sekunnin kuluttua… Nähneet Olet kirjautunut ulos @@ -2068,7 +2068,7 @@ Onnittelut! Personoi profiili ohittaa tämän kysymyksen - Ei varmuutta vielä\? Voit %s + Etkö ole vielä varma\? Voit %s Identiteettipalvelin ei tarjoa käytäntöä Piilota identiteettipalvelimen käytäntö Näytä identiteettipalvelimen käytäntö @@ -2307,4 +2307,54 @@ %1$d valittu %1$d valittu - + Puskuroidaan… + Ääniviestiä ei voi aloittaa + Tässä huoneessa on käytössä huoneversio %s, jonka tämä kotipalvelin on merkinnyt epävakaaksi. + Älä poistu mistään + Poistu kaikista + Poista profiilikuva + Vaihda profiilikuva + Puhelinnumeron haussa tapahtui virhe + + Kutsut lähetetty käyttäjälle %1$s ja yhdelle muulle + Kutsut lähetetty käyttäjälle %1$s ja %2$d muulle + + Kutsu lähetetty käyttäjille %1$s ja %2$s + Kutsu lähetetty käyttäjälle %1$s + Kutsu %s keskusteluun lähettämällä ensimmäinen viesti + Tästä alkaa yksityisviestihistoriasi sinun ja käyttäjän %s välillä. + %s alkaa tästä. + Salaus on säädetty väärin + Salaus on säädetty väärin. + Tällä kotipalvelimella on vanha versio. Pyydä kotipalvelimesi ylläpitäjää päivittämään se. Voit jatkaa, mutta jotkin ominaisuudet eivät välttämättä toimi oikein. + Ota yhteyttä + Jatka %s-kirjautumisella + tai + Keskustelujesi koti + Keskustelujesi koti + Laitetaan yhteydet kuntoon + Kenen kanssa juttelet eniten\? + ${app_name} toimii mainiosti työpaikallakin. Siihen luottavat maailman turvallisimmat organisaatiot. + Poistutaanko nykyisestä ryhmäpuhelusta ja vaihdetaan toiseen\? + Tämä palvelin on jo luettelossa + Tätä palvelinta tai sen huoneluetteloa ei löydy + Kuka vain voi koputtaa huoneeseen ja jäsenet voivat sen jälkeen hyväksyä tai hylätä + Poista osoitteen \"%1$s\" julkaiseminen\? + Huomaa, että maininnat ja avainsanailmoitukset eivät ole käytössä salausta käyttävissä huoneissa mobiililaitteilla. + Ota suora jako käyttöön + Toista aikajanalla olevat animoidut kuvat heti, kun ne näkyvät + Toista animoidut kuvat automaattisesti + Et saa ilmoituksia maininnoista ja avainsanoista salausta käyttävissä huoneissa mobiililaitteilla. + Huonepäivitykset + Botin lähettämät viestit + ${app_name} tarvitsee luvan ilmoitusten näyttämiseen. +\nAnna lupa. + Päivitä huone + Ota lykätyt yksityisviestit käyttöön + Poista valinta kaikista + Valitse kaikki + Anna mikrofonin käyttöoikeus ääniviestien lähettämiseksi. + Anna kameran käyttöoikeus järjestelmän asetuksista tämän toiminnon suorittamiseksi. + Tämän toiminnon suorittaminen vaatii enemmän oikeuksia. Anna oikeudet järjestelmän asetuksista. + Kuunnellaan ilmoituksia + \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-fr/strings.xml b/libraries/ui-strings/src/main/res/values-fr/strings.xml index d62d208e43..491a2660c4 100644 --- a/libraries/ui-strings/src/main/res/values-fr/strings.xml +++ b/libraries/ui-strings/src/main/res/values-fr/strings.xml @@ -2919,4 +2919,5 @@ Vous ne pouvez pas commencer un message vocal car vous êtes en train d’enregistrer une diffusion en direct. Veuillez terminer cette diffusion pour commencer un message vocal Impossible de démarrer un message vocal Erreur de connexion – Enregistrement en pause + Appliquer le formatage de code en ligne \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-hu/strings.xml b/libraries/ui-strings/src/main/res/values-hu/strings.xml index c265b79969..a44bc9b78b 100644 --- a/libraries/ui-strings/src/main/res/values-hu/strings.xml +++ b/libraries/ui-strings/src/main/res/values-hu/strings.xml @@ -2919,4 +2919,5 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze Nem lehet hang üzenetet indítani élő közvetítés felvétele közben. Az élő közvetítés bejezése szükséges a hang üzenet indításához Hang üzenetet nem lehet elindítani Kapcsolódási hiba – Felvétel szüneteltetve + Beágyazott kód formátum alkalmazása \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-in/strings.xml b/libraries/ui-strings/src/main/res/values-in/strings.xml index 8a05481fd5..ca871db81b 100644 --- a/libraries/ui-strings/src/main/res/values-in/strings.xml +++ b/libraries/ui-strings/src/main/res/values-in/strings.xml @@ -2861,4 +2861,5 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Kesalahan koneksi - Perekaman dijeda Anda tidak dapat memulai sebuah pesan suara karena Anda saat ini merekam sebuah siaran langsung. Silakan mengakhiri siaran langsung Anda untuk memulai merekam sebuah pesan suara Tidak dapat memulai pesan suara + Terapkan format kode dalam baris \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-is/strings.xml b/libraries/ui-strings/src/main/res/values-is/strings.xml index ba505bc0a3..8af9da3c9d 100644 --- a/libraries/ui-strings/src/main/res/values-is/strings.xml +++ b/libraries/ui-strings/src/main/res/values-is/strings.xml @@ -2382,4 +2382,44 @@ Náði því Þú endaðir talútsendingu. %1$s endaði talútsendingu. - + Víxla heilskjásham af/á + Víxla punktalista af/á + Víxla tölusettum lista af/á + Setja tengil + Virkja undirstrikun + Virkja yfirstrikun + Virkja skáletrað snið + Virkja feitletrað snið + Óstaðfest · Núverandi setan þín + Óstaðfest - Síðasta virkni %1$s + Staðfest - Síðasta virkni %1$s + Núverandi gátt: %s + Finn ekki endapunktinn. + Núverandi endapunktur: %s + Endapunktur + Tiltækar aðferðir + Ertu viss um að þú viljir stöðva þessa beinu útsendingu\? Þetta mun stöðva útsendinguna og full skráning hennar verður tiltæk á spjallrásinni. + Stöðva beina útsendingu\? + Villa í tengingu - Upptaka í bið + Tekst ekki að spila þessa talútsendingu. + Get ekki byrjað nýja talútsendingu + setja talútsendingu í bið + Spila eða halda áfram með talútsendingu + Stöðva upptöku á talútsendingu + Setja upptöku á talútsendingu í bið + Halda áfram með upptöku á talútsendingu + Nafnlaust lyklaborð + Tilgreindu ástæðu + Takmörk netþjóns á innsendingum skráa + Takmörk fyrir greiningu + Það eru engar skrár í þessari spjallrás + Útbúa nýtt samtal eða spjallrás + Staðfestingarkóðinn er ekki réttur. + Uppgötvanleg símanúmer + Umsögn um beta-útgáfu spjallþráða + Innifelur breytingar á auðkennismynd og birtingarnafni. + Birta atburði notandaaðgangs + Virkja beina deilingu + Beta-útgáfa spjallþráða + Beta-útgáfa spjallþráða + \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-it/strings.xml b/libraries/ui-strings/src/main/res/values-it/strings.xml index d8c81974b2..80a0b7045f 100644 --- a/libraries/ui-strings/src/main/res/values-it/strings.xml +++ b/libraries/ui-strings/src/main/res/values-it/strings.xml @@ -2909,4 +2909,6 @@ Non puoi iniziare un messaggio vocale perché stai registrando una trasmissione in diretta. Termina la trasmissione per potere iniziare un messaggio vocale Impossibile iniziare il messaggio vocale - + Applica formato codice interlinea + Errore di connessione - Registrazione in pausa + \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-ja/strings.xml b/libraries/ui-strings/src/main/res/values-ja/strings.xml index 0b987ce683..f51f2ba81a 100644 --- a/libraries/ui-strings/src/main/res/values-ja/strings.xml +++ b/libraries/ui-strings/src/main/res/values-ja/strings.xml @@ -1,10 +1,10 @@ - + %sの招待 %1$sが%2$sを招待しました %1$sがあなたを招待しました - %1$sが参加しました - %1$sが退出しました + %1$sがルームに参加しました + %1$sがルームから退出しました %1$sが招待を拒否しました %1$sが%2$sを追放しました %1$sが%2$sのブロックを解除しました @@ -14,27 +14,27 @@ %1$sが表示名を%2$sに設定しました %1$sが表示名を%2$sから%3$sに変更しました %1$sが表示名(%2$s)を削除しました - %1$sがテーマを%2$sに変更しました + %1$sがトピックを%2$sに変更しました %1$sがルーム名を%2$sに変更しました - %sがビデオ通話を開始しました。 - %sが音声通話を開始しました。 + %sがビデオ通話を発信しました。 + %sが音声通話を発信しました。 %sが電話に出ました。 %sが通話を終了しました。 ルームへの招待 %1$sと%2$s 空のルーム - %1$sが今後のルーム履歴を%2$sに見えるように設定しました。 + %1$sが今後のルームの履歴を「%2$s」閲覧可能に設定しました。 ルームのメンバー全員(招待された時点から) ルームのメンバー全員(参加した時点から) ルームのメンバー全員 全員 (アバターも変更されました) %1$sがルーム名を削除しました - %1$sがルームの説明を削除しました + %1$sがルームのトピックを削除しました %1$sが%2$sにルームへの招待を送りました %1$sが%2$sの招待を受け入れました ** 復号化できません:%s ** - 送信者の端末からこのメッセージの鍵が送信されていません。 + 送信者の端末からこのメッセージ用の鍵が送信されていません。 メッセージを送信できません Matrixエラー メールアドレス @@ -58,8 +58,8 @@ %1$sが参加しました ルームに参加しました %1$sを招待しました - ディスカッションを作成しました - %1$sがディスカッションを作成しました + 会話を作成しました + %1$sが会話を作成しました ルームを作成しました %1$sがルームを作成しました 招待 @@ -72,11 +72,11 @@ 共有 削除 招待 - 全ての発言を既読にする + 全て既読にする すぐに返信 開く 閉じる - クリップボードへコピー + クリップボードにコピーしました 警告 お気に入り メンバー @@ -87,40 +87,40 @@ 会話 不具合を報告 不具合の内容と状況の説明をお願いします。何をしましたか?何が起こるべきでしたか?実際に起こった事象は何でしょうか? - ここに不具合の内容を記述 + ここに不具合の内容を記述してください スクリーンショットの画像を送信 クラッシュ時のログを送信 ログを送信 開発者が問題を診断するために、このクライアントのログがバグレポートと一緒に送信されます。バグレポートは、ログとスクリーンショットを含めて、公開されることはありません。上記の説明文だけを送信したい場合は、以下のチェックを解除してください。 あなたは不満で端末を振っているようです。バグレポートの画面を開きますか? - 前回、アプリケーションは正常に停止しませんでした。クラッシュ報告の画面を開きますか? - 不具合を報告しました - 不具合の報告の送信に失敗しました (%s) + 前回アプリケーションは正常に停止しませんでした。クラッシュ報告の画面を開きますか? + バグレポートを送信しました + バグレポートの送信に失敗しました(%s) ルームに参加 音声通話 ビデオ通話 検索 ファイルを送信 - ユーザー名かパスワードが正しくありません + ユーザー名とパスワードの一方あるいは両方が正しくありません メールアドレスの形式が正しくありません このメールアドレスは既に登録されています。 パスワードを忘れましたか? - 正しいURLを入力して下さい + 正しいURLを入力してください 原寸 - 大き目 - 中程度 - 小さ目 - 通話終了 + + + + 通話が終了しました はい いいえ - 続行する + 続行 最新の未読へ移動 - ルームを退出 - このルームを退出してよろしいですか? + ルームから退出 + このルームから退出してよろしいですか? 招待 - %sさんが文字を入力しています… - %1$sさんと%2$sさんが文字を入力しています… - %1$sさん、%2$sさん他が文字を入力しています… + %sさんが入力しています… + %1$sさんと%2$sさんが入力しています… + %1$s、%2$s他が入力しています… 明るいテーマ 暗いテーマ 黒いテーマ @@ -130,11 +130,11 @@ ソースコードを表示 または 確認 - 送信中 (%s%%) + 送信中(%s%%) 削除 参加 このルームで発言する権限がありません。 - 自分のアイコン画像 + プロフィール画像 表示名 メールアドレスを追加 電話番号を追加 @@ -144,14 +144,14 @@ 1対1のチャットでのメッセージ グループチャットでのメッセージ ルームへ招待されたとき - 通話の呼び出しがあったとき - 自動発言プログラム(Bot)が発言した時 + 通話への招待 + ボットによるメッセージ 端末起動時に開始 - アプリを閉じているときの動作 + バックグラウンド同期 同期のリクエストを失敗とするまでの時間 同期の間隔 - 一時保存を消去 - メディアの一時保存を消去 + キャッシュを消去 + メディアのキャッシュを消去 メディアファイルを保存 ユーザー設定 通知 @@ -160,7 +160,7 @@ 高度な設定 暗号 通知対象 - 端末の電話帳 + 端末の連絡先 端末の電話帳の使用を許可 電話帳の国番号 全てのメッセージにタイムスタンプを表示 @@ -168,29 +168,29 @@ ID(端末固有番号) 公開端末名 公開端末名の更新 - 最後のオンライン日時 + 直近のオンライン日時 %1$s @ %2$s 認証 ログイン中のアカウント 言語を選択 言語 インターフェース - 電子メールを確認して、本文中のURLをクリックしてください。完了したら「続行する」をクリックしてください。 - このメールアドレスは既に使われています。 - あなたのパスワードは更新されました + 電子メールを確認して、本文中のURLをクリックしてください。完了したら「続行」をクリックしてください。 + このメールアドレスは既に使用されています。 + パスワードを更新しました 国を選択 3日 1週間 1ヵ月 永久に - ルームの説明 - ルームの履歴の可視範囲 - ルームの履歴を読める人は\? + トピック + ルームの履歴の表示対象 + 履歴を閲覧できる人は? 誰でも メンバーのみ(この設定を選択した時点から) メンバーのみ(招待を送った時点から) メンバーのみ(参加した時点から) - ブロックされたユーザー + ブロックしたユーザー 高度な設定 このルームのサーバー内識別ID ラボ @@ -199,14 +199,14 @@ メインアドレスとしての設定を解除 セッションID フォントの大きさ - とても小さい - 小さい + 最小 + 標準 - 大きい - より大きい - とても大きい - 巨大 - 発言更新を確認しています + + 巨大 + 極大 + 最大 + イベントを待機しています 復号化されたソースコードを表示 名前変更 音声通話を開始 @@ -218,7 +218,7 @@ サインアウト 送信 このホームサーバーは、あなたがロボットではないことの確認を求めています - メールアドレスの認証に失敗しました:電子メールのリンクをクリックしたことを確認してください + メールアドレスの認証に失敗しました。電子メール内のリンクを開いたことを確認してください 不正な形式のJSON 有効なJSONを含んでいませんでした ログイン要求が多すぎます @@ -230,8 +230,8 @@ ダイレクトメッセージ ブロック ブロックを解除 - この参加者の発言を全て非表示 - このメンバーの発言を全て表示 + 無視 + 無視を解除 メンション ログアウト 無視 @@ -239,7 +239,7 @@ 結果がありません 利用規約 著作権 - 個人情報保護方針 + プライバシーポリシー ホームサーバー IDサーバー この電話番号は既に使用されています。 @@ -247,15 +247,13 @@ 現在のパスワード 新しいパスワード パスワードの更新に失敗しました - %sの全てのメッセージを表示しますか? -\n -\nこの操作はアプリを再起動するため、時間がかかる場合があります。 - 外観 + %sの全てのメッセージを表示しますか? + テーマ 公開端末名 ルームのエンドツーエンド暗号鍵をエクスポート 認証済 このルームに参加していません。 - このルームで権限がありません。 + このルームでそれを行う権限がありません。 ルーム %s は閲覧できません。 ユーザー名 ホームサーバーのURL @@ -263,13 +261,13 @@ Matrixアプリを追加 権限の数値は正の整数で入力してください。 Matrixの連絡先のみ - 通信先が通話の受取に失敗しました。 + 相手が電話に出られませんでした。 情報 - ${app_name}は、音声通話を実行するためにマイクへアクセスするための許可を必要としています。 - ${app_name}はビデオ通話を行うためにカメラとマイクにアクセスする許可を必要としています。 + ${app_name}は、音声通話を実行するためにマイクにアクセスする権限を必要としています。 + ${app_name}は、ビデオ通話を行うためにカメラとマイクにアクセスする権限を必要としています。 \n \n通話をするためには、次のポップアップでアクセスを許可してください。 - 発言を通報 + コンテンツを報告 写真を撮影 動画を撮影 認証を開始 @@ -277,10 +275,10 @@ リクエストにroom_idがありません。 リクエストの送信に失敗しました。 ウィジェットを作成できません。 - ウィジェットをこのルームから削除してもよろしいですか? - 一致していない場合は、コミュニケーションのセキュリティーが破られている可能性があります。 + ウィジェットをこのルームから削除してよろしいですか? + 一致していない場合は、コミュニケーションのセキュリティーが損なわれている可能性があります。 このセッションでは、未認証のセッションに対して暗号化されたメッセージを送信しない。 - 認証済のセッションに対してのみ暗号化 + 認証済のセッションにのみ暗号化 インポート ローカルファイルから鍵をインポート ルームの暗号鍵をインポート @@ -291,45 +289,45 @@ 鍵をローカルファイルにエクスポート ルームの暗号鍵をエクスポート 通話 - 通知あり(音量大) - 通知あり(サイレント) - 不具合の報告 - このユーザーにあなたと同じ権限を与えます。この変更は取り消せません。 + 通知(音量大) + 通知(サイレント) + バグレポート + このユーザーにあなたと同じ権限レベルを与えようとしています。この変更は取り消せません。 \nよろしいですか? - 信用する - 信用しない + 信頼する + 信頼しない フィンガープリント(%s): リモートサーバーのIDを認証できませんでした。 - 誰かが不当にあなたの通信を傍受しているか、あなたの電話がリモートサーバーの証明書を信用していない可能性があります。 - サーバーの管理者が、これは想定されていることであると言っているのであれば、以下のフィンガープリントが、管理者によるフィンガープリントと一致していることを確認してください。 + これは、誰かがあなたのトラフィックを傍受しているか、あなたの電話機がリモートサーバーから提供された証明書を信頼していないことを意味している可能性があります。 + サーバーの管理者が、これは想定されていることであると述べた場合は、以下のフィンガープリントが、管理者によるフィンガープリントと一致することを確認してください。 証明書はあなたの電話により信頼されていたものから変更されています。これはきわめて異常な事態です。この新しい証明書を承認しないことを強く推奨します。 - 証明書は以前信頼されていたものから信頼されていないものへと変更されています。サーバーがその証明書を更新した可能性があります。サーバーの管理者に連絡して、適切なフィンガープリントを確認してください。 - サーバーの管理者が上のフィンガープリントと一致するものを発行した場合に限り、証明書を承認してください。 + 証明書が以前に信頼されたものから信頼されていないものに変更されました。サーバーが証明書を更新した可能性があります。サーバーの管理者に連絡して、適切なフィンガープリントを確認してください。 + サーバーの管理者が上記のものと一致するフィンガープリントを発行した場合にのみ、証明書を承認してください。 検索 - このアプリの情報をシステム設定で表示。 + このアプリケーションの情報をシステム設定で表示。 アプリの情報 自分の表示名を含むメッセージ 自分のユーザー名を含むメッセージ バージョン olmのバージョン - サードパーティーの使用に関する掲示 + 外部ライブラリーのライセンス ホーム画面 - 逃した通知があるルームを固定 - 未読のあるルームを固定 + 逃した通知があるルームをピン止め + 未読メッセージがあるルームをピン止め 分析 復号エラー セッションキー 未認証 - 認証する - 他のセッションのユーザー設定で、以下を比較して確認してください: - ルームのディレクトリを選択 + 認証 + 他のセッションのユーザー設定で、以下を比較して承認してください: + ルームのディレクトリーを選択 サーバー名 %sサーバー上の全てのルーム 全てのローカルの%sルーム 端末のカメラを使用 コマンドエラー 認識されないコマンド:%s - + オフ 音量大 暗号化されたメッセージ 読み込んでいます… @@ -339,8 +337,8 @@ 全てのメッセージ ホーム画面にショートカットを作成 インラインURLプレビュー - 暗号鍵を要求している新しいセッション \'%s\' を追加しました。 - 未認証のセッション \'%s\' が暗号鍵を要求しています。 + 暗号鍵を要求している新しいセッション\'%s\'を追加しました。 + 未認証のセッション\'%s\'が暗号鍵を要求しています。 作成 ホーム ルーム @@ -354,31 +352,31 @@ メンバー - %d名のメンバー + %d人のメンバー %d件の新しいメッセージ アバター - スタンプを送る + ステッカーを送信 ダウンロード システムアラート - 可能であれば、英語で説明文を記述してください。 + 可能であれば、英語で詳細を記述してください。 音声を送信 - スタンプを送信 - 現在、有効なステッカーパックがありません。 + ステッカーを送信 + 現在、ステッカーパックが有効になっていません。 \n \nいくつか追加しますか? - 申し訳ありません、この操作を完了するための外部アプリが見つかりません。 - あなたの他のセッションに暗号鍵を再要求する。 + 申し訳ありません。この操作を完了するための外部アプリケーションが見つかりません。 + あなたの他のセッションに暗号鍵を再要求。 鍵をこのセッションに送信できるように、メッセージを復号化できる他の端末で${app_name}を起動してください。 %d個選択済 ユーザーをメンションするとき、バイブレーションで通知 送信の前にメディアをプレビュー - アカウントを停止 - 自分のアカウントを停止 + アカウントを無効化 + 自分のアカウントを無効化 分析データを送信 ${app_name}はアプリを改善するため、匿名の分析データを収集します。 @@ -392,56 +390,56 @@ %d個のウィジェットが使用中 必要な変数が見つかりません。 - 動作を表示 + アクションを表示 指定したIDのユーザーをブロック 指定したIDのユーザーのブロックを解除 ユーザーの権限レベルを規定 指定したIDのユーザーの管理者権限を取り消す - 指定したユーザーを現在のルームに招待 - 指定されたアドレスのルームに参加 - ルームを退室 - ルームの説明を設定 + 指定したIDのユーザーを現在のルームに招待 + 指定したアドレスのルームに参加 + ルームから退出 + ルームのトピックを設定 指定したIDのユーザーをこのルームから追放 表示するニックネームを変更 Markdown書式の入/切 Matrixアプリの管理を修正するには - %1$sのホームサーバーの使用を継続するには、利用規約を確認し、同意する必要があります。 + %1$sのホームサーバーを引き続き使用するには、利用規約を確認して同意する必要があります。 エラー - 今すぐ確認 - アカウントを停止 - この操作により、あなたのアカウントは永久に使えなくなります。あなたはログインできなくなり、誰も同じユーザーIDを再登録できなくなります。アカウントが参加している全てのルームを退出し、IDサーバーからアカウントの詳細は削除されます。 この操作は取り消せません。 + 確認 + アカウントを無効化 + この操作により、あなたのアカウントは永久に使えなくなります。ログインしたり同じユーザーIDを再登録したりすることはできなくなります。あなたのアカウントは参加している全てのルームから退出し、あなたのIDサーバーからアカウントの詳細が削除されます。<b>この操作は取り消せません。</b> \n -\nアカウントを停止しても、 デフォルトではあなたが送信したメッセージの履歴は消去されません。メッセージの履歴の消去を望む場合は、以下のボックスにチェックを入れてください。 +\nアカウントを無効化しても、<b>デフォルトではあなたが送信したメッセージの履歴は消去されません</b>。メッセージの履歴を消去する場合は、以下のボックスにチェックを入れてください。 \n -\nMatrixのメッセージの見え方は、電子メールと同様のものです。メッセージの履歴を消去すると、あなたが送信したメッセージは、新規または未登録のユーザーに共有されることはありませんが、既にメッセージを取得している登録ユーザーは、今後もそのコピーにアクセスできます。 - アカウントを停止するときに、自分の送信した全てのメッセージの履歴を消去してください(警告: この操作により、今後のユーザーは会話を不完全な形で見ることになります) - アカウントを停止 +\nMatrixのメッセージの見え方は、電子メールと同様のものです。メッセージの履歴を消去すると、あなたがこれまで送信したメッセージは、新規または未登録のユーザーに共有されることはありませんが、既にメッセージを取得している登録ユーザーは、今後もそのコピーにアクセスできます。 + アカウントを無効化する際、全ての送信済のメッセージを消去(警告:今後のユーザーには、不完全な会話が表示されます) + アカウントを無効化 パスワードを入力してください。 このルームは置き換えられており、アクティブではありません。 こちらから継続中の会話を確認 このルームは別の会話の続きです - 以前のメッセージを見るには、ここをクリックしてください - サービス管理者に連絡してください + 以前のメッセージを表示するには、ここをクリックしてください + サービス管理者に連絡 このホームサーバーはリソース制限の1つを超過しているため、 ユーザーがログインできなくなることがあります - このホームサーバーはリソース制限の1つを超過しています。 + このホームサーバーはリソースの上限に達しました。 このホームサーバーは月間アクティブユーザーの上限に達しているため、 ユーザーがログインできなくなることがあります - このホームサーバーは月間アクティブユーザーの上限に達しています。 + このホームサーバーは月間アクティブユーザー数の上限に達しました 。 この制限を上げるには、%sしてください。 - このサービスを使い続けるには、%sしてください。 + このサービスの使用を継続するには、%sしてください。 申し訳ありません、エラーが発生しました エクスポートされた鍵を暗号化するパスフレーズを作成してください。 鍵をインポートするには、同一のパスフレーズを入力する必要があります。 パスフレーズの作成 - パスフレーズが一致していません + パスフレーズが一致しません %1$s:%2$s %d+ 展開 折りたたむ - 承諾 - このホームサーバーの方針を確認し承諾してください: + 同意 + このホームサーバーの運営方針を確認し、同意してください: 通話設定画面 着信に${app_name}の既定の着信音を使用 着信音 - 着信音を選んでください: + 着信音を選んでください: 会話から追放 鍵のバックアップ 鍵のバックアップを使用 @@ -451,7 +449,7 @@ リアルタイム性を重視して最適化 バックグラウンド同期を行わない 入力中通知を送信 - 文字入力中であることを他のメンバーに伝えます。 + 文字入力中であることを他のメンバーに表示。 開封確認メッセージを表示 開封確認メッセージをクリックすると、詳細な一覧を確認できます。 Enterキーでメッセージを送信 @@ -465,11 +463,11 @@ パスワード パスワード 今ここでサインアウトすると、あなたの暗号化されたメッセージは失われてしまいます - 鍵のバックアップは現在処理中です。処理中にサインアウトすると、暗号化されたメッセージにアクセスできなくなります。 + 鍵をバックアップしています。処理中にサインアウトすると、暗号化されたメッセージにアクセスできなくなります。 暗号化されたメッセージにアクセスできなくなることを防ぐため、鍵の安全なバックアップはあなたのセッション全てで有効化してください。 暗号化されたメッセージは不要です 鍵をバックアップしています… - 続行しますか? + よろしいですか? バックアップ サインアウトする前に鍵をバックアップしないと、暗号化されたメッセージにアクセスできなくなります。 暗号鍵の管理 @@ -483,15 +481,15 @@ バージョン アルゴリズム 署名 - 通知に関する問題の解決 - システム設定。 - アカウント設定。 + 通知に関する問題解決 + システムの設定。 + アカウントの設定。 カスタム設定。 - 起動時に実行 + 端末起動時に開始 バックグラウンド制限の確認 編集 返信 - メッセージが削除されました + メッセージを削除しました 削除済のメッセージを表示 削除されたメッセージに関する通知を表示 ユーザーによって削除されたイベント @@ -503,14 +501,14 @@ 作成 名前 公開 - 誰でもこのルームに参加できるようになります + 誰でもこのルームに参加できます 一般 セキュリティーとプライバシー ヘルプと概要 ダイレクトメッセージ (編集済) - 会話を検索… - 全てのメッセージ (音量大) + 会話を絞り込む… + 全てのメッセージ(音量大) 全てのメッセージ メンションのみ ミュート @@ -527,7 +525,7 @@ いったん有効にすると、暗号化を無効にすることはできません。 セキュリティー 詳細を表示 - その他の設定 + その他 管理者としての操作 ルームの設定 通知 @@ -535,17 +533,17 @@ %1$d人の参加者 アップロード - ルームを退出 - ルームから退室しています… + ルームから退出 + ルームから退出しています… 管理者 モデレーター カスタム - 招待者 + 招待中 ユーザー %1$sの管理者 %1$sのモデレーター - %1$sのデフォルトユーザー - %2$sのカスタム (%1$d) + %1$sの既定のユーザー + %2$sのカスタム(%1$d) タイムライン エンドツーエンド暗号化を有効にする… 暗号化を有効にする @@ -553,19 +551,19 @@ クロス署名は有効です \n秘密鍵は端末内にあります。 クロス署名は有効です -\n鍵は信頼されています +\n鍵は信頼されています。 \n秘密鍵は不明です - クロス署名は有効です + クロス署名は有効です。 \n鍵は信頼されていません クロス署名は無効です - 有効なセッション + 使用中のセッション 全てのセッションを表示 - セッションの管理 - このセッションからログアウト + セッションを管理 + このセッションからサインアウト %d件のアクティブなセッション - このログインを認証 + この端末を認証 QRコード はい いいえ @@ -574,18 +572,18 @@ アカウントデータ 削除… 削除の確認 - このイベントを削除してよろしいですか?ルーム名や説明の変更を削除すると、変更が取り消されますのでご注意ください。 - 暗号化は有効です - このルーム内でのメッセージはエンドツーエンド暗号化されます。詳細の確認や認証はユーザーのプロフィールをご確認ください。 + このイベントを削除してよろしいですか?ルーム名やトピックの変更を削除すると、変更が取り消されます。 + 暗号化が有効です + このルーム内でのメッセージはエンドツーエンドで暗号化されます。詳細の確認や認証はユーザーのプロフィールをご確認ください。 暗号化が有効になっていません - 通知設定 + 通知の設定 切断 サインアウトしてよろしいですか? 既読にする コピー 成功 通知 - 破棄 + 取り消す 再生 閉じる スキップ @@ -601,10 +599,10 @@ カメラ ギャラリー ステッカー - スパムメッセージです + スパムです 不適切なメッセージです その他の報告… - コンテンツを報告 + このコンテンツを報告 このコンテンツを報告する理由 報告 ユーザーを無視 @@ -612,38 +610,38 @@ 元の大きさのまま画像を送信 - 自分に電話をかけることはできません + 自分に電話を発信することはできません マークダウン書式 - メッセージ送信前にマークダウン書式を適用します。これにより、アスタリスクを使用して斜体のテキストを表示するなどの高度な書式設定が利用できます。 + メッセージ送信前にマークダウン書式を適用します。アスタリスクを使用して斜字体のテキストを表示するなどの高度な書式設定が利用できます。 音声とビデオ - 国際電話番号形式で入力してください(電話番号の最初に「+」が付きます) + 国際電話番号形式で入力してください(電話番号の最初に「+」を付けてください) メールアドレス あなたのアカウントに追加されたメールアドレスはありません あなたのアカウントに追加された電話番号はありません 電話番号 あなたのMatrixアカウントに登録されたメールアドレスと電話番号を管理 メールアドレスと電話番号 - 有効化 - このセッションで通知が無効化されています。 -\n${app_name} の設定をご確認ください。 - このセッションで通知は有効化されています。 + 有効にする + このセッションで通知が無効になっています。 +\n${app_name}の設定をご確認ください。 + このセッションで通知は有効になっています。 セッションの設定。 - 有効化 - あなたのアカウントで通知が無効化されています。 + 有効にする + あなたのアカウントで通知が無効になっています。 \nアカウント設定をご確認ください。 - あなたのアカウントで通知は有効化されています。 + あなたのアカウントで通知は有効になっています。 設定を開く - システム設定で通知が無効化されています。 -\nシステム設定をご確認ください。 - システム設定で通知は有効化されています。 + システム設定で通知が無効になっています。 +\nシステム設定を確認してください。 + システム設定で通知は有効になっています。 バッテリー最適化 %d秒 - 拡張設定 + 高度な設定 現在の言語 他の利用可能な言語 - メッセージエディタ + メッセージエディター 環境設定 この端末で設定 セキュアバックアップを再設定 @@ -652,8 +650,8 @@ ルームを作成しています… 招待されています %sからの招待 - 概ね完了しました。%sの画面にも同じシールドアイコンが表示されていますか? - 相手ユーザーの端末のコードをスキャンし、相互に安全性を認証 + 概ね完了しました。%sにも同じマークが表示されていますか? + 相手のユーザーの端末のコードをスキャンし、安全に相互を認証 相手のコードをスキャン スキャンできません 拒否 @@ -662,7 +660,7 @@ 意図しない通話を防止 SSLエラー。 SSLエラー:相手のIDが認証されていません。 - このURLからホームサーバーに接続できませんでした、ご確認ください + このURLからホームサーバーに接続できませんでした。URLを確認してください 有効なMatrixサーバーのアドレスではありません この電話番号は既に登録されています。 シングルサインオンを使用してサインイン @@ -677,12 +675,12 @@ 電話 サウンドデバイスを選択 リアルタイム接続を確立できませんでした。 -\nホームサーバーの管理者に、通話が正常に動作するためにTURNを設定するようご連絡ください。 - 終了 +\n安定した通話のために、ホームサーバーの管理者にTURNサーバーの設定を依頼してください。 + 電話を切る 拒否 - 承諾 - ウィジェットを削除できませんでした - ウィジェットを追加できませんでした + 同意 + ウィジェットの削除に失敗しました + ウィジェットの追加に失敗しました ビデオ通話を開始 通話を開始する権限がありません このルームで通話を開始する権限がありません @@ -691,16 +689,16 @@ なし トピック ルーム名 - このルーム内のメッセージはエンドツーエンド暗号化されていません。 - ここでのメッセージはエンドツーエンド暗号化されていません。 + このルームのメッセージはエンドツーエンドで暗号化されていません。 + ここでのメッセージはエンドツーエンドで暗号化されていません。 設定 - あなたにはこのルームの暗号化を有効にする権限がありません。 + このルームの暗号化を有効にする権限がありません。 未読メッセージ タイムラインでのスワイプによる返信を有効にする タイムラインで非表示のイベントを表示 QRコードをスキャン QRコード - QRコードによる追加 + QRコードで追加 コードを共有 ${app_name}で話しましょう:%s 友達を招待 @@ -710,7 +708,7 @@ インテグレーションが無効になっています インテグレーションマネージャー インテグレーションを許可 - FCMトークンが正常に取得されました: + FCMトークンを正常に取得しました: \n%1$s Firebaseトークン Playサービスを修正 @@ -721,17 +719,17 @@ 1つ以上のテストが失敗しました。調査用のバグレポートを送信してください。 1つ以上のテストが失敗しました。提案された修正を試してください。 基本的な診断はOKです。 それでも通知が届かない場合は、調査用のバグレポートを送信してください。 - 実行しています…(%1$dの%2$d) + 実行しています…(%2$d個のうち%1$d個目のテスト) テストを実行 - 診断トラブルシューティング + 問題解決に関する調査結果 イベントごとの通知の優先順位 - メールであなたに送ったリンクをクリックして確認してください。 + メールで送信したリンクをクリックしたことを確認してください。 %sを削除しますか? ブロックされたユーザーを絞り込む - トピックを変更 - ルームをアップグレード - m.room.server_acl eventsを送信 - 権限を変更 + トピックの変更 + ルームのアップグレード + m.room.server_acl eventsの送信 + 権限の変更 ルーム名の変更 履歴の見え方の変更 ルームの暗号化の有効化 @@ -750,7 +748,7 @@ ルームに関する変更を行うために必要な役割を選択 ルームの権限 権限 - ルームに関する変更を行うために必要な役割を表示し更新します。 + ルームに関する変更を行うために必要な役割を表示し更新。 ブロックを解除すると、ユーザーはルームに再び参加できるようになります。 ユーザーをブロック ブロックする理由 @@ -762,48 +760,48 @@ ユーザーを追放 このユーザーの招待をキャンセルしてよろしいですか? 招待をキャンセル - このユーザーを解除すると、そのユーザーからの全てのメッセージが再び表示されます。 - ユーザーを無視しない + このユーザーの無視を解除すると、そのユーザーからの全てのメッセージが再び表示されます。 + ユーザーの無視を解除 このユーザーを無視すると、あなたが共有しているルームからそのユーザーのメッセージが削除されます。 \n \nこの操作は、設定からいつでも元に戻すことができます。 ユーザーを無視 降格 - あなたは自分自身を降格させようとしています。この変更は取り消せません。あなたがルームの中で最後の特権ユーザーである場合、特権を再取得することはできません。 + あなたは自分自身を降格させようとしています。この変更は取り消せません。あなたがルームの中で最後の特権ユーザーである場合、特権を再取得することはできなくなります。 降格しますか? 招待をキャンセル - このルームは公開されていません。 招待がなければ再び参加することはできません。 + このルームは公開されていません。再度参加するには、招待が必要です。 連絡先へのアクセスを許可します。 QRコードをスキャンするには、カメラへのアクセスを許可する必要があります。 - 通話をかけました - %sが通話をかけました + 通話を保留しました + %sが通話を保留しました 保留 - 通話をやり直す + 通話を再開 ビデオ通話が行われています… - 有効な認証情報がないため、権限がありません - ${app_name} 呼び出し失敗 - ルームディレクトリの全てのルームを表示(露骨なコンテンツのあるルームを含む)する。 + 有効な認証情報がないため、許可されていません + ${app_name}が呼び出しに失敗しました + ルームディレクトリーの全てのルームを表示(露骨なコンテンツのあるルームを含む)。 露骨なコンテンツのあるルームを表示 - ルームディレクトリ + ルームディレクトリー 新着情報 - 非公開 + 非公開にする 切り替える 追加 - %1$sがエンドツーエンド暗号化(認識されていないアルゴリズム %2$s)をオンにしました。 - エンドツーエンド暗号化(認識されていないアルゴリズム %1$s)をオンにしました。 - 会話を始める - %1$sがエンドツーエンド暗号化をオンにしました。 - エンドツーエンド暗号化をオンにしました。 - ゲストがルームに参加するのを拒否しました。 - %1$sはゲストがルームに参加するのを拒否しました。 - ゲストがルームに参加するのを拒否しました。 - %1$sはゲストがルームに参加するのを拒否しました。 + %1$sがエンドツーエンド暗号化(認識されていないアルゴリズム %2$s)を有効にしました。 + エンドツーエンド暗号化(認識されていないアルゴリズム %1$s)を有効にしました。 + 会話を開始 + %1$sがエンドツーエンド暗号化を有効にしました。 + エンドツーエンド暗号化を有効にしました。 + ゲストがルームに参加することを拒否しました。 + %1$sはゲストがルームに参加することを拒否しました。 + ゲストがルームに参加することを拒否しました。 + %1$sはゲストがルームに参加することを拒否しました。 ここにゲストが参加することを許可しました。 %1$sはここにゲストが参加することを許可しました。 - ゲストがルームに参加するのを許可しました。 - %1$sはゲストがルームに参加するのを許可しました。 - システムデフォルト - このルームのメインおよび代替のアドレスを変更しました。 + ゲストがルームに参加することを許可しました。 + %1$sはゲストがルームに参加することを許可しました。 + システムの既定 + このルームのメインおよび代替アドレスを変更しました。 このルームの代替アドレスを変更しました。 このルームの代替アドレス %1$s を削除しました。 @@ -851,19 +849,19 @@ %1$sがこのルームのアドレスに%2$sを追加しました。 %sがこのルームのサーバーのアクセス制御リストを変更しました。 - IPリテラルに一致するサーバーは禁止されています。 - ・IPリテラルに一致するサーバーを許可します。 + ・IPリテラルに一致するサーバーはブロックされています。 + ・IPリテラルに一致するサーバーを許可されています。 ・%sに一致するサーバーは許可されています。 - ・%sに一致するサーバーは禁止されています。 + ・%sに一致するサーバーはブロックされています。 %sがこのルームのサーバーアクセス制御リストを設定しました。 %sがここをアップグレードしました。 %sがこのルームをアップグレードしました。 - 今後のメッセージを%1$sに見えるように設定しました。 - 今後のルーム履歴を%1$sに見えるように設定しました。 - %1$sが今後のメッセージを%2$sに見えるように設定しました。 + 今後のメッセージを「%1$s」閲覧可能に設定しました。 + 今後のルームの履歴を「%1$s」閲覧可能に設定しました。 + %1$sが今後のメッセージを「%2$s」閲覧可能に設定しました。 %sが通話を設定するためにデータを送信しました。 - 通話を開始しました。 - ビデオ通話を開始しました。 + 音声通話を発信しました。 + ビデオ通話を発信しました。 %1$sをブロックしました。理由:%2$s %1$sが%2$sをブロックしました。理由:%3$s %1$sのブロックを解除しました。理由:%2$s @@ -874,7 +872,7 @@ %1$sが招待を拒否しました。理由:%2$s 退出しました。理由:%1$s %1$sが退出しました。理由:%2$s - このルームを退出しました。理由:%1$s + このルームから退出しました。理由:%1$s 初期同期: \n退出したルームをインポートしています 初期同期: @@ -882,9 +880,9 @@ 初期同期: \n会話を読み込んでいます \n多くのルームに参加している場合、読み込みに時間がかかるかもしれません - %1$sがこのルームを退出しました。理由:%2$s - このルームに参加しました。理由:%1$s - %1$sがこのルームに参加しました。理由:%2$s + %1$sがこのルームから退出しました。理由:%2$s + 参加しました。理由:%1$s + %1$sが参加しました。理由:%2$s このルームに参加しました。理由:%1$s %1$sがこのルームに参加しました。理由:%2$s %1$sがあなたを招待しました。 理由:%2$s @@ -892,8 +890,8 @@ %1$sが%2$sを招待しました。 理由:%3$s あなたの招待です。理由:%1$s %1$sの招待です。理由:%2$s - メッセージを送っています… - メッセージを送りました + メッセージを送信しています… + メッセージを送信しました 初期同期: \nアカウントデータをインポートしています 初期同期: @@ -912,10 +910,10 @@ %1$s、%2$s、%3$sと%4$s %1$s、%2$sと%3$s - %1$sの権限レベルを%2$sから%3$sへ変更しました。 + %1$sの権限レベルを%2$sから%3$sへ カスタム カスタム (%1$d) - デフォルト + 既定 モデレーター 管理者 %1$sウィジェットを変更しました @@ -935,12 +933,12 @@ %1$sにルームへの招待を送りました ルームのアバターを削除しました %1$sがルームのアバターを削除しました - ルームの説明を削除しました + ルームのトピックを削除しました ルーム名を削除しました - ディスカバリー設定を管理します。 + ディスカバリーの設定を管理。 ディスカバリー(発見) これにより、現在のキーまたはフレーズが置き換えられます。 - 新しいセキュリティーキーを生成するか、既存のバックアップに新しいセキュリティーフレーズを設定します。 + 新しいセキュリティーキーを生成するか、既存のバックアップに新しいセキュリティーフレーズを設定してください。 サーバー上の暗号鍵をバックアップして、暗号化されたメッセージとデータへのアクセスが失われるのを防ぎましょう。 メッセージ作成画面に絵文字キーボードを開くためのボタンを追加 絵文字キーボードを表示 @@ -948,17 +946,17 @@ アカウントのイベントを表示 招待、追放、ブロックは影響を受けません。 参加・退出イベントを表示 - /confettiコマンドを使用するか、❄️または🎉を含むメッセージを送信 + /confettiコマンドを使用すると、❄️または🎉を含むメッセージを送信 チャットでエフェクトを表示 ホームサーバーがこの機能をサポートしている場合は、チャット内のリンクをプレビューします。 - ボット、ブリッジ、ウィジェット、ステッカーパックを管理します。 -\nインテグレーションマネージャーは、構成データを受信し、ユーザーに代わってウィジェットの変更や、ルーム招待の送信、権限の設定などを行うことができます。 + インテグレーションマネージャーを使用すると、ボット、ブリッジ、ウィジェット、ステッカーパックを管理できます。 +\n設定データを受信し、ユーザーに代わってウィジェットの変更、ルームへの招待の送信、権限レベルの設定を行うことができます。 インテグレーション(統合) - アプリがバックグラウンドにある場合、着信メッセージは通知されません。 - ${app_name}は正確な時間に定期的にバックグラウンドで同期します(構成可能)。 -\nこれは無線とバッテリーの使用量に影響し、${app_name}がイベントを待機していることを示す永続的な通知が表示されます。 - ${app_name}は、端末の限られたリソース(バッテリーの残量)を維持する方法でバックグラウンド同期をします。 -\n端末の状態によっては、OSによって同期が延期される場合があります。 + アプリがバックグラウンドにある場合、受信するメッセージは通知されません。 + ${app_name}は、正確な時間(設定可能)に定期的にバックグラウンドで同期します。 +\nこれは無線とバッテリーの使用量に影響します。また、${app_name}がイベントを待機していることを示す永続的な通知が表示されます。 + ${app_name}は、端末の限られたリソース(バッテリーの残量)を維持する方法でバックグラウンド同期を行います。 +\nバッテリーの状態によっては、OSによって同期が延期される場合があります。 LEDの色、振動、音を選択してください… 通知(サイレント)を設定 通話の通知を設定 @@ -975,17 +973,17 @@ \nこのエラーは${app_name}の管理外です。 これはいくつかの理由で発生する可能性があります。 後で再試行するとうまくいくかもしれません。システム設定でGoogle Playサービスのデータ使用量が制限されていないか、端末の時刻が正しいかどうかを確認してください。カスタムROMで生じることもあります。 ${app_name}はバッテリー最適化の影響を受けません。 制限を無効にする - 起動時の開始を有効にする + 端末起動時の開始を有効にする 端末を再起動するとサービスが開始します。 通知がクリックされました! 通知をクリックしてください。 通知が表示されない場合は、システム設定を確認してください。 - 通知を表示 + 通知の表示 通知を表示しています。 クリックしてください! プッシュ通知の受信に失敗しました。 アプリケーションを再インストールすると解決するかもしれません。 アプリケーションはプッシュ通知を受信しています アプリケーションはプッシュ通知を待機しています プッシュ通知のテスト - FCMトークンのホームサーバーへの登録に失敗しました: + FCMトークンのホームサーバーへの登録に失敗しました: \n%1$s FCMトークンがホームサーバーに登録されました。 トークンの登録 @@ -996,25 +994,25 @@ \nこのエラーは${app_name}の管理外です。Googleによると、このエラーは、FCMに登録されている端末上のアプリの数が多すぎることを示唆しています。 このエラーは、アプリの数が極端に多い場合にのみ発生するため、平均的なユーザーには影響しません。 ${app_name}はGoogle Playサービスを使用してプッシュメッセージを配信していますが、正しく設定されていないようです: \n%1$s - FCMトークンの取得に失敗しました: + FCMトークンの取得に失敗しました: \n%1$s 🎉全てのサーバーの参加がブロックされています!このルームは使用できなくなりました。 変更はありません。 - • サーバーにマッチするIPリテラルが禁止されています。 - • サーバーにマッチするIPリテラルが許可されるようになりました。 + • IPリテラルに一致するサーバーが禁止されるようになりました。 + • IPリテラルに一致するサーバーが許可されるようになりました。 • %sに一致するサーバーが許可リストから削除されました。 • %sに一致するサーバーが許可されるようになりました。 - • %sに一致するサーバーが禁止リストから削除されました。 - • %sに一致するサーバーは禁止されています。 + • %sに一致するサーバーがブロックリストから削除されました。 + • %sに一致するサーバーはブロックされています。 - %1$s、%2$s、他%3$d人のユーザーが読みました + %1$s、%2$s、他%3$d人のユーザーが閲覧済 - %1$s、%2$s、%3$sが読みました - メッセージをマークダウンとして解釈せずにプレーンテキストとして送信 + %1$s、%2$s、%3$sが閲覧済 + メッセージをマークダウンとして解釈せず、プレーンテキストとして送信 ファイルとして保存 共有 完了 - 成功! + 成功しました! バックアップを作成しています パスフレーズを設定 手動で鍵をエクスポート @@ -1025,7 +1023,7 @@ ユーザー名を入力してください。 無視 共有 - 続行するには利用規約を承認する必要があります。 + 続行するには、このサービスの利用規約に同意する必要があります。 全てブロック 許可 ルームID @@ -1043,7 +1041,7 @@ 新しいイベント 不明なIP - %2$d個の鍵のうち%1$d個のインポートに成功。 + %2$d個の鍵のうち%1$d個をインポートしました。 鍵のバックアップを管理 鍵のエクスポートに成功しました @@ -1062,8 +1060,8 @@ ルームを追加 %sはあなたを招待しています このルームでグループ通話を開始する権限がありません - オーディオミーティングを開始 - 安全バックアップを設定 + 音声通話を開始 + セキュアバックアップを設定 鍵のバックアップで管理 鍵のバックアップを使用 暗号化されたメッセージとデータへのアクセスが失われるのを防ぎましょう @@ -1072,11 +1070,11 @@ バックアップの状態を確認しています バックアップを削除しています… このセッションで鍵のバックアップを使用するには、パスフレーズまたはリカバリーキーでバックアップを復元してください。 - バックアップには未認証のセッション %s による不正な署名があります - バックアップには認証済のセッション %s による不正な署名があります - バックアップには未認証のセッション %s による有効な署名があります + バックアップには未認証のセッション %s による不正な署名があります。 + バックアップには認証済のセッション %s による不正な署名があります。 + バックアップには未認証のセッション %s による有効な署名があります。 バックアップには認証済のセッション %s による署名があります。 - バックアップはこのセッションによる有効な署名があります。 + バックアップにはこのセッションによる有効な署名があります。 バックアップには%sというIDの不明のセッションによる署名があります。 このセッションでは鍵がバックアップされていません。 このセッションでは鍵のバックアップが無効になっています。 @@ -1088,7 +1086,7 @@ %d個のキーが含まれたバックアップを復元しました。 - バックアップが復元されました %s! + バックアップを復元しました %s! このリカバリーキーではバックアップを復号化できませんでした。正しいリカバリーキーを入力したことを確認してください。 リカバリーキーを入力してください 履歴のロックを解除 @@ -1097,27 +1095,27 @@ リカバリーキーを計算しています… バックアップを復元しています: このパスフレーズではバックアップを復号化できませんでした。正しい復旧用のパスフレーズを入力したことを確認してください。 - リカバリーキーを喪失しましたか? 設定で新しいリカバリーキーを設定できます。 + リカバリーキーを無くしましたか? 設定で新しいリカバリーキーを設定できます。 バックアップのバージョンを取得しています… 暗号化されたメッセージ履歴のロックを解除するには、復旧用のパスフレーズを使用してください 復旧用のパスフレーズが分からなければ、%sできます。 - リカバリーキーを使用して、暗号化されたメッセージの履歴のロックを解除 + リカバリーキーを使うと、暗号化されたメッセージの履歴のロックを解除できます リカバリーキーを入力 リカバリーキーを使用 ログアウトしたりこの端末を失くしたりすると、メッセージにアクセスできなくなる可能性があります。 - 続行しますか? + よろしいですか? 予期しないエラー リカバリーキー パスフレーズを使用してリカバリーキーを生成中です。数秒かかることがあります。 リカバリーキーを共有… - コピーをしてください + コピーしてください 中止 上書き 別のセッションで鍵のバックアップを既に設定しているようです。上書きしますか? ホームサーバーにバックアップが存在しています リカバリーキーが保存されました。 リカバリーキーを保存 - コピーをしました + コピーしました リカバリーキーはパスワードマネージャー(もしくは金庫)のような、非常に安全な場所で保管してください リカバリーキーはセーフティーネットとなります。パスフレーズを忘れた場合でも、リカバリーキーを使えば、暗号化されたメッセージにアクセスすることができます。 \nリカバリーキーは、パスワードマネージャー(もしくは金庫)のような、非常に安全な場所で保管してください。 @@ -1127,7 +1125,7 @@ 鍵のコピーを暗号化してホームサーバーに保存します。バックアップを保護するためにパスフレーズを設定してください。 \n \n最大限のセキュリティーを確保するために、アカウントのパスワードと異なるものに設定することが大切です。 - パスフレーズを使用してバックアップを保護します。 + パスフレーズを使用してバックアップを保護しましょう。 暗号化されたメッセージは、エンドツーエンドの暗号化によって保護されています。これらの暗号化されたメッセージを読むための鍵を持っているのは、あなたと受信者だけです。 \n \n鍵を失くさないよう、鍵を安全にバックアップしてください。 @@ -1136,36 +1134,36 @@ ${app_name}によるリカバリーキーの生成を望む場合、パスフレーズを削除してください。 マークダウンが無効です。 マークダウンが有効です。 - ”%s”とのコマンドはいくつかのパラメータが欠けているか不正です。 - 新しいセッションが暗号鍵を要請しています。 + コマンド\"%s\"はいくつかのパラメーターを欠いているか、パラメーターが正しくありません。 + 新しいセッションが暗号鍵を要求しています。 \nセッション名:%1$s -\n最後のオンライン日時:%2$s -\n新たにログインして新しいセッションを開始しなかった場合、この要求を無視してください。 +\n直近のオンライン日時:%2$s +\n新しいセッションにログインしなかった場合、この要求を無視してください。 未認証のセッションが暗号鍵を要求しています。 \nセッション名:%1$s -\n最後のオンライン日時:%2$s -\n新しいセッションにログインしていない場合、この要求を無視してください。 +\n直近のオンライン日時:%2$s +\n新しいセッションにログインしなかった場合、この要求を無視してください。 鍵の共有リクエスト - カスタムカメラ画面の代わりにシステムカメラを使用します。 + カスタムカメラ画面の代わりにシステムカメラを開始。 使用中のウィジェットがありません インテグレーションを管理 - DRM保護されているメディアを読み込む + DRMで保護されているメディアを読み込む マイクの使用 カメラの使用 - このウィジェットは次のリソースの使用を要求します: + このウィジェットは次のリソースの使用を要求しています: 現在の会議から退出し、もう一つの会議に参加しますか? - 申し訳ありませんが、ビデオ会議に参加する途中で問題が発生しました - 申し訳ありませんが、古い端末(Android OS 6.0以前)はJitsiを使用したビデオ会議をサポートしていません + 申し訳ありませんが、グループ通話に参加する際に問題が発生しました + 申し訳ありませんが、古い端末(Android OS 6.0以前)はJitsiを使用したグループ通話をサポートしていません あなたの表示名 ウィジェットの読み込みに失敗しました。 \n%s ウィジェットを再読み込み これを使用すると、クッキーが設定され、データが%sと共有される可能性があります: - ウィジェットの追加者: - **送信に失敗 - ルームを開いてください + ウィジェットを追加した人: + **送信に失敗しました - ルームを開いてください 新しい招待 %1$sと%2$s - %1$sに%2$sと%3$s + %2$sと%3$sで%1$s %d件の通知 @@ -1177,39 +1175,39 @@ 既に一覧に載っているサーバーです サーバーまたはそのルーム一覧が見つかりません - 探索したい新しいサーバーの名前を入力してください。 + 探したい新しいサーバーの名前を入力してください。 新しいサーバーを追加 あなたのサーバー 暗号化されたメッセージの復元 ルームのバージョン - ブロックされたユーザー%d人 + %d人のブロックされたユーザー このルームのあるスペースのメンバーは、誰でもこのルームを発見し参加できます。ルームをスペースに追加できるのは、ルームの管理者だけです。 スペースのメンバーのみ 誰でもルームを発見し参加できます 公開 - 招待された人だけが発見し参加できます + 招待した人のみが検索・参加できます 非公開 不明のアクセス設定(%s) 誰でもルームにノックができ、メンバーがその参加を承認または拒否できます - 現在のルームディレクトリの見え方を取得できません(%1$s)。 - このルームを%1$sのルームディレクトリに公開しますか? + 現在のルームディレクトリーの見え方を取得できません(%1$s)。 + このルームを%1$sのルームディレクトリーに公開しますか? このアドレスを非公開にする このアドレスを公開 - アドレスを設定すれば、他のユーザーがあなたのホームサーバー (%1$s) を通じてこのルームを見つけられるようになります。 + アドレスを設定すると、他のユーザーがあなたのホームサーバー(%1$s)を通じてこのルームを見つけられるようになります。 ローカルアドレス - 新しい公開アドレス(例: #alias:server) + 新しい公開アドレス(例:#alias:server) 他の公開アドレスはまだありません。以下から追加できます。 他の公開アドレスはまだありません。 - \"%1$s\"を非公開にしますか? + アドレス\"%1$s\"を非公開にしますか? 公開 手動で新しいアドレスを公開 他の公開アドレス: - 公開アドレスを通して、どのサーバーのどのユーザーでも、このルームに参加できます。アドレスを公開するには、まずローカルアドレスとして設定する必要があります。 + 公開アドレスを通して、どのサーバーのユーザーでも、このルームに参加できます。アドレスを公開するには、まずローカルアドレスとして設定する必要があります。 公開アドレス - このルームのアドレスと、ルームディレクトリにおける見え方を管理できます。 - スペースのアドレスを管理できます。 + このルームのアドレスと、ルームディレクトリーにおける見え方を管理。 + このスペースのアドレスを管理。 スペースのアドレス ルームのアドレス ゲストの参加を許可 @@ -1221,27 +1219,27 @@ ホームサーバーAPIのURL アクセスを取り消す 表示 - このルーム内のメッセージはエンドツーエンド暗号化されています。 + このチャットのメッセージはエンドツーエンドで暗号化されています。 ダイレクトメッセージ 新しいダイレクトメッセージを送信 - メールアドレス(任意) - メールアドレス - アカウント復旧用のメールアドレスを設定します。後からオプションで知人に見つけてもらえるようにできます。 + 電子メール(任意) + 電子メール + アカウント復旧用のメールアドレスを設定します。後からこのメールアドレスによって知人に見つけてもらえるようにできます(任意)。 メールアドレスを設定 メールアドレスを確認しました 発見可能なメールアドレス 続行するには利用規約を承認してください ホームサーバーの利用規約を承認したら、再試行してください。 - 次に + 次へ 次へ - 次に - 次に - 次に + 次へ + 次へ + 次へ ユーザー名を選択してください。 - ユーザー名やパスワードが正しくありません。 入力したパスワードは、スペースで開始または終了していますので、ご確認ください。 + ユーザー名やパスワードが正しくありません。入力されたパスワードがスペースで開始または終了しています。確認してください。 そのユーザー名は既に使用されています ユーザー名 - ユーザー名またはメールアドレス + ユーザー名または電子メール %sでサインイン %sでサインアップ %sで続行 @@ -1257,8 +1255,8 @@ カスタムと高度な設定 組織向けのプレミアムホスティング 組織向けのプレミアムホスティング - 最大のパブリックサーバーで、数百万人に無料で参加 - メールと同じように、アカウントには1つのホームがありますが、誰とでも話すことができます + 最大の公開サーバーで、数百万人に無料で参加 + 電子メールと同じように、アカウントには1つのホームがありますが、誰とでも話すことができます サーバーを選択 始めましょう エクスペリエンスを拡張およびカスタマイズ @@ -1268,30 +1266,30 @@ ここが%sとのダイレクトメッセージのスタート地点です。 変更履歴はありません メッセージの変更履歴 - ファイル %1$s をダウンロードしました! + ファイル %1$s をダウンロードしました! ビデオを%d%%圧縮しています 画像を圧縮しています… 暗号化されたルームで完全な履歴を表示 フィードバックを送信 - フィードバックを送信できませんでした (%s) - ありがとうございます、あなたのフィードバックは正常に送信されました + フィードバックの送信に失敗しました(%s) + ありがとうございます。フィードバックを正常に送信しました 追加で確認が必要な事項がある場合は、連絡可 フィードバック 現在「スペース」のベータ版を使用しています。あなたのフィードバックは今後のバージョンに反映されます。ご意見を最大限に参考にさせていただくため、あなたのプラットフォームとユーザー名を記録させていただきます。 スペースについてのフィードバック - 提案の送信に失敗しました(%s) - ありがとうございます、提案は正常に送信されました + 提案の送信に失敗しました(%s) + ありがとうございます。提案を送信しました トークンの登録 - アプリケーションの表示名: - App ID: - Push Key: + アプリケーションの表示名: + App ID: + Push Key: 登録されたプッシュゲートウェイはありません プッシュ通知に関するルールが定義されていません プッシュ通知に関するルール - あなたは既にこのルームを見ています! - その他のサードパーティーの使用に関する掲示 + 既にこのルームを表示しています! + その他の外部ライブラリーのライセンス Matrix SDKのバージョン - ファイル\"%1$s\"からエンドツーエンド暗号鍵をインポートします。 + ファイル\"%1$s\"からエンドツーエンド暗号鍵をインポート。 鍵のバックアップデータの取得中にエラーが発生しました 信頼情報の取得中にエラーが発生しました ルームが作成されましたが、一部の招待が送信されていません。理由: @@ -1299,36 +1297,36 @@ \n%s ルームの設定 トピック - ルームの説明(任意) + ルームのトピック(任意) ルーム名 このルームはプレビューできません。参加しますか? 現在、このルームにはアクセスできません。 \n後でもう一度やり直すか、ルームの管理者にアクセス権があるかどうかを確認するよう依頼してください。 このルームはプレビューできません - お待ち下さい… + お待ちください… ネットワークがありません。インターネット接続を確認してください。 不正な形式のイベントです。表示できません ルームの管理者によってモデレートされたイベント リアクション - リアクションを見る + リアクションを表示 リアクションを追加 同意 リアクション - ルームがここに表示されます。右下の[+]をタップして、既存のルームを検索するか、独自のルームを開始します。 - ダイレクトメッセージの会話がここに表示されます。右下の[+]をタップして開始します。 + ルームがここに表示されます。右下の+をタップすると、既存のルームを検索するか、自分のルームを開始できます。 + ダイレクトメッセージの会話がここに表示されます。右下の+をタップして開始します。 ルーム 会話 未読メッセージはありません 未読はありません! %sがセッションの認証を要求しています - リトライ - 他のホームサーバーに接続しようとしているようですね。サインアウトしますか? + 再試行 + 他のホームサーバーに接続しようとしているようです。サインアウトしますか? IDサーバーを使用していません 不明なエラー このルームを含む参加済のスペース - このルームにアクセスできるスペースを決定します。スペースが選択されると、そのメンバーはルーム名を見つけて参加できます。 + このルームにアクセスできるスペースを選択してください。選択したスペースのメンバーはルーム名を検索し、参加できるようになります。 了解 - 完了しました! + 認証しました! メッセージの新しい鍵 暗号化されたメッセージを決して失わないために セキュアバックアップ @@ -1336,13 +1334,13 @@ %1$s:%2$s %3$s %1$s:%2$s あなたが知らないかもしれない他のスペースやルーム - 誰がこのルームを検索し、参加できるか決める。 + 誰がこのルームを検索・参加できるか選択してください。 タップしスペースを編集 スペースを選択 アクセス可能なスペース スペースのメンバーに発見とアクセスを許可します。 スペース %s のメンバーが検索、プレビュー、参加できます。 - 非公開(招待のみ) + 非公開(招待者のみ参加可能) 既定のメディアソース 既定の圧縮率 ルームのアップグレード @@ -1356,63 +1354,63 @@ ダイレクトメッセージ 自分のユーザー名 自分の表示名 - グループチャットでのメッセージの暗号化 - 個別チャットでのメッセージの暗号化 - 以下がメッセージに含まれる場合に通知 + グループチャットで暗号化されたメッセージ + 1対1のチャットで暗号化されたメッセージ + 以下の場合に通知 その他 メンションとキーワード 通知のデフォルト - %d個の不在着信(ビデオ) + %d件の不在着信(ビデオ) - %d個の不在着信(音声) + %d件の不在着信(音声) - デフォルトで使いもう尋ねない + 既定に設定し、次回から確認しない 鍵の共有リクエストの履歴を送信 結果がありません - 自分に電話をかけることはできません。参加者が招待を受け入れるまでお待ちください - ミーティングはJitsiのセキュリティーとパーミッションポリシーを使用します。会議中は、現在ルームにいる全ての人に招待状が表示されます。 + 自分に電話を発信することはできません。参加者が招待を受け入れるまでお待ちください + ミーティングはJitsiのセキュリティーとパーミッションポリシーを使用します。ミーティング中は、現在ルームにいる全ての人に招待が表示されます。 権限がありません 音声メッセージを送信するには、マイクの権限を許可してください。 この操作を実行するには、システム設定からカメラの権限を許可してください。 この操作を実行するための権限がありません。システム設定から権限を付与してください。 IDサーバーに接続できませんでした - IDサーバーのURLを入力 + IDサーバーのURLを入力してください 同意する - 同意を撤回 + 同意を取り消す あなたの連絡先から他のユーザーを発見するために、メールアドレスや電話番号をこのIDサーバーに送信することに同意しています。 メールと電話番号を送信 - %sにメールを送りました。メールを確認してリンクをクリックしてください - %sにメールを送りました。メールの確認リンクをクリックしてください + %sにメールを送りました。メールを確認して承認リンクをクリックしてください + %sにメールを送りました。メールを確認して承認リンクをクリックしてください 発見可能な電話番号 - IDサーバーとの接続を解除すると、他のユーザーによって発見されなくなり、また、メールアドレスや電話で他のユーザーを招待することができなくなります。 + IDサーバーとの接続を解除すると、他のユーザーによって見つけられなくなり、また、メールアドレスや電話で他のユーザーを招待することもできなくなります。 電話番号を追加すると、発見可能に設定する電話番号を選択できるようになります。 メールアドレスを追加すると、発見可能に設定するメールアドレスを選択できるようになります。 - 現在、IDサーバーを使用していません。あなたの知っている連絡先を発見したり、その連絡先から発見されるようにするには、以下でIDサーバーを設定してください。 - あなたは現在%1$sを使って連絡先を見つけたり、連絡先から見つけられるようにしています。 + 現在、IDサーバーを使用していません。連絡先を見つけたり、連絡先から見つけてもらったりするには、以下でIDサーバーを設定してください。 + 現在%1$sを使用して、自分の連絡先を見つけたり、連絡先から見つけてもらったりできるようにしています。 IDサーバーを変更 IDサーバーの設定 - IDサーバーの切断 + IDサーバーから切断 IDサーバー ボット、ブリッジ、ウィジェット、ステッカーパックを使用 他の人が見つけられるように 利用規約 編集履歴を表示 提案 - クリップボードにコピーされたリンク - メイン画面に未読通知専用のタブを追加する。 + リンクをクリップボードにコピーしました + メイン画面に未読通知専用のタブを追加。 ルーム名を検索 - 名前もしくはID (#例えば:matrix.org) - ルームディレクトリを見る + 名前もしくはID(#example:matrix.org) + ルームディレクトリーを見る 新しいルームを作成 お探しのものが見つかりませんか? - あなたの提案をここに書いてください - ご意見・ご感想をお聞かせください。 + 提案をここに書いてください + 意見・感想を聞かせてください。 提案する - フォーマット: - URL: - セッションの表示名: + フォーマット: + URL: + セッションの表示名: 以下のうちいずれかが流出、あるいはハッキングされた恐れがあります。 \n \n- あなたのパスワード @@ -1423,7 +1421,7 @@ \n設定画面からパスワードとリカバリーキーを早急に変更することを推奨します。 電子メール アドレス - 続行する + 続行 ファイル このユーザーはスペースから追放されます。 \n @@ -1444,7 +1442,7 @@ %sとのビデオ通話 呼び出しています… ホームサーバーを選択 - %sのURLにあるホームサーバーに接続できません。リンクを確認するか、手動でホームサーバーを選択してください。 + URL %s のホームサーバーに接続できません。リンクを確認するか、手動でホームサーバーを選択してください。 後で スペース スレッドから @@ -1454,7 +1452,7 @@ PINコードを有効にする これを「招待者のみ参加可能」に設定しました。 ルームの設定 - コンテンツが報告されました + コンテンツを報告しました ヘルプとサポート ヘルプ ${app_name}の運営方針 @@ -1472,14 +1470,14 @@ このルームにファイルはありません このルームにメディアはありません 公開ルームをアップグレード - 非公開スペース + 非公開のスペース 公開スペース - 送信済 - 送信中 + 送信しました + 送信しています 種類 確認済 選択済 - ビデオ + 動画 画像 スクリーンショット 接続 @@ -1492,7 +1490,7 @@ 暗号化されていません 終了 再読み込み - セッション一覧 + セッション 警告 無視を解除 動画。 @@ -1505,7 +1503,7 @@ 警告 警告 成功しました! - 続行する + 続行 警告! 位置情報 メディア @@ -1521,7 +1519,7 @@ 音声メッセージを録音できません ルームのアップグレードには権限が必要です アップグレード - アップグレードが必要です + アップグレードが必要 非公開のルームをアップグレード 音声メッセージを一時停止 このメールアドレスをアカウントにリンク @@ -1544,11 +1542,11 @@ 位置情報を共有 スペースに関する変更を行うために必要な役割を更新する権限がありません スペースに関する変更を行うために必要な役割を選択 - スペースに関する変更を行うために必要な役割を表示し更新します。 - フィルター + スペースに関する変更を行うために必要な役割を表示し更新。 + 絞り込む スレッド スレッド - スペースをアップグレード + スペースのアップグレード スペース名の変更 スペースの権限 応答がありません @@ -1566,48 +1564,48 @@ ルーム名を設定 アカウントの設定 キーワード - ルームから退出しました! + ルームから退出しています! 吹き出しでメッセージを表示 電子メールによる通知 - セッションからサインアウトしました! + セッションからサインアウトしています! なし メンションとキーワードのみ ルームのスレッドを絞り込む 退出 携帯端末では、暗号化されたルームでのメンションとキーワードの通知は受信できません。 - ルームの暗号化の有効化 + スペースの暗号化の有効化 スペースのメインアドレスの変更 スペースのアバターの変更 アンケートを作成 アンケートを作成 - 暗号化が正しく設定されていないため、メッセージを送ることができません。クリックして設定を開いてください。 - 暗号化が正しく設定されていないため、メッセージを送ることができません。管理者に連絡して、暗号化を正しい状態に復元してください。 - %2$dの%1$d - あなたは既にこのスレッドを見ています! - ルームに表示 - ルームに表示 + 暗号化が正しく設定されていないため、メッセージを送信できません。クリックして設定を開いてください。 + 暗号化が正しく設定されていないため、メッセージを送信できません。管理者に連絡して、暗号化を正しい状態に復元してください。 + %2$d個のうち%1$d個 + 既にこのスレッドを表示しています! + ルーム内で表示 + ルーム内で表示 スレッドを表示 このルームへの参加は許可されていません - "トピック: " + "トピック: " トラブルシューティング 鍵のバックアップのバナーを閉じる キーワードに「%s」を含めることはできません %sへのメール通知を有効にする ヒント:メッセージを長押しして「%s」を選択。 - スレッドを用いると、会話のテーマを保ったり、会話を追跡したりするのが容易になります。 - あなたの非公開スペース + スレッド機能を使うと、会話のテーマを維持したり、会話を簡単に追跡したりすることができます。 + あなたの非公開のスペース あなたの公開スペース - 自分のみ - スレッドで議論を整理して管理 + 自分専用 + スレッド機能を使って、会話をまとめましょう %sを待機しています… この端末でスキャン - 認証を送信済 + 認証を送信しました このセッションを認証 音声 ルームのアドレスを入力してください このアドレスは既に使用されています スペースのアドレス - 一致しません + 一致していません 一致しています サインイン サインアウトしました @@ -1638,8 +1636,8 @@ 音声メッセージを録音 あなたのホームサーバーの運営方針 一番下に移動 - %sが読みました - %1$sと%2$sが読みました + %sが閲覧済 + %1$sと%2$sが閲覧済 ファイルを使用 暗号化を有効にしますか? キャンセルしました @@ -1669,20 +1667,20 @@ 新しいPINコード PINコードを再設定 PINコードを忘れましたか? - PINコードを入力 + PINコードを入力してください PINコードを確認 サードパーティー製ライブラリー これはいつでも設定から無効にできます 私たちは、情報を第三者と共有することはありません - 私たちは、アカウントのデータを記録したり分析したりすることはありません + 私たちは、アカウントのいかなるデータも記録したり分析したりすることは<b>ありません</b> ${app_name}の改善を手伝う このルームを「リンクを知っている人が参加可能」に設定しました。 - どのユーザーも無視していません + 無視しているユーザーはいません キーワードを入力するとリアクションを検索できます。 変更を加えませんでした %1$sは変更を加えませんでした - %d人のユーザーが読みました + %d人のユーザーが閲覧済 スペースへのアクセス このサーバーは運営方針を提供していません。 @@ -1699,7 +1697,7 @@ 連絡先を発見するには、連絡先のデータ(メールアドレスと電話番号)をあなたのIDサーバーに送信する必要があります。プライバシーの保護のため、データは送信前にハッシュ化されます。 メールアドレスと電話番号を%sに送信 このIDサーバーは運営方針を提供していません - IDサーバーの運営方針を隠す + IDサーバーの運営方針を表示しない IDサーバーの運営方針を表示 アカウントの新しいパスワードを設定… シェイクを検出しました! @@ -1712,18 +1710,18 @@ 復号エラーを自動的に報告する。 変更を有効にするにはアプリケーションの再起動が必要です。 LaTeXによる数学表記を有効にする - 以下が含まれる場合に通知 + 以下の場合に通知 アップグレードすると、このルームの新しいバージョンが作成されます。今ある全てのメッセージは、アーカイブしたルームに残ります。 誰がアクセスできますか? 通知は%1$sで管理できます。 暗号化されたルームでのメンションとキーワードによる通知は、携帯端末では利用できません。 ユーザーを無視し、そのメッセージを非表示に設定 - %sとのコマンドは認識されていますが、スレッドではサポートされていません。 + コマンド\"%s\"は認識されていますが、スレッドではサポートされていません。 誰でもスペースを発見し参加できます 法的情報 ユーザーに関する情報を表示 - このルームにおいてのみアバターを変更 - このルームにおいてのみ表示名を変更 + このルームでのみアバターを変更 + このルームでのみ表示名を変更 ユーザーの無視を解除し、以後のメッセージを表示 続行するには%sを入力してください 有効なリカバリーキーではありません @@ -1735,12 +1733,12 @@ アンケートの終了後に結果を公開 結果はアンケートを終了した後でのみ明らかにされます 以下で開く - 暗号化のアップグレードが利用可能です + 暗号化のアップグレードが利用できます SSSSキーをリカバリーキーから生成しています ${app_name} iOS \n${app_name} Android - ${app_name}ウェブ版 -\n${app_name}デスクトップ版 + ${app_name} ウェブ版 +\n${app_name} デスクトップ版 リカバリーキーを選択、直接入力、あるいはクリップボードからペースト リカバリーキーを使用 暗号化されたルームでのみサポート @@ -1750,14 +1748,14 @@ %1$d個の投票 アンケートを締め切り、最終結果を表示します。 - 招待者のみ参加可能。個人やチームに最適 + 招待者のみ参加可能。個人やチーム向け スペースを作成 連絡先をスペースに招待 IDサーバーには利用規約がありません あなたの連絡先は非公開です。端末の連絡先からユーザーを発見するためには、連絡先の情報をIDサーバーに送信する許可が必要です。 ディスカバリー設定を開く トピックを追加 - %sはルームを作成し設定しました。 + %sがルームを作成し設定しました。 参加しました。 %sが参加しました。 このルームで使用されている暗号化はサポートされていません @@ -1777,9 +1775,9 @@ 録音を削除 音声メッセージがアクティブの間は返信や編集はできません このアンケートを削除してよろしいですか?一度削除すると復元することはできません。 - 共有データの取り扱いに失敗しました - 回転とクロップ - ルームを探索 + 共有データを取り扱えませんでした + 回転とトリミング + ルームを探す 既存のルームとスペースを追加 スレッドのメッセージを有効にする おすすめに追加 @@ -1791,16 +1789,16 @@ \n既存のスペースを別のスペースに追加できます。 あなたのホームサーバーはまだスペースをサポートしていないようです 画像を追加 - このコンテンツは不適切な投稿として報告されています。 + このコンテンツを不適切な投稿として報告しました。 \n \nこのユーザーのコンテンツをこれ以上見たくなければ、ユーザーを無視してそのメッセージを非表示にできます。 - このコンテンツはスパムとして報告されています。 + このコンテンツをスパムとして報告しました。 \n \nこのユーザーのコンテンツをこれ以上見たくなければ、ユーザーを無視してそのメッセージを非表示にできます。 - このコンテンツが報告されています。 + このコンテンツを報告しました。 \n \nこのユーザーのコンテンツをこれ以上見たくなければ、ユーザーを無視してそのメッセージを非表示にできます。 - %1$sにより%2$sに + %1$sが%2$sにアップロード 質問あるいはトピック アンケートの質問あるいはトピック 少々お待ちください。少し時間がかかるかもしれません。 @@ -1819,7 +1817,7 @@ 再送信 コードを入力 電話番号 - 退席中 + 離席中 オフライン オンライン 一般 @@ -1828,7 +1826,7 @@ 認証済 未送信のメッセージを削除 カスタムイベントを送信 - ルームの状態を探索 + ルームの状態を調査 開封確認メッセージを表示 通知しない ファイルから鍵をインポート @@ -1842,7 +1840,7 @@ 初めに設定画面でIDサーバーの利用規約を承認してください。 初めにIDサーバーを設定してください。 - %1$d個の投票があります。結果を見るには投票してください + 合計%1$d票。投票すると結果を確認できます 未認証の端末で暗号化 メッセージを紙吹雪と共に送信 @@ -1850,27 +1848,27 @@ 紙吹雪🎉を送る 降雪❄️を送る あなたのチームのメッセージングに。 - エンドツーエンドで暗号化され、電話番号不要。広告やデータマイニング無し。 + エンドツーエンドで暗号化されており、登録に電話番号は不要です。広告もデータ収集もありません。 会話の保存先を自分で決められ、自分で管理できる独立したコミュニケーション。Matrixをもとに。 - お宅での対面会話と同じぐらいのプライバシーを提供する、セキュアで独立したコミュニケーション。 - セキュアメッセージング - 管理権を握るのは、あなたです。 + オンライン上でも対面の会話と同じレベルでプライバシーを守る、安全で独立したコミュニケーション。 + 安全なメッセージのやりとり。 + 主導権を握るのは、あなたです。 ${app_name}の使用に関するヘルプ 詳細なログは、イライラシェイクでログを送信する際に、より多くのログを提供することで、開発者にとっての助けになります。有効にした場合でも、メッセージの内容やその他のプライベートな情報は記録されません。 ルームのアップグレードは高度な作業であり、不具合や欠けている機能、セキュリティー上の脆弱性がある場合に推奨されます。 \nアップグレードは通常、ルームがサーバー上で処理される仕方にだけ影響します。 - 一度有効にしたルームの暗号化は無効にすることはできません。暗号化されたルームで送信されたメッセージは、サーバーからは見ることができず、そのルームのメンバーだけが見ることができます。暗号化を有効にすると、多くのボットやブリッジが正常に動作しなくなる場合があります。 + 一度有効にしたルームの暗号化は無効にすることはできません。暗号化されたルームで送信されたメッセージは、サーバーからは閲覧できず、そのルームのメンバーだけが閲覧できます。暗号化を有効にすると、多くのボットやブリッジが正常に動作しなくなる可能性があります。 %sして、このルームを皆に紹介しましょう。 このコードを共有し、スキャンして追加してもらい、会話を始めましょう。 - 正当な参加者が%sにアクセスできることを確認してください。 - 参加者を追加 + 正しい参加者が%sにアクセスできるようにしましょう。 + 連絡先を追加 %d人の知り合いがすでに参加しています %sに招待 ユーザー名かメールアドレスで招待 - %sを退出してよろしいですか? - スペースは、ルームや連絡先をグループ化する新しい方法です。 + %sから退出してよろしいですか? + スペースは、ルームや連絡先をまとめる新しい方法です。 招待されています 新しいスペースを、あなたが管理するスペースに追加。 注意:アプリケーションが再起動します @@ -1884,10 +1882,10 @@ %1$d個の投票に基づく - %1$d個の投票に基づく最終結果 + 合計%1$d票の投票に基づく最終結果 - 新しいセッションが認証されました。セッションは暗号化されたメッセージにアクセスでき、他のユーザーには信頼済として表示されます。 - このルームを同じホームサーバー上で組織内のチームとのコラボレーションにのみ使用するなら、このオプションを有効にするといいかもしれません。これは後から変更できません。 + 新しいセッションが認証されました。セッションは暗号化されたメッセージにアクセスすることができます。また、セッションは他のユーザーに「信頼済」として表示されます。 + このルームを同じホームサーバー上で組織内のチームとのコラボレーションにのみ使用する場合、このオプションを有効にするといいかもしれません。これは後から変更できません。 %sに属していないユーザーによるこのルームへの参加を、今後永久に拒否 プレーンテキストメッセージの前に ( ͡° ͜ʖ ͡°) を付ける このメールアドレスのドメインの登録は許可されていません @@ -1900,16 +1898,16 @@ デバッグ用の情報を画面に表示 初期同期中… 説明文が短すぎます - サインインして暗号鍵を取り戻さなければ、暗号化されたメッセージにアクセスできなくなります。 + サインインして暗号鍵を復旧しないと、暗号化されたメッセージにアクセスできなくなります。 再サインイン - 有効なホームサーバーを発見できません。識別子を確認してください + 有効なホームサーバーを発見できません。IDを確認してください どこかのホームサーバーで既にアカウントを登録している場合、以下でMatrix ID(例:@user:domain.com)とパスワードを使用してください。 入力したコードが正しくありません。確認してください。 - これは正しいユーザー識別子ではありません。正しいフォーマットは「@user:homeserver.org」です。 - パスワードをお忘れの場合、戻ってパスワードを再設定してください。 + これは正しいユーザーIDではありません。正しいフォーマットは「@user:homeserver.org」です。 + パスワードを忘れた場合、戻ってパスワードを再設定してください。 メールボックスを確認してください カスタムサーバーに接続 - 既にアカウントを持っています + 既にアカウントがあります 既存のサーバーに参加しますか? この質問をスキップ 友達と家族 @@ -1930,14 +1928,14 @@ 非公開で招待が必要なルームは表示されていません。 \nルームを追加する権限はありません。 非公開で招待が必要なルームは表示されていません。 - 知人に見つけてもらえるように電話番号を設定できます。任意です。 + 知人に見つけてもらえるように電話番号を設定できます(任意)。 メッセージキー 復旧用のパスフレーズ - ニックネームの色を変更 + 表示名の色を変更 パスワードはまだ変更されていません。 \n \n変更作業を中止しますか? - %1$sに確認メールを送信しました。 + %1$sに認証メールを送信しました。 メールボックスを確認してください サインインに戻る 元の大きさのままメディアファイルを送信 @@ -1950,15 +1948,15 @@ 信頼されていません 認証の要求 %sがキャンセルしました - 既読 + 閲覧済 認証 - メールアドレスが正しくないようです + メールアドレスの形式が正しくありません 国際電話番号の形式を使用してください。 - 国際電話番号は「+」から始まる必要があります + 国際電話番号は「+」から始めてください コードを%1$sに送信しました。以下に入力して認証してください。 このメールアドレスはどのアカウントにも登録されていません パスワードを変更すると、全てのセッションでのエンドツーエンド暗号鍵がリセットされ、暗号化されたメッセージ履歴が読めなくなります。パスワードを再設定する前に、鍵のバックアップを設定するか、他のセッションからルームの鍵をエクスポートしておいてください。 - パスワードの再設定を確認するために確認メールを送信します。 + パスワードの再設定を確認するために認証メールを送信します。 このメールアドレスはどのアカウントにも登録されていません。 このアプリでは、このホームサーバーにアカウントを作成できません。 \n @@ -1966,12 +1964,12 @@ 申し訳ありませんが、このサーバーはアカウントの新規登録を受け入れていません。 このアプリではこのホームサーバーにサインインできません。このホームサーバーは次のサインインの方法に対応しています:%1$s \n -\nウェブクライエントを使用してサインインしますか? - %1$sを読み込み中にエラーが発生しました(%2$d) - 利用したいサーバーのアドレスを入力してください - 利用したいModular Elementまたはサーバーのアドレスを入力してください - 迷っていますか?%sしてもいいです - みんなと繋がる手助けをいたします。 +\nウェブクライアントを使用してサインインしますか? + ページ %1$s を読み込んでいる際にエラーが発生しました(%2$d) + 使用したいサーバーのアドレスを入力してください + 使用したいModular Elementまたはサーバーのアドレスを入力してください + 迷っていますか?%s + みんなと繋がる手助けをいたします 自分のコード 招待を%1$sと他%2$d人に送信しました @@ -1988,13 +1986,13 @@ 音を出さずに通知 音を出して通知 既定の信頼レベル - いくつかのメッセージは送信されませんでした + いくつかのメッセージが送信されませんでした 保存されていない変更があります。変更を破棄しますか? このルームはまだ作成されていません。キャンセルしますか? テキストメッセージで共有 保護を設定 このルームのみ - 誰でも参加可能。コミュニティーに最適 + 誰でも参加可能。コミュニティー向け 既存のスペースに参加するには、招待が必要です。 これは後から変更できます 変更を破棄 @@ -2002,13 +2000,13 @@ QRコードがスキャンされていません! 内容を通知に表示 プッシュ通知は無効になっています - ユーザーのブロックを解除できませんでした + ユーザーのブロックの解除に失敗しました %1$sへの招待を取り消しますか? 連絡先を取得しています… RiotはElementになりました! このメッセージにアクセスできません アバターを設定 - IDサーバーのURLを入力 + IDサーバーのURLを入力してください マイクのミュートを解除 マイクをミュート %1$sを使用 @@ -2027,22 +2025,22 @@ \n - 認証している相手が接続しているホームサーバー \n - あなたか相手のインターネット接続 \n - あなたか相手の端末 - セキュアではない - 信頼できないサインイン + セキュアではありません + 信頼されていないサインイン 使用できない文字が含まれています ${app_name}の改善と課題抽出のために、匿名の使用状況データの送信をお願いします。複数の端末での使用を分析するために、あなたの全端末共通のランダムな識別子を生成します。 \n \n%sで利用規約を閲覧できます。 最初の検索結果のみ表示しています。文字をもっと入力してください… matrix.toリンクのフォーマットが正しくありませんでした - 注意!この端末には暗号鍵を含む個人情報が保存されています。 + 注意!この端末には暗号鍵を含む個人データが保存されています。 \n -\nこの端末での使用を終了、または他のアカウントにサインインしたい場合、このデータをクリアしてください。 +\nこの端末での使用を終了、または他のアカウントにサインインする場合、このデータをクリアしてください。 この端末に現在保存されている全てのデータをクリアしますか? \nアカウントデータとメッセージにアクセスするにはもう一度サインインしてください。 現在のセッションはユーザー %1$s のものですが、あなたが提供している認証情報はユーザー %2$s のものです。この操作は${app_name}ではサポートされていません。 -\nまずデータをクリアし、その後、別のアカウントにサインインしてください。 - 暗号化されたメッセージがどの端末でも読めるように、サインインしてこの端末にのみ保存されている暗号鍵を取り戻してください。 +\nデータをクリアし、その後、別のアカウントにサインインしてください。 + 暗号化されたメッセージがどの端末でも読めるように、サインインしてこの端末にのみ保存されている暗号鍵を復旧してください。 あなたのホームサーバー(%1$s)の管理者があなたを%2$sのアカウントからサインアウトしました(%3$s)。 いくつかの原因が考えられます: \n @@ -2054,16 +2052,14 @@ リクエストが多すぎます。%1$d秒後に再試行できます… - このホームサーバーは古いバージョンです。管理者にアップグレードを要請してください。続行できますが、いくつかの機能が正しく作動しない可能性があります。 + このホームサーバーは古いバージョンです。管理者にアップグレードを依頼してください。続行できますが、いくつかの機能が正しく作動しない可能性があります。 ホームサーバーのバージョンが古すぎます - ただいま%1$sにメールを送信しました。 -\nアカウント登録を続行するにはメール内のリンクをクリックしてください。 + %1$sにメールを送信しました。 +\nアカウント登録を続行するには、メール内のリンクをクリックしてください。 CAPTCHA認証を行ってください - アカウントがまだ登録されていません。 -\n -\n登録を中止しますか? + アカウントがまだ作成されていません。登録を中止しますか? %1$sにアカウント登録 - あなたはすべてのセッションからログアウトしており、これ以上プッシュ通知を受け取れません。通知を再び有効にするには、各端末でサインインしてください。 + すべてのセッションからログアウトしているため、プッシュ通知を受け取れません。通知を再び有効にするには、各端末でサインインしてください。 セキュリティーフレーズを設定 セキュリティーフレーズを使用 セキュリティーキーは、パスワードマネージャーもしくは金庫のような安全な場所で保管してください。 @@ -2078,8 +2074,8 @@ サーバー上の暗号鍵をバックアップして、暗号化されたメッセージとデータへのアクセスが失われるのを防ぎましょう。 いまキャンセルすると、ログインできなくなった際に、暗号化されたメッセージとデータを失ってしまう可能性があります。 \n -\nまた、設定から、安全なバックアップの設定や鍵の管理を行うことができます。 - USBメモリーもしくはバックアップドライブに保存 +\n設定から、セキュアバックアップの設定や鍵の管理を行うこともできます。 + USBメモリーやバックアップ用のドライブに保存 鍵のバックアップの設定 自己署名キーを同期しています ユーザーキーを同期しています @@ -2091,12 +2087,12 @@ 鍵は既に最新です! 鍵をリセット 質問は空にできません - ここで送受信されるメッセージはエンドツーエンド暗号化されています。 + ここでのメッセージはエンドツーエンドで暗号化されています。 \n -\nメッセージは安全に保護されており、メッセージのロックを解除できる固有の鍵を持っているのはあなたと受信者だけです。 - この部屋のメッセージはエンドツーエンド暗号化されています。 +\nメッセージは安全に保護されており、メッセージのロックを解除するための固有の鍵は、あなたと受信者だけが持っています。 + このルームのメッセージはエンドツーエンドで暗号化されています。 \n -\nメッセージは安全に保護されており、メッセージのロックを解除できる固有の鍵を持っているのはあなたと受信者だけです。 +\nメッセージは安全に保護されており、メッセージのロックを解除するための固有の鍵は、あなたと受信者だけが持っています。 ステートキー リカバリーキーを以下に保存 送信者が意図的に鍵を送信しなかったため、このメッセージにアクセスすることができません @@ -2113,8 +2109,8 @@ ナビゲーションのメニューを開く 承諾しました %sが承諾しました - %sが認証済 - %sを認証する + %sを認証済 + %sを認証 絵文字を比較して認証 絵文字を比較して認証 対面でない場合は、代わりに絵文字を比較してください @@ -2128,31 +2124,31 @@ あなただけが知っている秘密のパスワードを入力してください。バックアップ用にセキュリティーキーを生成します。 暗号化されたメッセージにアクセスするには、ログインを認証し、本人確認を行う必要があります。 暗号化されたメッセージにアクセスするには、あなたの他のセッションからログインを認証し、本人確認を行う必要があります。 - 詳しく知る + 詳細を確認 セキュリティーを高めるために、使い捨てコードが一致しているのを確認して、%sを認証しましょう。 暗号化の設定が正しくありません。 暗号化を復元 - 暗号化を有効な状態に取り戻すために、管理者に連絡してください。 - このユーザーとのメッセージはエンドツーエンド暗号化されており、第三者には読めません。 + 暗号化を正常な状態に戻すために、管理者に連絡してください。 + このユーザーとのメッセージはエンドツーエンドで暗号化されており、第三者が解読することはできません。 このコードを相手の画面に現れているコードと比較してください。 - 絵文字を比較して、同じ順番に現れているのを確認してください。 - セキュリティーを高めるために、対面で行うか、他の信頼できる通信手段を利用しましょう。 - 選択されたエモートを虹色にして送信します - 選択されたテキストを虹色にして送信します - ${app_name}がID%1$sのイベントを処理中にエラーが発生しました - ${app_name}は%1$sという種類のイベントに対応していません + 絵文字を比較して、同じ順番で現れていることを確認してください。 + セキュリティーを高めるために、対面で行うか、他の通信手段を利用しましょう。 + 指定したエモートを虹色で送信 + 指定したテキストを虹色で送信 + ${app_name}は、ID \'%1$s\'のイベントのコンテンツを描画している際にエラーに遭遇しました + ${app_name}は\'%1$s\'という種類のイベントに対応していません 既読通知へ移動 大切に保護しましょう 完了! アカウントパスワードと違うものにしてください。 続行するには%sを入力してください。 認証を中止しました - 今中止すると、%1$s(%2$s)を認証しません。認証は相手のユーザープロフィール画面からもう一度開始できます。 + 中止すると、%1$s(%2$s)を認証しません。認証は、相手のユーザープロフィール画面から改めて開始できます。 中止すると、新しい端末では暗号化されたメッセージが読めず、他のユーザーに信頼されません 中止すると、この端末では暗号化されたメッセージが読めず、他のユーザーに信頼されません - 自分ではない + ログインしていません 新しいセッションを認証して、暗号化されたメッセージにアクセスできるようにしましょう。 - 新しいログイン。あなたですか? + 新しいログインです。ログインしましたか? ${app_name} Android ルームの管理者によって削除されています。理由:%1$s ユーザーによって削除されています。理由:%1$s @@ -2161,34 +2157,34 @@ 既存のセッションにアクセスできない場合 %1$sというタイプのアカウントデータを削除しますか? \n -\n予期しないトラブルを起こす可能性があるので注意してください。 +\n予期しない動作が起こる可能性があるため、注意して使用してください。 %1$s(%2$s)が新しいセッションでサインインしました: - このセッションは%1$s(%2$s)によって認証されているので、メッセージのセキュリティは信頼できます。 - 既存のセッションでこのセッションを認証して、暗号化されたメッセージへアクセスできるようにしましょう。 - あなたはこのセッションを認証しているので、メッセージのセキュリティは信頼できます。 + このセッションは%1$s(%2$s)によって認証されているので、メッセージのセキュリティーは信頼できます。 + 既存のセッションでこのセッションを認証して、暗号化されたメッセージにアクセスできるようにしましょう。 + あなたはこのセッションを認証しているので、メッセージのセキュリティーは信頼できます。 利用可能な暗号情報がありません 既定のバージョン 非公開のルームとダイレクトメッセージにおけるエンドツーエンド暗号化は、あなたのサーバーの管理者により既定として無効にされています。 理由を含める - %1$sが%2$sの権限レベルを変更しました。 - %1$sの権限レベルを変更しました。 + %1$sが%2$s変更しました。 + %1$s変更しました。 誰と使いますか? - どんなスペースを作りますか? - 自分と仲間の非公開のスペース - ルームを整理するためのプライベートスペース + 作成するスペースの種類を選択してください + 自分とチームメイトの非公開のスペース + ルームを整理するための非公開のスペース ここが会話のスタート地点です。 - ここが%sのスタート地点です。 - あと少しです!確認を待機しています… - あと少しです!もう一方のデバイスは同じマークを表示していますか? + ここが%sの始まりです。 + もう少しです!確認を待機しています… + あと少しです!もう一方の端末は同じマークを表示していますか? %sを待機しています… - このユーザーがこのセッションを認証するまで、送受信されるメッセージには警告マークが付きます。手動で認証することも可能です。 + このユーザーがこのセッションを認証するまで、送受信されるメッセージには警告マークが付きます。手動で認証することもできます。 セッションの取得に失敗しました 誰がチームの仲間ですか? - %sを探索できるようになります + %sを探せるようになります 私のスペース %1$s %2$s に参加してください スキップ ルームの通知 - 現在、IDサーバーを使用していません。あなたの知っているチームメイトを発見したり、そのチームメイトから発見されるようにするには、以下でIDサーバーを設定してください。 + 現在、IDサーバーを使用していません。あなたの知っているチームメイトを招待したり、チームメイトから見つけてもらったりするには、以下でIDサーバーを設定してください。 ディスカバリーの設定を終了します。 ここの参加者はあなただけです。退出すると、今後あなたを含めて誰も参加できなくなります。 再び招待されない限り、再参加することはできません。 @@ -2211,17 +2207,17 @@ 再認証が必要です 全てリセット 連絡先 - 認証をキャンセルしました。あらためて開始してください。 + 認証をキャンセルしました。改めて開始してください。 押し続けて録音し、離すと送信 PINコードを設定してください %d個のサーバーアクセス制御リストの変更 置き換えられたルームに参加 - このルームが発見できません。存在することを確認してください。 + このルームを発見できません。存在することを確認してください。 指紋や顔画像など、端末に固有の生体認証を有効にする。 絵文字で認証 - テキストで認証 + テキストを使って手動で認証 復旧用の手段を全て無くしてしまいましたか?全てリセットする クロス署名に対応した他のMatrixのクライアントでも使用できます。 どのような議論を%sで行いたいですか? @@ -2231,20 +2227,20 @@ 最新の${app_name}を他の端末で、${app_name} ウェブ版、${app_name} デスクトップ版、${app_name} iOS、${app_name} Android、あるいはクロス署名に対応した他のMatrixのクライアントでご使用ください スライドして通話を終了 電話番号を検索する際にエラーが発生しました - 着信を拒否しました + 通話を拒否しました それぞれにルームを作りましょう。後から追加することもできます(既にあるルームも追加できます)。 このスペースを特定できるような特徴を記入してください。これはいつでも変更できます。 目立つように特徴を記入してください。これはいつでも変更できます。 未読のメッセージ数のみを通知に表示。 2分間${app_name}を使用しないと、PINコードが要求されます。 🔐️ ${app_name}で話しましょう - 個人情報保護の観点から、${app_name}はハッシュ化されたメールアドレスと電話番号の送信のみをサポートしています。 + プライバシーの保護の観点から、${app_name}はハッシュ化されたメールアドレスと電話番号の送信のみをサポートしています。 アプリの名前を変更しました!アプリは最新版で、アカウントにはログイン済です。 ステートイベントを送信 ステートイベント カスタムのステートイベントを送信 ステートイベントを送信しました! - 続行するには名前を付けてください。 + 続行するには名前を設定してください。 どんな作業に取り組みますか? あと%1$d件 @@ -2253,7 +2249,7 @@ 通話の転送中にエラーが発生しました 2分後にPINコードを要求 ルーム名やメッセージの内容などの詳細を表示。 - エラーが多すぎます。ログアウトしました + 多数のエラーが発生したため、ログアウトしました 警告!もう一度誤ったコードを入力すると、ログアウトします! コードが誤っています。残りの試行回数は%d回です @@ -2263,7 +2259,7 @@ \n \n続行してよろしいですか? このリンクを再確認してください - ログインを認証してください:%1$s + 新しいログインがあなたのアカウントにアクセスしています。ログインを認証してください:%1$s 機密ストレージのアクセスに失敗しました この設定を有効にすると、全てのアクティビティーにFLAG_SECUREを追加します。変更を有効にするにはアプリケーションの再起動が必要です。 このアカウントは無効化されています。 @@ -2271,14 +2267,14 @@ 印刷して安全な場所に保管 %2$sと%1$sが設定されました。 \n -\n安全な場所で保管してください!それらは、アクティブなセッションを全て失ってしまった際、暗号化されたメッセージや安全な情報のロックを解除するために必要となります。 +\n安全な場所で保管してください!アクティブなセッションを全て失ってしまった際、暗号化されたメッセージや安全な情報のロックを解除するために必要となります。 作成したアイデンティティーキーを公開しています アプリケーションのスクリーンショットを防ぐ 続行するには%1$sか%2$sを使用してください。 エラーのためメッセージが送信されませんでした %sにいない人を探していますか? 直接${app_name}で招待を受け取るには、設定画面から%sしてください。 - PINコードでしか${app_name}のロックを解除することはできません。 + PINコードでしか${app_name}のロックを解除できません。 ${app_name}を開く際には、毎回PINコードの入力が必要です。 あなたがブロックされているルームを開くことはできません。 PINコードの認証に失敗しました。新しいコードを入力してください。 @@ -2291,9 +2287,9 @@ 確認のため、セキュリティーフレーズを再入力してください。 ホームサーバー(%1$s)は、IDサーバーに%2$sを設定するように提案しています IDサーバー %s から切断しますか? - ダイレクトメッセージを作成できませんでした。招待したユーザーを確認し、もう一度やり直してください。 + ダイレクトメッセージを作成できませんでした。招待したいユーザーを確認し、もう一度やり直してください。 セキュリティーフレーズ - 自分と仲間 + 自分とチームメイト メッセージの種類がありません 絵文字の一覧を閉じる 絵文字の一覧を開く @@ -2311,7 +2307,7 @@ ビデオ通話が拒否されました 音声通話が拒否されました %1$sは通話を拒否しました - このデバイスを認証可能な他の端末が全くない場合にのみ、続行してください。 + この端末を認証できる他の端末が全くない場合にのみ、続行してください。 このセッションを信頼済として認証すると、暗号化されたメッセージにアクセスすることができます。このアカウントにサインインしなかった場合は、あなたのアカウントのセキュリティーが破られている可能性があります: アカウントのセキュリティーが破られている可能性があります 選択したスペースに追加 @@ -2331,8 +2327,8 @@ %d個のエントリー 保存して続行 - 設定を保存しました。 - これは後から変更できます。 + 設定画面からいつでもプロフィールを更新できます + 表示名にプロフィール画像を追加しましょう プロフィール画像を追加 これは後から変更できます 表示名 @@ -2342,20 +2338,20 @@ 位置情報(ライブ)を共有 中止 表示名を選択 - あなたのアカウント %s が作成されました。 + あなたのアカウント %s が作成されました おめでとうございます! - 近日中にスレッドはベータ版となります。 + 近日中にスレッド機能はベータ版となります。 \n \nその準備として、この時点以前に作成されたスレッドは、通常の返信として表示するように変更します。 \n -\nスレッドはMatrixの仕様の一部になったため、これは一度限りの変更です。 - スレッドはベータ版になります 🎉 +\nスレッド機能はMatrixの仕様の一部になったため、これは一度限りの変更です。 + スレッド機能はベータ版になります 🎉 無効にする - スレッドについてのフィードバック + スレッド機能についてのフィードバック フィードバックを送信 ベータ版 ベータ版 - 試す + 試してみる オフラインモード 新着はありません。 - ユーザーの無視が解除されました @@ -2367,7 +2363,7 @@ ${app_name}をシンプルにするために、タブはオプションになりました。右上のメニューから管理できます。 新しいレイアウトにようこそ! アニメーション画像を自動再生 - エンドポイントのホームサーバーへの登録に失敗しました: + エンドポイントのホームサーバーへの登録に失敗しました: \n%1$s エンドポイントがホームサーバーに登録されました。 エンドポイントの登録 @@ -2375,14 +2371,14 @@ ${app_name}は通知の表示に権限が必要です。 \n権限を与えてください。 - %1$sと他%2$d名 + %1$sと他%2$d人 %1$sと%2$s - ホームサーバーがサポートしていないため、スレッド機能は不安定かもしれません。スレッドのメッセージは安定して表示されないおそれがあります。%sスレッド機能を有効にしてよろしいですか? - スレッド(ベータ版) - スレッドを用いると、会話のテーマを保ったり、会話を追跡したりするのが容易になります。%sスレッドを有効にするとアプリケーションが再起動します。再起動には時間がかかる可能性があります。 - スレッド(ベータ版) - ${app_name}は通知を表示するために許可を必要としています。通知にはメッセージや招待などが表示されます。 + ホームサーバーがサポートしていないため、スレッド機能は不安定かもしれません。スレッドのメッセージが安定して表示されないおそれがあります。%sスレッド機能を有効にしてよろしいですか? + スレッド機能(ベータ版) + スレッド機能を使うと、会話のテーマを維持したり、会話を簡単に追跡したりすることができます。%sスレッド機能を有効にするとアプリケーションが再起動します。再起動には時間がかかる可能性があります。 + スレッド機能(ベータ版) + ${app_name}は、通知を表示するための権限を必要としています。通知にはメッセージや招待などが表示されます。 \n \n通知を表示するには、次のポップアップでアクセスを許可してください。 メールアドレスが認証されていません。メールボックスを確認してください @@ -2403,18 +2399,18 @@ このステップをスキップ 問題ありません! 進みましょう - ユーザー名 / メールアドレス / 電話番号 + ユーザー名 / 電子メール / 電話番号 あなたは人間ですか? - %sに送信された手順に従ってください + %sに送信された手順に従ってください。 パスワードを再設定 パスワードを忘れた場合 電子メールを再送信 電子メールが届いていませんか? - %sに送信された手順に従ってください + %sに送信された手順に従ってください。 メールアドレスを認証 コードを再送信 コードが%sに送信されました - 電話番号を確認してください + 電話番号を確認 全ての端末からサインアウト パスワードを再設定 パスワードは8文字以上に設定してください。 @@ -2431,7 +2427,7 @@ リッチテキストエディターを有効にする 最初のメッセージを送信する際にダイレクトメッセージを作成 遅延DMを有効にする - スペースがありません。 + まだスペースがありません。 新しいレイアウトを有効にする アクティビティー順 アルファベット順 @@ -2449,12 +2445,12 @@ \n \nアプリケーションが再起動します。再起動には時間がかかる可能性があります。 初期同期のリクエスト - %sの子スペースを折りたたむ - %sの子スペースを展開 - ルームを探索 + %sのサブスペースを折りたたむ + %sのサブスペースを展開 + ルームを探す スペースを変更 ルームを作成 - チャットを開始 + 会話を開始 全ての会話 ${app_name}にようこそ、 \n%s。 @@ -2472,7 +2468,7 @@ QRコードが不正です。 スペースは、ルームと連絡先をまとめる新しい方法です。はじめに、スペースを作成しましょう。 最近の履歴を表示 - この暗号化されたメッセージの信頼性はこの端末では保証できません。 + この暗号化されたメッセージの真正性はこの端末では保証できません。 アカウントが安全かどうか確認してください 未認証のセッションがあります 連絡先 @@ -2485,7 +2481,7 @@ 音声配信を終了しました。 %1$sが音声配信を終了しました。 - %1$dを選択しました + %1$d個選択済 有効にすると、このアプリケーションを使用している際にも、他のユーザーにオフラインとして表示されます。 最近のチャットをシステムの共有メニューに表示 @@ -2494,10 +2490,368 @@ 自動的に設定 フォントの大きさを選択 ⚠ 未認証の端末がこのルームにあります。あなたが送信するメッセージを復号化することはできません。 - このルームの未認証のセッションに暗号化されたメッセージを送信しない。 + このルームの未認証のセッションに対して暗号化されたメッセージを送信しない。 あなたのホームサーバーはスレッドの一覧表示をまだサポートしていません。 ここに新しいリクエストと招待が表示されます。 リッチテキストエディターを試してみる(プレーンテキストモードは近日公開) タブを使用してElementの表示をシンプルにする セッションの詳細 + + %1$d日以上使用されていません + + + %1$d日以上使用されていません(%2$s) + + 地図を読み込めません +\nこのホームサーバーは地図が読み込むよう設定されていないおそれがあります。 + スペースは、ルームや連絡先をまとめる新しい方法です。右下のボタンを使うと、既存のルームを追加したり新たに作成したりできます。 + セキュリティーに関する勧告 + その他のセッション + セキュリティーを最大限に高めるには、不明なセッションや利用していないセッションからサインアウトしてください。 + 生体認証を有効にできませんでした。 + 関連付けに失敗しました。 + おかえりなさい! + または + %sに返信しています + アニメーション画像がタイムラインに表示されたらすぐに再生 + 斜字体にする + 非アクティブなセッションは、しばらく使用されていませんが、暗号鍵を受信しているセッションです。 +\n +\n使用していないセッションを削除すると、セキュリティーとパフォーマンスが改善されます。また、新しいセッションが疑わしい場合に、より容易に特定できるようになります。 + 非アクティブなセッション + 改善したセッションの管理画面を使用します。 + 未認証のセッション + ルームのタイムラインで音声配信を録音して送信することを可能にします。 + 音声配信を有効にする + 未読のメッセージがある場合は、ここに表示されます。 + 未読はありません。 + クライアントの情報の保存を有効にする + セッション名は連絡先にも表示されます。 + セッション名を設定すると、端末をより簡単に認識できるようになります。 + このセッションでプッシュ通知を受信。 + 絞り込みを解除 + 絞り込む + アプリケーション、端末、アクティビティーに関する情報。 + 直近のアクティビティー + セッション名 + + %1$d件のセッションからサインアウト + + 使用していないセッションはありません。 + 未認証のセッションはありません。 + 認証済のセッションはありません。 + + 使用していない古いセッション(%1$d日以上使用されていません)からサインアウトすることを検討してください。 + + 非アクティブ + セッションを認証すると、より安全なメッセージのやりとりが可能になります。見覚えのない、または使用していないセッションがあれば、サインアウトしましょう。 + 未認証 + 認証済 + 太字にする + 端末に接続しています + ホームサーバーはQRコードによるサインインをサポートしていません。 + 安全な接続を確立しました + 確認 + リンクを設定 + 全画面モードを切り替える + テキスト + リンク + リンクを作成 + リンクを編集 + 返信先 + アンケート + アクセストークン + アクセストークンを用いると、あなたのアカウントの全ての情報にアクセスできます。外部に公開したり、誰かと共有したりしないでください。 + セッションを選択 + このセッションからサインアウト + IPアドレスを表示しない + IPアドレスを表示 + 他の全てのセッションからサインアウト + サインアウト + 非アクティブ + 安全なメッセージのやりとりの準備ができていません + 未認証 + 安全なメッセージのやりとりの準備ができました + 認証済 + 全てのセッション + 端末 + セッション + 現在のセッション + 非アクティブなセッション + 未認証のセッション + 以下の勧告に従い、アカウントのセキュリティーを改善しましょう。 + 全て表示(%1$d) + 詳細を表示 + セッションを認証 + このセッションは暗号化をサポートしていないため、認証できません。 + セキュリティーと安定性の観点から、このセッションを認証するかサインアウトしてください。 + より安全なメッセージのやりとりのために、現在のセッションを認証しましょう。 + このセッションは安全なメッセージのやりとりの準備ができています。 + 現在のセッションは安全なメッセージのやりとりに対応しています。 + 認証の状態が不明です + 未認証のセッション + 認証済のセッション + 端末の種類が不明です + デスクトップ + ウェブ + 携帯端末 + + %d件のメッセージを削除しました + + 位置情報の共有を有効にする + 注意:これは一時的な実装による試験機能です。位置情報の履歴を削除することはできません。高度なユーザーは、あなたがこのルームで位置情報(ライブ)の共有を停止した後でも、あなたの位置情報の履歴を閲覧することができます。 + 位置情報(ライブ)の共有 + エンドポイントが見つかりません。 + 現在のエンドポイント:%s + エンドポイント + 現在%sを使用しています。 + 方法 + バックグラウンド同期 + バックグラウンド同期以外の方法がありません。 + Google Playサービス以外の方法がありません。 + 利用可能な方法 + 通知方法 + Googleサービス + 通知の受信方法を選択してください + 画面を共有しています + ${app_name}画面共有 + テキストの装飾 + 連絡先 + カメラ + 位置情報 + アンケート + 音声配信 + 添付ファイル + ステッカー + 音声配信を開始 + 位置情報(ライブ) + 位置情報を共有 + このルームでの位置情報(ライブ)の共有には適切な権限が必要です。 + 位置情報(ライブ)の共有に必要な権限がありません + 一時的な実装。位置情報がルームの履歴に残ります + 位置情報(ライブ)の共有を有効にする + 位置情報を共有しています + ${app_name}位置情報(ライブ) + 残り%1$s + 位置情報(ライブ)を表示 + 位置情報(ライブ)が終了しました + 位置情報(ライブ)を読み込んでいます… + 8時間 + 1時間 + 15分 + 位置情報(ライブ)を共有する時間 + 現在の位置にズーム + 地図で選択した位置情報のピン + アンケートの取得中にエラーが発生しました。 + アンケートを表示しています + このルームに過去のアンケートはありません + 過去のアンケート + このルームに実施中のアンケートはありません + 実施中のアンケート + 復号エラーにより、いくつかの投票はカウントできません + アンケートを終了しました。 + アンケートが終了するまで結果は表示できません + 履歴を共有している暗号化されたルームに招待する際、暗号化された履歴が表示されるようになります。 + MSC3061:過去のメッセージ用にルームの鍵を共有 + ライブ配信を終了してよろしいですか?配信を終了し、録音をこのルームで利用できるよう設定します。 + ライブ配信を停止しますか? + 残り%1$s + 接続エラー - 録音を停止しました + この音声配信を再生できません。 + 既に音声配信を録音しています。新しく始めるには今の音声配信を終了してください。 + 他の人が既に音声配信を録音しています。新しく始めるには音声配信が終わるまで待機してください。 + このルームで音声配信を開始する権限がありません。ルームの管理者に連絡して権限の付与を依頼してください。 + 新しい音声配信を開始できません + 音声配信を一時停止 + 音声配信の録音を再開 + バッファリングしています… + ライブ配信 + ライブ + (%1$s) + %1$s(%2$s) + %1$sを再生できません + %1$sを一時停止 + %1$sを再生 + %1$d分%2$d秒 + ライブ配信を録音しているため、音声メッセージを開始できません。音声メッセージの録音を開始するには、ライブ配信を終了してください + 音声メッセージを開始できません + 全てのメッセージに最新のプロフィール情報(アバターと表示名)を表示。 + 最新のユーザー情報を表示 + 検索結果がありません + 退出しない + 全て退出 + 現在、このエイリアスにはアクセスできません。 +\n後でもう一度やり直すか、ルームの管理者にアクセス権があるかどうかを確認するよう依頼してください。 + %sのメンバーにはなりません + 設定を開く + ビデオ通話の着信中 + 音声通話の着信中 + 暗号化されたルーム内の現在のアウトバウンドグループセッションを強制的に破棄 + 会話で入力したデータに基づいて入力履歴や辞書などに関する個人用データを変更しないようキーボードに指示します。いくつかのキーボードでは、この設定が無視されることがあります。 + プライベートキーボード + このチャットのメッセージはエンドツーエンドで暗号化されます。 + このQRコードは不正な形式です。他の方法で認証を試してください。 + 最新版を入手(注意:サインインの際に問題が起こる可能性があります) + 暗号化されたメッセージの履歴にアクセスできません。鍵の安全なバックアップと認証用の鍵をリセットして、やり直してください。 + この端末を認証できません + セッション + アンケートの履歴 + 音声配信を開始しました + 位置情報(ライブ)を共有しました + プレーンテキストメッセージの前に (╯°□°)╯︵ ┻━┻ を付ける + このリンクを開けません:コミュニティー機能はスペース機能に変更されました + 電子メールを入力してください + %sの利用規約と運営方針を確認してください + サーバーの運営方針 + 問い合わせる + 自分でサーバーを運営しますか? + サーバーのURL + ホームサーバーのアドレスを入力してください + あなたのホームサーバーのアドレスを入力してください。ここにあなたの全てのデータがホストされます + サーバーを選択 + 編集 + 8文字以上にしてください + アカウントを作成 + ホームに移動 + プロフィールを設定 + ${app_name}は職場利用にも最適です。世界で最も安全な組織によって信頼されています。 + 音声配信 + スペースの一覧を開く + 新しい会話またはルームを作成 + 通知方法をリセット + セッションID: + 続行 + データをアップデートしています… + 編集中 + バックアップにはこのユーザーによる有効な署名があります。 + 開発者ツールの画面を開く + ルームが見つかりませんでした。 +\n後でやり直してください。%s + 直接共有を有効にする + 信頼済の信頼レベル + 警告の信頼レベル + %1$sと相談しています + 初めに相談 + 実施中の通話(%1$s)・ + + %1$d件の実施中の通話・ + + 実施中の通話(%1$s) + 不在着信(ビデオ) + 不在着信(音声) + 実施中のビデオ通話 + 実施中の音声通話 + 新しい生体認証の方法が最近追加されたため、生体認証は無効になりました。設定から再び有効にできます。 + このIDとの連携は現在ありません。 + このホームサーバーは数字だけからなるユーザー名を承諾しません。 + 最初のメッセージを送信すると、%sを会話に招待 + Nightly build + あなたの他の端末でコードをスキャンするか、もしくは反対に、この端末でスキャンしてください + Element Matrix Services(EMS)は、高速、安全でリアルタイムのコミュニケーション向きの、堅牢で安定したホスティングサービスです。<a href=\"${ftue_ems_url}\">element.io/ems</a>で方法を調べましょう。 + アカウントにサインインするサーバー + アカウントを作成するサーバー + スレッド機能については、改良した通知など新機能の追加などを行っています。フィードバックをお聞かせください! + 🔒 セキュリティーの設定で、全てのルームに関して認証済のセッションにのみ暗号化を行うよう設定しました。 + プレゼンス(ステータス表示) + 取り込み中 + 他の人は %s であなたを見つけることができます + 有効: + プロフィールのタグ: + 問題が発生しました。ネットワークの接続を確認して、もう一度やり直してください。 + 引用 + チーム、友達、組織向けのオールインワンの安全なチャットアプリです。はじめに、チャットを作成するか既存のルームに参加しましょう。 + 一致していませんか? + 接続に失敗しました + サインイン済の端末を確認してください。以下のコードが表示されているはずです。以下のコードがサインイン済の端末と一致していることを確認してください: + サインイン済の端末で以下のQRコードをスキャンしてください: + この端末を使い、QRコードをスキャンして新しい端末でサインインできます。2つの方法があります: + このスペース内のもの + 正しい参加者が%sにアクセスできるようにしましょう。後から追加で招待できます。 + 終了したアンケート + アンケートを終了しました。 + アンケートを作成しました。 + ステッカーを送信しました。 + 動画を送信しました。 + 画像を送信しました。 + 音声メッセージを送信しました。 + 音声ファイルを送信しました。 + ファイルを送信しました。 + インラインコードの装飾を適用 + 箇条書きリストの表示を切り替える + 番号付きリストの表示を切り替える + 下線で装飾 + 打ち消し線で装飾 + このコードの出所を知っていることを確認してください。端末をリンクすると、あなたのアカウントに無制限にアクセスできるようになります。 + もう一度試してください + サインインしています + この端末でQRコードを表示 + 「QRコードをスキャン」を選択してください + 「QRコードでサインイン」を選択してください + 「QRコードを表示」を選択してください + 設定から「セキュリティーとプライバシー」を開いてください + 他の端末でアプリを開いてください + もう一方の端末のサインインはキャンセルされました。 + もう一方のデバイスは既にサインインしています。 + リクエストはもう一方の端末で拒否されました。 + 時間内にリンクが完了しませんでした。 + この端末とのリンクはサポートしていません。 + サインアウトした端末で以下のQRコードをスキャンしてください。 + この端末のカメラを使用して、他の端末に表示されているQRコードをスキャンしてください: + %s +\nは空です。 + クライアントの名称、バージョン、URLを記録し、セッションマネージャーでより容易にセッションを認識できるよう設定。 + セッション名を変更 + 絞り込む + 直近のオンライン日時 %1$s + + 使用していない古いセッション(%1$d日以上使用されていません)からサインアウトすることを検討してください。 + + 未認証のセッションを認証するか、サインアウトしてください。 + 未認証・現在のセッション + 未認証・直近のオンライン日時 %1$s + 認証済・直近のオンライン日時 %1$s + 現在のセッションを認証すると、このセッションの認証の状態を確認できます。 + セキュリティーを最大限に高めるには、セッションを認証し、不明なセッションや利用していないセッションからサインアウトしてください。 + Element Callウィジェットを自動で承認し、カメラまたはマイクのアクセス権を付与 + Element Callの権限のショートカットを有効にする + 現在のゲートウェイ:%s + ゲートウェイ + + %d個の方法が見つかりました。 + + フォトライブラリー + %1$s前に更新済 + %1$sまで共有(ライブ) + 他のアンケートを読み込む + + 過去%1$d日に実施されたアンケートはありません。 +\nさらにアンケートを読み込み、前の月のアンケートを表示。 + + + 過去%1$d日に実施中のアンケートはありません。 +\nさらにアンケートを読み込み、前の月のアンケートを表示。 + + 30秒早送り + 30秒巻き戻す + 音声配信を再生または再開 + 音声配信の録音を停止 + 音声配信の録音を一時停止 + %1$s、%2$s、%3$s + 録音をタップして停止または再生 + 非公開で招待が必要なものは表示されていません。 + 下書きを取り消しました + あなたが参加するダイレクトメッセージとルームの他のユーザーは、あなたのセッションの一覧を閲覧できます。 +\n +\nセッションの一覧から、相手はあなたとやり取りしていることを確かめることができます。なお、あなたがここに入力するセッション名は相手に対して表示されます。 + このセッションは暗号化をサポートしていないため、認証できません。 +\n +\nこのセッションでは、暗号化が有効になっているルームに参加することができません。 +\n +\nセキュリティーとプライバシー保護の観点から、暗号化をサポートしているMatrixのクライアントの使用を推奨します。 + 未認証のセッションは、認証情報でログインされていますが、クロス認証は行われていないセッションです。 +\n +\nこれらのセッションは、アカウントの不正使用を示している可能性があるため、注意して確認してください。 + 認証済のセッションは、パスフレーズの入力、または他の認証済のセッションで本人確認を行ったセッションです。 +\n +\n認証済のセッションには、暗号化されたメッセージを復号化する際に使用する全ての鍵が備わっています。また、他のユーザーに対しては、あなたがこのセッションを信頼していることが表示されます。 \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-pl/strings.xml b/libraries/ui-strings/src/main/res/values-pl/strings.xml index 4419187ba5..2bdd6e806d 100644 --- a/libraries/ui-strings/src/main/res/values-pl/strings.xml +++ b/libraries/ui-strings/src/main/res/values-pl/strings.xml @@ -345,7 +345,7 @@ Importuj klucze z lokalnego pliku Importuj Szyfruj wiadomości tylko do zaufanych sesji - Nigdy nie wysyłaj szyfrowanych wiadomości do niezweryfikowanych sesji (bez zielonej tarczy) z tego urządzenia. + Nigdy nie wysyłaj szyfrowanych wiadomości do niezweryfikowanych sesji (bez zielonej tarczy) z tej sesji. Aby sprawdzić czy ta sesja jest zaufana, skontaktuj się z jej właścicielem używając innych form (np. osobiście lub telefonicznie) i zapytaj czy klucz, który widzą w ustawieniach użytkownika dla tego urządzenia pasuje do klucza poniżej: Jeśli klucz pasuje, potwierdź to przyciskiem poniżej. Jeśli nie, to ktoś inny najprawdopodobniej przejmuje lub podszywa się pod tą sesję i powinieneś dodać tę sesję do czarnej listy. W przyszłości proces weryfikacji będzie bardziej skomplikowany. Wyślij naklejkę diff --git a/libraries/ui-strings/src/main/res/values-ru/strings.xml b/libraries/ui-strings/src/main/res/values-ru/strings.xml index 5938200c1e..bf329b89ec 100644 --- a/libraries/ui-strings/src/main/res/values-ru/strings.xml +++ b/libraries/ui-strings/src/main/res/values-ru/strings.xml @@ -2540,7 +2540,7 @@ Домашний сервер не принимает имя пользователя, состоящее только из цифр. Пропустить этот шаг Сохранить и продолжить - Зайдите в настройки чтобы изменить Ваш профиль + Ваши предпочтения были сохранены Выглядит хорошо! ${app_name} также отлично подходит для работы. Ему доверяют самые надёжные организации в мире. Резервная копия имеет действительную подпись для данного пользователя. diff --git a/libraries/ui-strings/src/main/res/values-sk/strings.xml b/libraries/ui-strings/src/main/res/values-sk/strings.xml index c9e92d323b..82deefb371 100644 --- a/libraries/ui-strings/src/main/res/values-sk/strings.xml +++ b/libraries/ui-strings/src/main/res/values-sk/strings.xml @@ -2979,4 +2979,5 @@ Nemôžete spustiť hlasovú správu, pretože práve nahrávate živé vysielanie. Ukončite prosím živé vysielanie, aby ste mohli začať nahrávať hlasovú správu Nemožno spustiť hlasovú správu Chyba pripojenia - nahrávanie pozastavené + Použiť formát riadkového kódu \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-sq/strings.xml b/libraries/ui-strings/src/main/res/values-sq/strings.xml index 374080cb23..447a2b52d7 100644 --- a/libraries/ui-strings/src/main/res/values-sq/strings.xml +++ b/libraries/ui-strings/src/main/res/values-sq/strings.xml @@ -2887,4 +2887,23 @@ S’arrihet të luhet ky transmetim zanor. Nisni një transmetim zanor Shërbyesi juaj Home s’mbulon ende paraqitje rrjedhash. - + Apliko format kodi brendazi + Gabim në sjellje pyetësorësh. + Ngarko më tepër pyetësorë + Shfaqje pyetësorësh + + S’ka pyetësorë të kaluar për ditën e kaluar. +\nQë të shihni pyetësorë për ditët e kaluara, ngarkoni më tepër pyetësorë. + S’ka pyetësorë aktivë për %1$d ditët e kaluara. +\nQë të shihni pyetësorë për ditët e kaluara, ngarkoni më tepër pyetësorë. + + + S’ka pyetësorë aktivë për ditën e kaluar. +\nQë të shihni pyetësorë për ditët e kaluara, ngarkoni më tepër pyetësorë. + S’ka pyetësorë aktivë për%1$d ditët e kaluara. +\nQë të shihni pyetësorë për ditët e kaluara, ngarkoni më tepër pyetësorë. + + Gabim lidhjeje - Incizimi u ndal + S’mund të nisni një mesazh zanor teksa jeni aktualisht duke incizuar një transmetim të drejtpërdrejtë. Ju lutemi, përfundoni transmetimin tuaj të drejtpërdrejtë, që të mund të nisni incizimin e një mesazhi zanor + S’niset dot mesazh zanor + \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-sv/strings.xml b/libraries/ui-strings/src/main/res/values-sv/strings.xml index 877a95f2de..597dd90b2f 100644 --- a/libraries/ui-strings/src/main/res/values-sv/strings.xml +++ b/libraries/ui-strings/src/main/res/values-sv/strings.xml @@ -2898,4 +2898,26 @@ Omröstningshistorik Din hemserver har inte stöd för att lista trådar än. Ja, sluta - + Fel vid hämtning av omröstningar. + Laddar fler omröstning + Visar omröstningar + + Det finns inga aktiva omröstningar från förra dagen. +\nLadda fler omröstningar för att se omröstningar från tidigare dagar. + Det finns inga aktiva omröstningar från senaste %1$d dagarna. +\nLadda fler omröstningar för att se omröstningar från tidigare dagar. + + + Det finns inga omröstningar från förra dagen. +\nLadda fler omröstningar för att se omröstningar från tidigare dagar. + Det finns inga omröstningar från senaste %1$d dagarna. +\nLadda fler omröstningar för att se omröstningar från tidigare dagar. + + På grund av avkrypteringsfel så kanske vissa röster inte räknas + Anslutningsfel - Inspelning pausad + Kan inte spela den här röstsändningen. + Du kan inte påbörja ett röstmeddelande eftersom du för närvarande spelar in en röstsändning. Vänligen avsluta din röstsändning för att börja spela in ett röstmeddelande + Kan inte starta röstsändning + Startade en röstsändning + Använd inline-kodformat + \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-uk/strings.xml b/libraries/ui-strings/src/main/res/values-uk/strings.xml index 0f6027903f..26d38bb324 100644 --- a/libraries/ui-strings/src/main/res/values-uk/strings.xml +++ b/libraries/ui-strings/src/main/res/values-uk/strings.xml @@ -3039,4 +3039,5 @@ Ви не можете розпочати запис голосового повідомлення, оскільки ви записуєте трансляцію наживо. Будь ласка, заверште її, щоб розпочати запис голосового повідомлення Не вдалося розпочати запис голосового повідомлення Помилка з\'єднання - Запис призупинено + Застосовувати вбудований формат коду \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-zh-rCN/strings.xml b/libraries/ui-strings/src/main/res/values-zh-rCN/strings.xml index 1e75540acf..f3b5854afb 100644 --- a/libraries/ui-strings/src/main/res/values-zh-rCN/strings.xml +++ b/libraries/ui-strings/src/main/res/values-zh-rCN/strings.xml @@ -1803,7 +1803,7 @@ %d 个条目 这不是有效的 Matrix QR码 - 扫描二维码 + 扫描QR码 添加人员 邀请朋友 服务器版本 @@ -2819,4 +2819,5 @@ 无法播放此语音广播。 你无法启动语音消息因为你正在录制实时广播。请终止实时广播以开始录制语音消息 无法启动语音消息 - + 结束了投票。 + \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-zh-rTW/strings.xml b/libraries/ui-strings/src/main/res/values-zh-rTW/strings.xml index c650a1e6b2..b3845e550d 100644 --- a/libraries/ui-strings/src/main/res/values-zh-rTW/strings.xml +++ b/libraries/ui-strings/src/main/res/values-zh-rTW/strings.xml @@ -2856,4 +2856,8 @@ 過去 %1$d 天沒有活躍的投票。 \n載入更多投票以檢視過去幾天的投票。 - + 連線錯誤 - 錄製已暫停 + 您無法開始語音訊息,因為您目前正在錄製直播。請結束您的直播以開始錄製語音訊息 + 無法開始語音訊息 + 套用內嵌程式碼格式 + \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values/strings.xml b/libraries/ui-strings/src/main/res/values/strings.xml index e690f06bbb..66c66bbcbe 100644 --- a/libraries/ui-strings/src/main/res/values/strings.xml +++ b/libraries/ui-strings/src/main/res/values/strings.xml @@ -1063,6 +1063,9 @@ Discovery Manage your discovery settings. + Account + Your account details are managed separately at %1$s. + Analytics Send analytics data @@ -3120,6 +3123,7 @@ You are already recording a voice broadcast. Please end your current voice broadcast to start a new one. Unable to play this voice broadcast. Connection error - Recording paused + Unable to decrypt this voice broadcast. %1$s left Stop live broadcasting? @@ -3207,6 +3211,7 @@ Displaying polls Load more polls Error fetching polls. + View poll in timeline Share location @@ -3502,7 +3507,11 @@ Set link Toggle numbered list Toggle bullet list + Indent + Unindent + Toggle quote Apply inline code format + Toggle code block Toggle full screen mode Text diff --git a/plugins/src/main/kotlin/extension/CommonExtension.kt b/plugins/src/main/kotlin/extension/CommonExtension.kt index 615ce6b440..654ea1f55e 100644 --- a/plugins/src/main/kotlin/extension/CommonExtension.kt +++ b/plugins/src/main/kotlin/extension/CommonExtension.kt @@ -43,6 +43,7 @@ fun CommonExtension<*, *, *, *>.androidConfig(project: Project) { lintConfig = File("${project.rootDir}/tools/lint/lint.xml") checkDependencies = true abortOnError = true + ignoreTestFixturesSources = true } } @@ -67,6 +68,7 @@ fun CommonExtension<*, *, *, *>.composeConfig() { // Disabled until lint stops inspecting generated ksp files... // error.add("ComposableLambdaParameterNaming") error.add("ComposableLambdaParameterPosition") + ignoreTestFixturesSources = true } } diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index 710f603cad..8b63c38244 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -54,6 +54,7 @@ fun DependencyHandlerScope.allLibraries() { implementation(project(":libraries:matrixui")) implementation(project(":libraries:core")) implementation(project(":libraries:architecture")) + implementation(project(":libraries:dateformatter")) implementation(project(":libraries:di")) } diff --git a/settings.gradle.kts b/settings.gradle.kts index 990c073700..01ee64a0f7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -42,6 +42,7 @@ include(":libraries:rustsdk") include(":libraries:matrix") include(":libraries:matrixui") include(":libraries:textcomposer") +include(":libraries:dateformatter") include(":libraries:elementresources") include(":libraries:ui-strings") include(":libraries:testtags") diff --git a/tests/uitests/src/main/kotlin/io/element/android/tests/uitests/ShowkaseButton.kt b/tests/uitests/src/main/kotlin/io/element/android/tests/uitests/ShowkaseButton.kt index 9ae1b78577..441a7cbd78 100644 --- a/tests/uitests/src/main/kotlin/io/element/android/tests/uitests/ShowkaseButton.kt +++ b/tests/uitests/src/main/kotlin/io/element/android/tests/uitests/ShowkaseButton.kt @@ -20,10 +20,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -32,6 +28,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.theme.components.Text @Composable fun ShowkaseButton( @@ -59,8 +61,15 @@ fun ShowkaseButton( } } -@Preview(group = "Buttons", name = "Showkase button") +@Preview @Composable -fun ShowkaseButtonPreview() { +fun ShowkaseButtonLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun ShowkaseButtonDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { ShowkaseButton() } diff --git a/tests/uitests/src/test/kotlin/io/element/android/tests/uitests/ScreenshotTest.kt b/tests/uitests/src/test/kotlin/io/element/android/tests/uitests/ScreenshotTest.kt index 1c26565acf..1ccb0d76b2 100644 --- a/tests/uitests/src/test/kotlin/io/element/android/tests/uitests/ScreenshotTest.kt +++ b/tests/uitests/src/test/kotlin/io/element/android/tests/uitests/ScreenshotTest.kt @@ -36,7 +36,7 @@ import app.cash.paparazzi.Paparazzi import com.airbnb.android.showkase.models.Showkase import com.google.testing.junit.testparameterinjector.TestParameter import com.google.testing.junit.testparameterinjector.TestParameterInjector -import io.element.android.libraries.designsystem.ElementXTheme +import io.element.android.libraries.designsystem.theme.ElementTheme import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -75,7 +75,6 @@ class ScreenshotTest { @TestParameter(valuesProvider = PreviewProvider::class) componentTestPreview: TestPreview, @TestParameter baseDeviceConfig: BaseDeviceConfig, @TestParameter(value = ["1.0"/*, "1.5"*/]) fontScale: Float, - @TestParameter(value = ["light", "dark"]) theme: String, @TestParameter(value = ["en" /*"fr", "de", "ru"*/]) localeStr: String, ) { paparazzi.unsafeUpdateConfig( @@ -101,7 +100,7 @@ class ScreenshotTest { override fun getOnBackPressedDispatcher() = OnBackPressedDispatcher() } ) { - ElementXTheme(darkTheme = (theme == "dark")) { + ElementTheme { Box(modifier = Modifier.background(MaterialTheme.colorScheme.background)) { componentTestPreview.Content() } diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[Color_Material Design_DarkGrey,NEXUS_5,1,light,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Color_Material Design_DarkGrey,NEXUS_5,1,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[Color_Material Design_DarkGrey,NEXUS_5,1,light,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Color_Material Design_DarkGrey,NEXUS_5,1,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[Color_Material Design_LightGrey,NEXUS_5,1,light,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Color_Material Design_LightGrey,NEXUS_5,1,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[Color_Material Design_LightGrey,NEXUS_5,1,light,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Color_Material Design_LightGrey,NEXUS_5,1,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_Body Large,NEXUS_5,1,light,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_Body Large,NEXUS_5,1,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_Body Large,NEXUS_5,1,light,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_Body Large,NEXUS_5,1,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_Body1,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_Body1,NEXUS_5,1,en].png new file mode 100644 index 0000000000..e2d264f3fd --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_Body1,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e60a495255bf6048af20f1cd7365caf6ca6f5d9e97bbf1519ce469f3cccec958 +size 6064 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_BodySmall,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_BodySmall,NEXUS_5,1,en].png new file mode 100644 index 0000000000..058c257e7d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_BodySmall,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3025338830e0e20781a727ef61751be208d2c633e271f42fe91dbf89df43dbf5 +size 6681 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_H1,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_H1,NEXUS_5,1,en].png new file mode 100644 index 0000000000..69c38b48fe --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_H1,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c68a348292da117047095d945c7b3fa764e4894fbc25354a88607af33378b90 +size 4775 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_Headline Large,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_Headline Large,NEXUS_5,1,en].png new file mode 100644 index 0000000000..eb99d57c55 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_Headline Large,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a8960ee49f84a98f0c309ea2d3cc208bfdf493c8843066830ee228e0c3b25634 +size 9695 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_Headline Medium,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_Headline Medium,NEXUS_5,1,en].png new file mode 100644 index 0000000000..173136b77d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_Headline Medium,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dcfab4f43c1a796c9166116285353c4351182db3d5417f657c8a48e9798c6e87 +size 9589 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_Headline Small,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_Headline Small,NEXUS_5,1,en].png new file mode 100644 index 0000000000..a8d282ced2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_Headline Small,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12fdbd29d34a17ceec91dd37fddb46560a9047df5a4878491443e75c1fb2ad44 +size 8214 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_bodyMedium,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_bodyMedium,NEXUS_5,1,en].png new file mode 100644 index 0000000000..b282061ed3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_bodyMedium,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5334cd5b273fc2e6ac65c730bc88adf74fa6b89fda0e76009d35a75bf2790559 +size 7728 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_titleMedium,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_titleMedium,NEXUS_5,1,en].png new file mode 100644 index 0000000000..935ccb6489 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_titleMedium,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:310e8dfa7d52b5ee835243b1025a7e20ec3fb7dafee62c230c870720e91cf897 +size 7044 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_titleSmall,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_titleSmall,NEXUS_5,1,en].png new file mode 100644 index 0000000000..6316774563 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_titleSmall,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c4cbc97971b8a1561c232f14cb8600fea59527e7315bca9d225dde13cb1edd9 +size 6223 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..b8fd07a2b9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c434e7bb1316fdd1668c9d41675ff7fe1acc62dfdc6c64e96e485155a51befc4 +size 27698 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.login.changeserver_null_DefaultGroup_ChangeServerContentPreview_,NEXUS_5,1,light,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_,NEXUS_5,1,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.login.changeserver_null_DefaultGroup_ChangeServerContentPreview_,NEXUS_5,1,light,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_,NEXUS_5,1,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.root_null_DefaultGroup_LoginRootScreenDarkPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.root_null_DefaultGroup_LoginRootScreenDarkPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..640514fca1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.root_null_DefaultGroup_LoginRootScreenDarkPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7393dde7da886bec54405dc602e6584eca8d8617f3f28d9b00e708b0381f3c2b +size 25315 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.login.root_null_DefaultGroup_LoginContentPreview_,NEXUS_5,1,light,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.root_null_DefaultGroup_LoginRootScreenLightPreview_,NEXUS_5,1,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.login.root_null_DefaultGroup_LoginContentPreview_,NEXUS_5,1,light,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.root_null_DefaultGroup_LoginRootScreenLightPreview_,NEXUS_5,1,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.logout_null_DefaultGroup_LogoutPreferenceViewDarkPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.logout_null_DefaultGroup_LogoutPreferenceViewDarkPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..92c0633a91 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.logout_null_DefaultGroup_LogoutPreferenceViewDarkPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:155e39fcadcc8bc039267a8b0dca338a92bcb3b4b23ab55a0cd9f3ef91c729fc +size 8888 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.logout_null_DefaultGroup_LogoutContentPreview_,NEXUS_5,1,light,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.logout_null_DefaultGroup_LogoutPreferenceViewLightPreview_,NEXUS_5,1,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.logout_null_DefaultGroup_LogoutContentPreview_,NEXUS_5,1,light,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.logout_null_DefaultGroup_LogoutPreferenceViewLightPreview_,NEXUS_5,1,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline.components_null_DefaultGroup_MatrixUserRowDarkPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline.components_null_DefaultGroup_MatrixUserRowDarkPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..58ab573693 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline.components_null_DefaultGroup_MatrixUserRowDarkPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef86e92ea3caad3687d66458e3b0832361808692783671db54dd321d61564792 +size 5836 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline.components_null_DefaultGroup_MatrixUserRowLightPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline.components_null_DefaultGroup_MatrixUserRowLightPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..9067e20c9d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline.components_null_DefaultGroup_MatrixUserRowLightPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:674265aa2728d2f1724abcf600a573307d69d8dd77212eaf269176e56dc82fd5 +size 5610 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenDarkPreview__0,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenDarkPreview__0,NEXUS_5,1,en].png new file mode 100644 index 0000000000..0aa521f943 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenDarkPreview__0,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7be62fbfef658b54e9a2fea2b66cae87de145f1dd8dd70baa4828d1c2a8c9c96 +size 31637 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenDarkPreview__1,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenDarkPreview__1,NEXUS_5,1,en].png new file mode 100644 index 0000000000..8548e31a8c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenDarkPreview__1,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ffbfcdf2cd238cd58bac1cbc141be5aa7e74184c4b35018777552d6956e17263 +size 43423 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenDarkPreview__2,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenDarkPreview__2,NEXUS_5,1,en].png new file mode 100644 index 0000000000..556e55dcc1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenDarkPreview__2,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49fb267e9136c8b78f083156681b9432c757af755a2161cfdea42df43371e645 +size 33431 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenDarkPreview__3,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenDarkPreview__3,NEXUS_5,1,en].png new file mode 100644 index 0000000000..ea261625d8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenDarkPreview__3,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa2177214c486e5af577c7d001bf92f1fbe7e1b68adcef3c2d7b91fe1d95a864 +size 55533 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenDarkPreview__4,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenDarkPreview__4,NEXUS_5,1,en].png new file mode 100644 index 0000000000..a23377387a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenDarkPreview__4,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ffb69c688191b581c220d2b1b4f89be74a59496caaf39a1e54a3d5ddc586b319 +size 29603 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenDarkPreview__5,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenDarkPreview__5,NEXUS_5,1,en].png new file mode 100644 index 0000000000..47a852803b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenDarkPreview__5,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:461792b0350436f4dd3527243b8a8fb57d619cb94c6f49db93462675c1ada886 +size 51009 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__0,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__0,NEXUS_5,1,en].png new file mode 100644 index 0000000000..3d1094d520 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__0,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3697be1c70d071df9f1af57085d31a44a175b323602bbbe00dea091757d5e06d +size 31265 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__1,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__1,NEXUS_5,1,en].png new file mode 100644 index 0000000000..77adf1dcdc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__1,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37461ec9e58f5999e5d0bc0810a84131ec5faaec2b7e99359b36f05d8865d3d2 +size 41757 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__2,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__2,NEXUS_5,1,en].png new file mode 100644 index 0000000000..9509ba2522 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__2,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b4489e2f3f326f952d4680adcb14e88174628454bd3b3503864166074c5ca5c +size 33177 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__3,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__3,NEXUS_5,1,en].png new file mode 100644 index 0000000000..88aa21ce03 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__3,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ee3a40f5c8131f588642420d4e682f6ed0a00d6faf3d793b249e28c43b09e9c +size 52914 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__4,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__4,NEXUS_5,1,en].png new file mode 100644 index 0000000000..68a59311cc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__4,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:154c03f8e6eb6be2cb26fe077b4e6397df13173fe6bfcd3f23bc8287a37921b7 +size 29275 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__5,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__5,NEXUS_5,1,en].png new file mode 100644 index 0000000000..8d8bc4078e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__5,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a9a1a9ac69f15110934ae2de3ce55d7c1d3027079e68ecdf5dcb2861863ce8e +size 49057 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.root_null_DefaultGroup_PreferencesRootViewDarkPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.root_null_DefaultGroup_PreferencesRootViewDarkPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..80c9003f06 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.root_null_DefaultGroup_PreferencesRootViewDarkPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55f4d1755b6692051b0fcdf7014cf567cd924dbebed18d22999a6472618a44c2 +size 26524 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.root_null_DefaultGroup_PreferencesRootViewLightPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.root_null_DefaultGroup_PreferencesRootViewLightPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..4ee53a9091 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.root_null_DefaultGroup_PreferencesRootViewLightPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:30a23b12d9f25e216e15df05ea4be2ca5d10d16b1c4be25fee876a255c6a19b3 +size 26287 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.bugreport_null_DefaultGroup_BugReportViewDarkPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.bugreport_null_DefaultGroup_BugReportViewDarkPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..547f890601 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.bugreport_null_DefaultGroup_BugReportViewDarkPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a73f66fa8ddb5c69580e59bf86364f430cf89c0710bcba98b8d8ccf605d16c2 +size 50660 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.rageshake.bugreport_null_DefaultGroup_BugReportContentPreview_,NEXUS_5,1,light,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.bugreport_null_DefaultGroup_BugReportViewLightPreview_,NEXUS_5,1,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.rageshake.bugreport_null_DefaultGroup_BugReportContentPreview_,NEXUS_5,1,light,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.bugreport_null_DefaultGroup_BugReportViewLightPreview_,NEXUS_5,1,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.crash.ui_null_DefaultGroup_CrashDetectionContentDarkPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.crash.ui_null_DefaultGroup_CrashDetectionContentDarkPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..374ed70e4e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.crash.ui_null_DefaultGroup_CrashDetectionContentDarkPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9ab15f317d30dbbd2dd1efae4e364e05e0523790b506d970465879f71e41dec +size 28995 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.crash.ui_null_DefaultGroup_CrashDetectionContentLightPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.crash.ui_null_DefaultGroup_CrashDetectionContentLightPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..35277743a5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.crash.ui_null_DefaultGroup_CrashDetectionContentLightPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:971012a67b23f0c07957b439af663ed44b47310089e20374bee1d8c4712bd209 +size 29213 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.detection_null_DefaultGroup_RageshakeDialogContentDarkPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.detection_null_DefaultGroup_RageshakeDialogContentDarkPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..396101cd5f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.detection_null_DefaultGroup_RageshakeDialogContentDarkPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:643bb6ca2dba3bd686d633beb103cd92a70547486c397d6472b1488d57b027c9 +size 33156 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.detection_null_DefaultGroup_RageshakeDialogContentLightPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.detection_null_DefaultGroup_RageshakeDialogContentLightPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..082816d1dc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.detection_null_DefaultGroup_RageshakeDialogContentLightPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:127f77b02e010acd936c86d1b54c25e152e3bfe660a0cf07c674bd17569a72cc +size 33163 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.preferences_null_DefaultGroup_RageshakePreferencesViewDarkPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.preferences_null_DefaultGroup_RageshakePreferencesViewDarkPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..1d080b2c8b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.preferences_null_DefaultGroup_RageshakePreferencesViewDarkPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c385c5b124217368b054e85e8313a0dac30274ea6fc08f9a71f60e80f547df0 +size 22589 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.rageshake.preferences_null_DefaultGroup_RageshakePreferencesViewPreview_,NEXUS_5,1,light,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.preferences_null_DefaultGroup_RageshakePreferencesViewLightPreview_,NEXUS_5,1,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.rageshake.preferences_null_DefaultGroup_RageshakePreferencesViewPreview_,NEXUS_5,1,light,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.preferences_null_DefaultGroup_RageshakePreferencesViewLightPreview_,NEXUS_5,1,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.preferences_null_DefaultGroup_RageshakePreferencesViewNotSupportedDarkPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.preferences_null_DefaultGroup_RageshakePreferencesViewNotSupportedDarkPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..c408edcdc4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.preferences_null_DefaultGroup_RageshakePreferencesViewNotSupportedDarkPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eac040a95a0e551d61fb0a14c26190304ac97b8a575a0911812f3fdade88f0e6 +size 21014 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.rageshake.preferences_null_DefaultGroup_RageshakePreferenceNotSupportedPreview_,NEXUS_5,1,light,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.preferences_null_DefaultGroup_RageshakePreferencesViewNotSupportedLightPreview_,NEXUS_5,1,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.rageshake.preferences_null_DefaultGroup_RageshakePreferenceNotSupportedPreview_,NEXUS_5,1,light,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.preferences_null_DefaultGroup_RageshakePreferencesViewNotSupportedLightPreview_,NEXUS_5,1,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist_null_DefaultGroup_RoomListViewDarkPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist_null_DefaultGroup_RoomListViewDarkPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..877935d457 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist_null_DefaultGroup_RoomListViewDarkPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c086ed8fea4f3369a93b5f00e76b8ecdfe4e7874042d277583987db0003bca4b +size 34956 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist_null_DefaultGroup_RoomListViewLightPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist_null_DefaultGroup_RoomListViewLightPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..517438eb48 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomlist_null_DefaultGroup_RoomListViewLightPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab3d7fb8f67d178af44db4bae63f606eb88612c18b84d989c382fc17b9df068b +size 32363 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.avatar_null_DefaultGroup_AvatarDarkPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.avatar_null_DefaultGroup_AvatarDarkPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..e33d3ad878 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.avatar_null_DefaultGroup_AvatarDarkPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f59bf1e9e6695cab308f19400f8a4c58114b3f7e1a97e649c263e3b5b07b7de3 +size 8527 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.avatar_null_DefaultGroup_InitialsAvatarPreview_,NEXUS_5,1,light,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.avatar_null_DefaultGroup_AvatarLightPreview_,NEXUS_5,1,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.avatar_null_DefaultGroup_InitialsAvatarPreview_,NEXUS_5,1,light,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.avatar_null_DefaultGroup_AvatarLightPreview_,NEXUS_5,1,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.dialogs_null_DefaultGroup_ConfirmationDialogDarkPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.dialogs_null_DefaultGroup_ConfirmationDialogDarkPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..886a306c07 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.dialogs_null_DefaultGroup_ConfirmationDialogDarkPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8988d36c35a1b134e878a7276d18cf2cb7779ff6ceb4a7436cfa9aa2ad7bed87 +size 18722 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.dialogs_null_DefaultGroup_ConfirmationDialogLightPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.dialogs_null_DefaultGroup_ConfirmationDialogLightPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..7720c31f21 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.dialogs_null_DefaultGroup_ConfirmationDialogLightPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf1eaf7c71a6bd5855b40efc0852cf023c790142502fa1149878c8d0dd209be3 +size 18495 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.dialogs_null_DefaultGroup_ErrorDialogDarkPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.dialogs_null_DefaultGroup_ErrorDialogDarkPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..4fa59da9a0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.dialogs_null_DefaultGroup_ErrorDialogDarkPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eaf83db86c387f95244f106ee642a0524c24030f61d62f3cba3566fb37c00dcf +size 12280 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.dialogs_null_DefaultGroup_ErrorDialogLightPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.dialogs_null_DefaultGroup_ErrorDialogLightPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..2a7fdb03fc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.dialogs_null_DefaultGroup_ErrorDialogLightPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3766ad81b5bb9b1e674ad8ad6efc481a95bfd655195d5c8b814ba2f16594640 +size 12374 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_DefaultGroup_PreferenceCategoryDarkPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_DefaultGroup_PreferenceCategoryDarkPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..a8a3738f0e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_DefaultGroup_PreferenceCategoryDarkPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f67cd11f58cba1e7de19aab0c35ca0638b4c0f0dee373d4fc7155e903285e62 +size 15843 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.preferences_null_DefaultGroup_PreferenceCategoryPreview_,NEXUS_5,1,light,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_DefaultGroup_PreferenceCategoryLightPreview_,NEXUS_5,1,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.preferences_null_DefaultGroup_PreferenceCategoryPreview_,NEXUS_5,1,light,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_DefaultGroup_PreferenceCategoryLightPreview_,NEXUS_5,1,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_DefaultGroup_PreferenceSlideDarkPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_DefaultGroup_PreferenceSlideDarkPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..1bf464f832 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_DefaultGroup_PreferenceSlideDarkPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f5141e6fedfb2e6c22abbe9efd30258223283ee470d79d3aa59bbf58bc1ce900 +size 8904 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.preferences_null_DefaultGroup_PreferenceSlidePreview_,NEXUS_5,1,light,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_DefaultGroup_PreferenceSlideLightPreview_,NEXUS_5,1,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.preferences_null_DefaultGroup_PreferenceSlidePreview_,NEXUS_5,1,light,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_DefaultGroup_PreferenceSlideLightPreview_,NEXUS_5,1,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_DefaultGroup_PreferenceSwitchDarkPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_DefaultGroup_PreferenceSwitchDarkPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..a160900ada --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_DefaultGroup_PreferenceSwitchDarkPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb61f940a3a773ed05e515807797b88eb4a4f38ffc6f99a5c5816bd5d403fbdd +size 7546 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.preferences_null_DefaultGroup_PreferenceSwitchPreview_,NEXUS_5,1,light,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_DefaultGroup_PreferenceSwitchLightPreview_,NEXUS_5,1,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.preferences_null_DefaultGroup_PreferenceSwitchPreview_,NEXUS_5,1,light,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_DefaultGroup_PreferenceSwitchLightPreview_,NEXUS_5,1,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_DefaultGroup_PreferenceTextDarkPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_DefaultGroup_PreferenceTextDarkPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..49d48a0ead --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_DefaultGroup_PreferenceTextDarkPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55d597b5b3bb53c1f7fcd96104548c6b93de4a4ef464302c73ba04c6a6d85afd +size 6117 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.preferences_null_DefaultGroup_PreferenceTextPreview_,NEXUS_5,1,light,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_DefaultGroup_PreferenceTextLightPreview_,NEXUS_5,1,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.preferences_null_DefaultGroup_PreferenceTextPreview_,NEXUS_5,1,light,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_DefaultGroup_PreferenceTextLightPreview_,NEXUS_5,1,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.preferences_null_DefaultGroup_PreferenceScreenPreview_,NEXUS_5,1,dark,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_DefaultGroup_PreferenceViewDarkPreview_,NEXUS_5,1,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.preferences_null_DefaultGroup_PreferenceScreenPreview_,NEXUS_5,1,dark,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_DefaultGroup_PreferenceViewDarkPreview_,NEXUS_5,1,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.preferences_null_DefaultGroup_PreferenceScreenPreview_,NEXUS_5,1,light,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_DefaultGroup_PreferenceViewLightPreview_,NEXUS_5,1,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.preferences_null_DefaultGroup_PreferenceScreenPreview_,NEXUS_5,1,light,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_DefaultGroup_PreferenceViewLightPreview_,NEXUS_5,1,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components_null_DefaultGroup_LabelledCheckboxDarkPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components_null_DefaultGroup_LabelledCheckboxDarkPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..fb4edaf477 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components_null_DefaultGroup_LabelledCheckboxDarkPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bcbf114b16949dc147ed8437ad80470635c44716e9938b441d8ec2bd453bef66 +size 7801 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components_null_DefaultGroup_LabelledCheckboxPreview_,NEXUS_5,1,light,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components_null_DefaultGroup_LabelledCheckboxLightPreview_,NEXUS_5,1,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components_null_DefaultGroup_LabelledCheckboxPreview_,NEXUS_5,1,light,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components_null_DefaultGroup_LabelledCheckboxLightPreview_,NEXUS_5,1,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components_null_DefaultGroup_ProgressDialogDarkPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components_null_DefaultGroup_ProgressDialogDarkPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..91688bd758 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components_null_DefaultGroup_ProgressDialogDarkPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:17d4e90669b132283c8be57ac9e35fda09263f28f5f1075a3f8878ef5357cff3 +size 9648 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components_null_DefaultGroup_ProgressDialogLightPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components_null_DefaultGroup_ProgressDialogLightPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..bd42f78314 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components_null_DefaultGroup_ProgressDialogLightPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:473beb3dcaeb9325aa92f98582b322fe74e2026659fe3cbdff793ee68246e7c1 +size 9923 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_ButtonsDarkPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_ButtonsDarkPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..0eadae1fce --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_ButtonsDarkPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dce173f40598eb7ee75febe9135c6563ef7b35e6861dcf6f2d09c0af5febebb8 +size 13450 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_ButtonsLightPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_ButtonsLightPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..b46401dbe3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_ButtonsLightPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:52bbba0b16671da66ae06ad549151f794023043025971729ae1f5b9d6bb8f4ae +size 12689 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_CheckboxesDarkPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_CheckboxesDarkPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..4ee8b68466 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_CheckboxesDarkPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72b4f5c01f5236b7141b222cf8414f303ac60b74c386d2c71fa6348b2ab1173b +size 7506 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_CheckboxesLightPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_CheckboxesLightPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..3fe644f51e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_CheckboxesLightPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3fc3903727b9ff1941ae59bf0566ac7485705813606550ad8ac615726e4efab +size 6121 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_DividerDarkPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_DividerDarkPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..8d3376900b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_DividerDarkPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:274521bb420e73dcce6bca9d4328909d896998e4fdf3cebe79dd7c6c4873db45 +size 4469 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_DividerLightPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_DividerLightPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..3ffdafd3ac --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_DividerLightPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e3a82fb55f7a5be9ddbb5a8146d4fe53ad6111e21b40912428fd95dde85346c +size 4470 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_OutlinedTextFieldsDarkPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_OutlinedTextFieldsDarkPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..fb679b5908 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_OutlinedTextFieldsDarkPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e842ff561687f991106043a4f6b51455bd917db879ef1d624bd59df155ac61dd +size 21932 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_OutlinedTextFieldsLightPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_OutlinedTextFieldsLightPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..36cc8f6ff1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_OutlinedTextFieldsLightPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c0ef4d55a1a21fdcec03d5f95871cb0bea94350cbfc5ed7a50cc343df7898ec +size 21054 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_SlidersDarkPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_SlidersDarkPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..0261662a40 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_SlidersDarkPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73a3e053cfbc83db5222b89209597ba56c3797da224b2e4a70286740c686c2ab +size 6388 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_SlidersLightPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_SlidersLightPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..d7d6b5016d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_SlidersLightPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b9632ce17a34536ad14ada77f5852c43d0628444a2ee7a1b129432d98e92399 +size 6290 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_TextDarkPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_TextDarkPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..9a2284a7ab --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_TextDarkPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5127c51e1eb76ceb0620073449a5dde0805f9ba3d0fae6368aaf15473e837c1f +size 104643 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_TextLightPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_TextLightPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..cf5702384e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_TextLightPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2239989d1ea3db71952a74282d79a08c4e2dbc321c1cea6478b7cf25957bfecb +size 102883 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme_null_DefaultGroup_ColorAliasesDarkPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme_null_DefaultGroup_ColorAliasesDarkPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..f30ca8538a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme_null_DefaultGroup_ColorAliasesDarkPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75ed390c5f71cdfebf87f722b3d377f6c1a047d2b3791812516ddad2c2326407 +size 30804 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme_null_DefaultGroup_ColorAliasesLightPreview_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme_null_DefaultGroup_ColorAliasesLightPreview_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..ba868312fc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme_null_DefaultGroup_ColorAliasesLightPreview_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f388b7b6dce61610a5bb3d839f3be11be19363cf734a3f70032354d33f2b0db +size 32087 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme_null_DefaultGroup_ColorsSchemePreviewDark_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme_null_DefaultGroup_ColorsSchemePreviewDark_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..62ad4ddd72 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme_null_DefaultGroup_ColorsSchemePreviewDark_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:425c923cf0e7993816c237d22a50c32962dc58e42a56bba788742f5b5ac2fdc6 +size 115420 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme_null_DefaultGroup_ColorsSchemePreviewLight_,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme_null_DefaultGroup_ColorsSchemePreviewLight_,NEXUS_5,1,en].png new file mode 100644 index 0000000000..f4f1aeaa13 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme_null_DefaultGroup_ColorsSchemePreviewLight_,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c25192e1654d49d72af7591700ee88303562812de23ec1118e8f27d852da8a9 +size 116950 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.tests.uitests_null_DefaultGroup_ShowkaseButtonDarkPreview_null,NEXUS_5,1,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.tests.uitests_null_DefaultGroup_ShowkaseButtonDarkPreview_null,NEXUS_5,1,en].png new file mode 100644 index 0000000000..d3e7ef562a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.tests.uitests_null_DefaultGroup_ShowkaseButtonDarkPreview_null,NEXUS_5,1,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0eebdfaaf064be053b22b548fdf45fe218a341fa8718d204e8c2463e45a374bb +size 10649 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.tests.uitests_null_Buttons_Showkasebutton_null,NEXUS_5,1,light,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.tests.uitests_null_DefaultGroup_ShowkaseButtonLightPreview_null,NEXUS_5,1,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.tests.uitests_null_Buttons_Showkasebutton_null,NEXUS_5,1,light,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.tests.uitests_null_DefaultGroup_ShowkaseButtonLightPreview_null,NEXUS_5,1,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[Color_Material Design_DarkGrey,NEXUS_5,1,dark,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[Color_Material Design_DarkGrey,NEXUS_5,1,dark,en].png deleted file mode 100644 index 44aabba74b..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[Color_Material Design_DarkGrey,NEXUS_5,1,dark,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:457c973681addb003e8f84ce3706361cfe0bb88845799838971da910476a4ba8 -size 4371 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[Color_Material Design_LightGrey,NEXUS_5,1,dark,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[Color_Material Design_LightGrey,NEXUS_5,1,dark,en].png deleted file mode 100644 index 35c67a316c..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[Color_Material Design_LightGrey,NEXUS_5,1,dark,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f2c05b0f79fea3df1147a454202f1ca2973d48939e4fc296be171ad98f272333 -size 4371 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_Body Large,NEXUS_5,1,dark,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_Body Large,NEXUS_5,1,dark,en].png deleted file mode 100644 index 786ac1495b..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_Body Large,NEXUS_5,1,dark,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1d1b2622226f026d0231020c389b6e99a1c50cd0bc84d77df6060e390c8521fb -size 7056 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_Headline Small,NEXUS_5,1,dark,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_Headline Small,NEXUS_5,1,dark,en].png deleted file mode 100644 index ab15d2cfd5..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_Headline Small,NEXUS_5,1,dark,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:de0b7b271b6057db6ef81888356583bdc3432e4f26a52c58586f88043de20021 -size 9476 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_Headline Small,NEXUS_5,1,light,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_Headline Small,NEXUS_5,1,light,en].png deleted file mode 100644 index 02e426ff7b..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_Headline Small,NEXUS_5,1,light,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d02db7b3881676d8b194e6871e178af8af99e09bd5cc15b865a25bd405e3fc33 -size 9445 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.avatar_null_DefaultGroup_InitialsAvatarPreview_,NEXUS_5,1,dark,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.avatar_null_DefaultGroup_InitialsAvatarPreview_,NEXUS_5,1,dark,en].png deleted file mode 100644 index 2c7e9fbd5d..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.avatar_null_DefaultGroup_InitialsAvatarPreview_,NEXUS_5,1,dark,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9c0fb883d948d8177cd32eb81b24e7b104acd7b6b120b73d6693088f947701da -size 8295 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.dialogs_null_DefaultGroup_ConfirmationDialogPreview_,NEXUS_5,1,dark,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.dialogs_null_DefaultGroup_ConfirmationDialogPreview_,NEXUS_5,1,dark,en].png deleted file mode 100644 index 29165db48c..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.dialogs_null_DefaultGroup_ConfirmationDialogPreview_,NEXUS_5,1,dark,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f6d88788a186b08d8e2834c39efb1c6476cf51aec70aa08dc806eb69e2d98d9a -size 20004 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.dialogs_null_DefaultGroup_ConfirmationDialogPreview_,NEXUS_5,1,light,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.dialogs_null_DefaultGroup_ConfirmationDialogPreview_,NEXUS_5,1,light,en].png deleted file mode 100644 index 4193346f3c..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.dialogs_null_DefaultGroup_ConfirmationDialogPreview_,NEXUS_5,1,light,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:45860d45aefad7cfd7d7b9f92146799860a5ede7e815d5b28b606fc566b3838c -size 18344 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.dialogs_null_DefaultGroup_ErrorDialogPreview_,NEXUS_5,1,dark,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.dialogs_null_DefaultGroup_ErrorDialogPreview_,NEXUS_5,1,dark,en].png deleted file mode 100644 index 8bba8846db..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.dialogs_null_DefaultGroup_ErrorDialogPreview_,NEXUS_5,1,dark,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:39d924e3718d7f045c15888373a96ec85d4afd0ef405bf25edc453fddaacdc55 -size 13082 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.dialogs_null_DefaultGroup_ErrorDialogPreview_,NEXUS_5,1,light,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.dialogs_null_DefaultGroup_ErrorDialogPreview_,NEXUS_5,1,light,en].png deleted file mode 100644 index 49b0df4d6f..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.dialogs_null_DefaultGroup_ErrorDialogPreview_,NEXUS_5,1,light,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5e5bf72f9d90cbe4b4136a4a2e30168895a3354c6f35ed1d4fc77bfe2987fdaf -size 12565 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.preferences_null_DefaultGroup_PreferenceCategoryPreview_,NEXUS_5,1,dark,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.preferences_null_DefaultGroup_PreferenceCategoryPreview_,NEXUS_5,1,dark,en].png deleted file mode 100644 index 2d55ca4648..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.preferences_null_DefaultGroup_PreferenceCategoryPreview_,NEXUS_5,1,dark,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4e23f8342179fc1a88c278f8e81ddb026034958a6b2fdbaae06ff26dc282d483 -size 11654 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.preferences_null_DefaultGroup_PreferenceSlidePreview_,NEXUS_5,1,dark,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.preferences_null_DefaultGroup_PreferenceSlidePreview_,NEXUS_5,1,dark,en].png deleted file mode 100644 index 302811af53..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.preferences_null_DefaultGroup_PreferenceSlidePreview_,NEXUS_5,1,dark,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:eb61547b9e5b4666abd74145086ae2ee846dced3099f9b36dd2534047caf3266 -size 8465 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.preferences_null_DefaultGroup_PreferenceSwitchPreview_,NEXUS_5,1,dark,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.preferences_null_DefaultGroup_PreferenceSwitchPreview_,NEXUS_5,1,dark,en].png deleted file mode 100644 index 707aefe0fb..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.preferences_null_DefaultGroup_PreferenceSwitchPreview_,NEXUS_5,1,dark,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d5db2a9a3f2d9bec3d74b45892ee453c51afc3b3b41d6ec1624d72a8377176b4 -size 7095 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.preferences_null_DefaultGroup_PreferenceTextPreview_,NEXUS_5,1,dark,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.preferences_null_DefaultGroup_PreferenceTextPreview_,NEXUS_5,1,dark,en].png deleted file mode 100644 index 2fd0494ca5..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components.preferences_null_DefaultGroup_PreferenceTextPreview_,NEXUS_5,1,dark,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b9a36ce6978aa3182e519789a8ea7e1399ae63da630fa7f6727248ba8ba010f7 -size 4837 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components_null_DefaultGroup_LabelledCheckboxPreview_,NEXUS_5,1,dark,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components_null_DefaultGroup_LabelledCheckboxPreview_,NEXUS_5,1,dark,en].png deleted file mode 100644 index 9aa044dfae..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components_null_DefaultGroup_LabelledCheckboxPreview_,NEXUS_5,1,dark,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f331a07e5b56c550023b9c4eff3afd71fece8dd4d649fd1e2aaa178c27557fda -size 5007 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components_null_DefaultGroup_ProgressDialogPreview_,NEXUS_5,1,dark,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components_null_DefaultGroup_ProgressDialogPreview_,NEXUS_5,1,dark,en].png deleted file mode 100644 index 9c2e5c3087..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components_null_DefaultGroup_ProgressDialogPreview_,NEXUS_5,1,dark,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e302ba0695a7663e1abcb98d47cecf5b78968d76202841f9e251313d3dd8c579 -size 10232 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components_null_DefaultGroup_ProgressDialogPreview_,NEXUS_5,1,light,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components_null_DefaultGroup_ProgressDialogPreview_,NEXUS_5,1,light,en].png deleted file mode 100644 index 2a54849c8d..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.designsystem.components_null_DefaultGroup_ProgressDialogPreview_,NEXUS_5,1,light,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:08918409e8daa9f2d3498fe9f463323e737e59ec8472a50b6bf5ec13a1abacfc -size 9576 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.login.changeserver_null_DefaultGroup_ChangeServerContentPreview_,NEXUS_5,1,dark,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.login.changeserver_null_DefaultGroup_ChangeServerContentPreview_,NEXUS_5,1,dark,en].png deleted file mode 100644 index f168f5fb81..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.login.changeserver_null_DefaultGroup_ChangeServerContentPreview_,NEXUS_5,1,dark,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0689e18b72a961ecbea6011c73568703ba255375fdb9bbe34b2e94f08c379cc7 -size 27841 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.login.root_null_DefaultGroup_LoginContentPreview_,NEXUS_5,1,dark,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.login.root_null_DefaultGroup_LoginContentPreview_,NEXUS_5,1,dark,en].png deleted file mode 100644 index 815940ac34..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.login.root_null_DefaultGroup_LoginContentPreview_,NEXUS_5,1,dark,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bc2f6c7463b55716720344a2e873774ea09d98717add30b748a54f1e0374ec04 -size 24751 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.logout_null_DefaultGroup_LogoutContentPreview_,NEXUS_5,1,dark,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.logout_null_DefaultGroup_LogoutContentPreview_,NEXUS_5,1,dark,en].png deleted file mode 100644 index 89df9fb9e1..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.logout_null_DefaultGroup_LogoutContentPreview_,NEXUS_5,1,dark,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:accc42a849df3c0d8a16de0e21842139558d8c68cdcf9efcccf6b58a7cb3b4b3 -size 4742 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__0,NEXUS_5,1,dark,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__0,NEXUS_5,1,dark,en].png deleted file mode 100644 index 4676f3504b..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__0,NEXUS_5,1,dark,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3d5fb95fb866a51e2dbc26239dc70e37a92bddc79d4edc027226948dfe323312 -size 30189 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__0,NEXUS_5,1,light,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__0,NEXUS_5,1,light,en].png deleted file mode 100644 index 54c51dda5c..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__0,NEXUS_5,1,light,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5b7d3c2cf8d466f6e25404596079fb6254ad6e9fb3ba3f0f72ef1c0ba019dc60 -size 31165 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__1,NEXUS_5,1,dark,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__1,NEXUS_5,1,dark,en].png deleted file mode 100644 index 6dfe58cbdc..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__1,NEXUS_5,1,dark,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1be16bc1a73ef5bb7af740b8720c508dbc202d4748d60fb2e10fabcfdeacc75c -size 43668 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__1,NEXUS_5,1,light,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__1,NEXUS_5,1,light,en].png deleted file mode 100644 index 7b8a310f6f..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__1,NEXUS_5,1,light,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:02c410850ed4b8658a21c4a60b62757baa9799d4c6dee4d53d2e6b2ca76f7b61 -size 41942 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__2,NEXUS_5,1,dark,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__2,NEXUS_5,1,dark,en].png deleted file mode 100644 index 2c2a002ae5..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__2,NEXUS_5,1,dark,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:19bcd322392a4d27742a25989810e38d4ac6a38edfb5a228bc13364ed36e7101 -size 31691 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__2,NEXUS_5,1,light,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__2,NEXUS_5,1,light,en].png deleted file mode 100644 index 7344b3e35e..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__2,NEXUS_5,1,light,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0a0ea920c88ab127513918055621a75528de35d69296727a8be7368df4c848e9 -size 33040 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__3,NEXUS_5,1,dark,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__3,NEXUS_5,1,dark,en].png deleted file mode 100644 index b4dc232172..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__3,NEXUS_5,1,dark,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3ea0809dcddfc5df7aaba26f4ed74bebe404c86f5923c78541518fb7560e897e -size 55699 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__3,NEXUS_5,1,light,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__3,NEXUS_5,1,light,en].png deleted file mode 100644 index 89d55c25a3..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__3,NEXUS_5,1,light,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cc36b26cbaab9ee19c898a27bc5bc0a04c77e4004933c47fc69608df103e08d9 -size 52964 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__4,NEXUS_5,1,dark,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__4,NEXUS_5,1,dark,en].png deleted file mode 100644 index 9b684aa226..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__4,NEXUS_5,1,dark,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e50733c48b4cb37fc82c228a7013c7aa74e263b13014cb57c7bb1ca0c2ea91c6 -size 28631 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__4,NEXUS_5,1,light,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__4,NEXUS_5,1,light,en].png deleted file mode 100644 index 1d930443fe..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__4,NEXUS_5,1,light,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0500e499fb319057b0ca5feb9db9e429266a2832521b429d502d1f62804ee23f -size 29222 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__5,NEXUS_5,1,dark,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__5,NEXUS_5,1,dark,en].png deleted file mode 100644 index bf5d223a28..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__5,NEXUS_5,1,dark,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ab7cb0da1b64d2c6d87578debbc336feedbe203f6331726503a084101b9a10e6 -size 51186 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__5,NEXUS_5,1,light,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__5,NEXUS_5,1,light,en].png deleted file mode 100644 index 0a01c81f08..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__5,NEXUS_5,1,light,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5535df19747bdec380b79eec8b9d8e2d7e6ed765fe0bb4e4c1c2ffeb4de46854 -size 49115 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.preferences.root_null_DefaultGroup_PreferencesContentPreview_,NEXUS_5,1,dark,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.preferences.root_null_DefaultGroup_PreferencesContentPreview_,NEXUS_5,1,dark,en].png deleted file mode 100644 index 738199e997..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.preferences.root_null_DefaultGroup_PreferencesContentPreview_,NEXUS_5,1,dark,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a588e13eaa5fef1f93443445c15256037a70771260e24ca4ee6700e55519bf50 -size 27107 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.preferences.root_null_DefaultGroup_PreferencesContentPreview_,NEXUS_5,1,light,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.preferences.root_null_DefaultGroup_PreferencesContentPreview_,NEXUS_5,1,light,en].png deleted file mode 100644 index 9270e28bd0..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.preferences.root_null_DefaultGroup_PreferencesContentPreview_,NEXUS_5,1,light,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:58f4e6a4bed5bb2fe4cf2556ee04984dc3efb78ccc4bc501f98f733d1d849289 -size 26636 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.rageshake.bugreport_null_DefaultGroup_BugReportContentPreview_,NEXUS_5,1,dark,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.rageshake.bugreport_null_DefaultGroup_BugReportContentPreview_,NEXUS_5,1,dark,en].png deleted file mode 100644 index 91b32ac1a3..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.rageshake.bugreport_null_DefaultGroup_BugReportContentPreview_,NEXUS_5,1,dark,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9cf66a0074d1d7842ee176eb6a5e655fe9ed734a95078731cdacd4d7d8f2a80c -size 49391 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.rageshake.crash.ui_null_DefaultGroup_CrashDetectionContentPreview_,NEXUS_5,1,dark,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.rageshake.crash.ui_null_DefaultGroup_CrashDetectionContentPreview_,NEXUS_5,1,dark,en].png deleted file mode 100644 index 706c00952e..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.rageshake.crash.ui_null_DefaultGroup_CrashDetectionContentPreview_,NEXUS_5,1,dark,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e04b3a5a4db346d18666fc4df81fdbb1fa84dc526020fdc257c33190e122c11f -size 29757 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.rageshake.crash.ui_null_DefaultGroup_CrashDetectionContentPreview_,NEXUS_5,1,light,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.rageshake.crash.ui_null_DefaultGroup_CrashDetectionContentPreview_,NEXUS_5,1,light,en].png deleted file mode 100644 index a6318eea0e..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.rageshake.crash.ui_null_DefaultGroup_CrashDetectionContentPreview_,NEXUS_5,1,light,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:af0d4d6e3dcda60a7aa4aa20df2b012359c21d51bc9b94e4ed04b19b1d8f38e4 -size 28409 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.rageshake.detection_null_DefaultGroup_RageshakeDialogContentPreview_,NEXUS_5,1,dark,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.rageshake.detection_null_DefaultGroup_RageshakeDialogContentPreview_,NEXUS_5,1,dark,en].png deleted file mode 100644 index 20a47c41b4..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.rageshake.detection_null_DefaultGroup_RageshakeDialogContentPreview_,NEXUS_5,1,dark,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a4c1e1b89f3692c1441ec502afe20c1b1d3746c996435ba7abfd624ee88995dc -size 34328 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.rageshake.detection_null_DefaultGroup_RageshakeDialogContentPreview_,NEXUS_5,1,light,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.rageshake.detection_null_DefaultGroup_RageshakeDialogContentPreview_,NEXUS_5,1,light,en].png deleted file mode 100644 index 1a442816eb..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.rageshake.detection_null_DefaultGroup_RageshakeDialogContentPreview_,NEXUS_5,1,light,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c74aecba85977483d6415f6cacc2a45199f763773fde1a590a96d43d83b2de0e -size 32407 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.rageshake.preferences_null_DefaultGroup_RageshakePreferenceNotSupportedPreview_,NEXUS_5,1,dark,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.rageshake.preferences_null_DefaultGroup_RageshakePreferenceNotSupportedPreview_,NEXUS_5,1,dark,en].png deleted file mode 100644 index 003c8fc202..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.rageshake.preferences_null_DefaultGroup_RageshakePreferenceNotSupportedPreview_,NEXUS_5,1,dark,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0a0c3a23a06fa723cf6474fda8f8fb88f876b78d7293121ffb438eddacb834f8 -size 4895 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.rageshake.preferences_null_DefaultGroup_RageshakePreferencesViewPreview_,NEXUS_5,1,dark,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.rageshake.preferences_null_DefaultGroup_RageshakePreferencesViewPreview_,NEXUS_5,1,dark,en].png deleted file mode 100644 index e78a01a2ea..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.rageshake.preferences_null_DefaultGroup_RageshakePreferencesViewPreview_,NEXUS_5,1,dark,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:685ec863756abd3036b9db5303684b7a347ae148973670bfbedca421b0d0c464 -size 15532 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.roomlist_null_DefaultGroup_RoomListViewPreview_,NEXUS_5,1,dark,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.roomlist_null_DefaultGroup_RoomListViewPreview_,NEXUS_5,1,dark,en].png deleted file mode 100644 index d3499bc2fc..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.roomlist_null_DefaultGroup_RoomListViewPreview_,NEXUS_5,1,dark,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b76198faaae0e5bdc4d7ace41cad029f48603c2ee5dcb8e246cfeb524c1d39ad -size 35856 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.roomlist_null_DefaultGroup_RoomListViewPreview_,NEXUS_5,1,light,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.roomlist_null_DefaultGroup_RoomListViewPreview_,NEXUS_5,1,light,en].png deleted file mode 100644 index fad4fef94a..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.features.roomlist_null_DefaultGroup_RoomListViewPreview_,NEXUS_5,1,light,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c1aea2bc7f73fd2b3a7d11fcab917e61025dcd536bb912a4bfd7d0ceb33e726a -size 33553 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.tests.uitests_null_Buttons_Showkasebutton_null,NEXUS_5,1,dark,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.tests.uitests_null_Buttons_Showkasebutton_null,NEXUS_5,1,dark,en].png deleted file mode 100644 index 687e51b32c..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.x.tests.uitests_ScreenshotTest_preview_tests[io.element.android.x.tests.uitests_null_Buttons_Showkasebutton_null,NEXUS_5,1,dark,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:35d660e370ec4f7f4e0165418a2981f1afe057cac312ee9cad1c4a4dcc02f189 -size 10386 diff --git a/tools/detekt/detekt.yml b/tools/detekt/detekt.yml index 3634454339..9b95b0cc16 100644 --- a/tools/detekt/detekt.yml +++ b/tools/detekt/detekt.yml @@ -107,7 +107,7 @@ TwitterCompose: CompositionLocalAllowlist: active: true # You can optionally define a list of CompositionLocals that are allowed here - # allowedCompositionLocals: LocalSomething,LocalSomethingElse + allowedCompositionLocals: LocalColors CompositionLocalNaming: active: true ContentEmitterReturningValues: @@ -137,7 +137,7 @@ TwitterCompose: PreviewNaming: active: true PreviewPublic: - active: true + active: false # You can optionally disable that only previews with @PreviewParameter are flagged previewPublicOnlyIfParams: false RememberMissing: