From 09be39a7a6f868e87e1445d395a079ffc3540276 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 11 Jan 2023 16:40:37 +0100 Subject: [PATCH 01/96] Update README.md --- README.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index caa8fff8ef..0d60ca1a38 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,42 @@ -# 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 +* [Rust SDK](#rust-sdk) +* [Roadmap](#roadmap) +* [Contributing](#contributing) +* [Build instructions](#build-instructions) +* [Modules](#modules) +* [Support](#support) +* [Copyright & License](#copyright-&-license) + + + +## 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. + +## Modules This Android project is a multi modules project. @@ -25,3 +56,18 @@ Here is the current module dependency graph: +## 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. From ed167f3c5ea8a3908b9d42c02dcbfa78be171b72 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 11 Jan 2023 16:51:48 +0100 Subject: [PATCH 02/96] Add CONTRIBUTING.md file --- CONTRIBUTING.md | 166 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..c7acf734dd --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,166 @@ +# Contributing to Element Android + + + +* [Contributing code to Matrix](#contributing-code-to-matrix) +* [Android Studio settings](#android-studio-settings) + * [Template](#template) +* [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) + * [Internal tool](#internal-tool) + * [ktlint](#ktlint) + * [knit](#knit) + * [lint](#lint) + * [Unit tests](#unit-tests) + * [Tests](#tests) + * [Internationalisation](#internationalisation) + * [Adding new string](#adding-new-string) + * [Plurals](#plurals) + * [Editing existing strings](#editing-existing-strings) + * [Removing existing strings](#removing-existing-strings) + * [Renaming string ids](#renaming-string-ids) + * [Reordering strings](#reordering-strings) + * [Accessibility](#accessibility) + * [Layout](#layout) + * [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! From 99f4e570d56a10fe44b0da22fbd4a43455b4fbee Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 11 Jan 2023 16:54:50 +0100 Subject: [PATCH 03/96] Add AUTHORS.md file --- AUTHORS.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 AUTHORS.md 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) From 976e5534adb0a2d753af7d1ebff827b908c7fd37 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 11 Jan 2023 17:02:29 +0100 Subject: [PATCH 04/96] Add some docs - lots of outdated part. --- CONTRIBUTING.md | 10 +- README.md | 21 -- docs/_developer_onboarding.md | 202 ++++++++++++++++++ docs/analytics.md | 24 +++ docs/danger.md | 106 ++++++++++ docs/database_migration_test.md | 55 +++++ docs/design.md | 141 +++++++++++++ docs/flipper.md | 58 ++++++ docs/identity_server.md | 106 ++++++++++ docs/installing_from_ci.md | 52 +++++ docs/integration_tests.md | 131 ++++++++++++ docs/jitsi.md | 96 +++++++++ docs/nightly_build.md | 54 +++++ docs/notifications.md | 284 ++++++++++++++++++++++++++ docs/pull_request.md | 290 ++++++++++++++++++++++++++ docs/screenshot_testing.md | 72 +++++++ docs/ui-tests.md | 193 ++++++++++++++++++ docs/unit_testing.md | 351 ++++++++++++++++++++++++++++++++ 18 files changed, 2216 insertions(+), 30 deletions(-) create mode 100644 docs/_developer_onboarding.md create mode 100644 docs/analytics.md create mode 100644 docs/danger.md create mode 100644 docs/database_migration_test.md create mode 100644 docs/design.md create mode 100644 docs/flipper.md create mode 100644 docs/identity_server.md create mode 100644 docs/installing_from_ci.md create mode 100644 docs/integration_tests.md create mode 100644 docs/jitsi.md create mode 100644 docs/nightly_build.md create mode 100644 docs/notifications.md create mode 100644 docs/pull_request.md create mode 100644 docs/screenshot_testing.md create mode 100644 docs/ui-tests.md create mode 100644 docs/unit_testing.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c7acf734dd..5cd8785b33 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,28 +4,20 @@ * [Contributing code to Matrix](#contributing-code-to-matrix) * [Android Studio settings](#android-studio-settings) - * [Template](#template) * [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) - * [Internal tool](#internal-tool) * [ktlint](#ktlint) * [knit](#knit) * [lint](#lint) * [Unit tests](#unit-tests) * [Tests](#tests) * [Internationalisation](#internationalisation) - * [Adding new string](#adding-new-string) - * [Plurals](#plurals) - * [Editing existing strings](#editing-existing-strings) - * [Removing existing strings](#removing-existing-strings) - * [Renaming string ids](#renaming-string-ids) - * [Reordering strings](#reordering-strings) * [Accessibility](#accessibility) - * [Layout](#layout) + * [Jetpack Compose](#jetpack-compose) * [Authors](#authors) * [Thanks](#thanks) diff --git a/README.md b/README.md index 0d60ca1a38..2b3385c34b 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ The application is a total rewrite of [Element-Android](https://github.com/vecto * [Roadmap](#roadmap) * [Contributing](#contributing) * [Build instructions](#build-instructions) -* [Modules](#modules) * [Support](#support) * [Copyright & License](#copyright-&-license) @@ -36,26 +35,6 @@ Come chat with the community in the dedicated Matrix [room](https://matrix.to/#/ Just clone the project and open it in Android Studio. -## Modules - -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. - -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. - -Here is the current module dependency graph: - - - - ## 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) diff --git a/docs/_developer_onboarding.md b/docs/_developer_onboarding.md new file mode 100644 index 0000000000..3081d36a44 --- /dev/null +++ b/docs/_developer_onboarding.md @@ -0,0 +1,202 @@ +# 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) + * [Anvil](#anvil) + * [Node](#node) + * [Other frameworks](#other-frameworks) + * [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 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. + +Here is the current module dependency graph: + + + + +### Application + +(Note: to update) + +This is the UI part of the project. + +There are two variants of the application: `Gplay` and `Fdroid`. + +The main difference is about using Firebase on `Gplay` variant, to have Push from Google Services. `FDroid` variant cannot contain closed source dependency. + +`Fdroid` is using background polling to lack the missing of Pushed. Now a solution using UnifiedPush has ben added to the project. See refer to [the dedicated documentation](./unifiedpush.md) for more details. + +#### Anvil + +TODO + +##### Node + +TODO + +#### Other frameworks + +- Dependency injection is managed by [Dagger](https://dagger.dev/) (SDK) and [Hilt](https://developer.android.com/training/dependency-injection/hilt-android) (App); + +### Push + +Please see the dedicated documentation 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 + +TODO Update +All the dependencies are declared in `build.gradle` files. But some versions are declared in [this dedicated file](../dependencies.gradle). + +When adding a new dependency, you will have to update the file [dependencies_groups.gradle](../dependencies_groups.gradle) to allow the dependency to be downloaded from the artifact repository. Sometimes sub-dependencies need to be added too, until the project can compile. + +[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. +dependencies_group, gradle files, Dependabot, etc. + +### Test + +Please refer to [this dedicated document](./ui-tests.md). + +TODO add link to the dedicated screenshot test documentation + +### 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, not 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 report can contain: +- a screenshot of the current application state +- the application logs from up to 15 application starts +- the logcat logs +- the key share history (crypto data) + +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; +- 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; +- Type `/devtools` in a Room composer to access a developer menu. There are some other entry points. Developer mode has to be enabled; +- 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; +- Using logcat, filtering with `onResume` 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. + +## 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..9b2c176912 --- /dev/null +++ b/docs/analytics.md @@ -0,0 +1,24 @@ +# Analytics in Element + + + +* [Solution](#solution) +* [How to add a new Event](#how-to-add-a-new-event) +* [Forks of Element](#forks-of-element) + + + +## Solution + +Element is using PostHog to send analytics event. +We ask for the user to give consent before sending any analytics data. + +## How to add a new Event + +The analytics plan is shared between all Element clients. To add an Event, please open a PR to this project: https://github.com/matrix-org/matrix-analytics-events + +Then, once the PR has been merged, you can run the tool `import_analytic_plan.sh` to import the plan to Element, and then you can use the new Event. Note that this tool is run by Github action once a week. + +## Forks of Element + +Analytics on forks are disabled by default. Please refer to AnalyticsConfig and there implementation to setup analytics on your project. diff --git a/docs/danger.md b/docs/danger.md new file mode 100644 index 0000000000..34baa62e9e --- /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 +- 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/database_migration_test.md b/docs/database_migration_test.md new file mode 100644 index 0000000000..f7844abde8 --- /dev/null +++ b/docs/database_migration_test.md @@ -0,0 +1,55 @@ + + +* [Testing database migration](#testing-database-migration) + * [Creating a reference database](#creating-a-reference-database) + * [Testing](#testing) + + + +## Testing database migration + +### Creating a reference database + +Databases are encrypted, the key to decrypt is needed to setup the test. +A special build property must be enabled to extract it. + +Set `vector.debugPrivateData=true` in `~/.gradle/gradle.properties` (to avoid committing by mistake) + +Launch the app in your emulator, login and use the app to fill up the database. + +Save the key for the tested database +``` +RealmKeysUtils W Database key for alias `session_db_fe9f212a611ccf6dea1141777065ed0a`: 935a6dfa0b0fc5cce1414194ed190.... +RealmKeysUtils W Database key for alias `crypto_module_fe9f212a611ccf6dea1141777065ed0a`: 7b9a21a8a311e85d75b069a343..... +``` + + +Use the [Device File Explorer](https://developer.android.com/studio/debug/device-file-explorer) to extrat the database file from the emulator. + +Go to `data/data/im.vector.app.debug/files//` +Pick the database you want to test (name can be found in SessionRealmConfigurationFactory): + - crypto_store.realm for crypto + - disk_store.realm for session + - etc... + +Download the file on your disk + +### Testing + +Copy the file in `src/AndroidTest/assets` + +see `CryptoSanityMigrationTest` or `RealmSessionStoreMigration43Test` for sample tests. + +There are already some databases in the assets folder. +The existing test will properly detect schema changes, and fail with such errors if a migration is missing: + +``` +io.realm.exceptions.RealmMigrationNeededException: Migration is required due to the following errors: +- Property 'CryptoMetadataEntity.foo' has been added. +``` + +If you want to test properly more complex database migration (dynamic transforms) ensure that the database contains +the entity you want to migrate. + +You can explore the database with [realm studio](https://www.mongodb.com/docs/realm/studio/) if needed. + diff --git a/docs/design.md b/docs/design.md new file mode 100644 index 0000000000..f390725560 --- /dev/null +++ b/docs/design.md @@ -0,0 +1,141 @@ +# 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) + + + +## 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/flipper.md b/docs/flipper.md new file mode 100644 index 0000000000..495425ce5f --- /dev/null +++ b/docs/flipper.md @@ -0,0 +1,58 @@ +# Flipper + + + +* [Introduction](#introduction) +* [Setup](#setup) + * [Troubleshoot](#troubleshoot) + * [No device found issue](#no-device-found-issue) + * [Diagnostic Activity](#diagnostic-activity) + * [Other](#other) +* [Links](#links) + + + +## Introduction + +[Flipper](https://fbflipper.com) is a powerful tool from Meta, which allow to inspect the running application details and states from your computer. + +Flipper is configured in the Element Android project to let the developers be able to: +- inspect all the Realm databases content; +- do layout inspection; +- see the crash logs; +- see the logcat; +- see all the network requests; +- see all the SharedPreferences; +- take screenshots and record videos of the device; +- and more! + +## Setup + +- Install Flipper on your computer. Follow instructions here: https://fbflipper.com/docs/getting-started/index/ +- Run the debug version of Element on an emulator or on a real device. + +### Troubleshoot + +#### No device found issue + +The configuration of the Flipper application has to be updated. The issue has been asked and answered here: https://stackoverflow.com/questions/71744103/android-emulator-unable-to-connect-to-flipper/72608113#72608113 + +#### Diagnostic Activity + +Flipper comes with a Diagnostic Activity that you can start from command line using: + +```shell +adb shell am start -n im.vector.app.debug/com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity +``` + +It provides some log which can help to figure out what's going on client side. + +#### Other + +https://fbflipper.com/docs/getting-started/troubleshooting/android/ may help. + +## Links + +- Official Flipper website: https://fbflipper.com +- Realm Plugin for Flipper: https://github.com/kamgurgul/Flipper-Realm +- Dedicated Matrix room: https://matrix.to/#/#unifiedpush:matrix.org diff --git a/docs/identity_server.md b/docs/identity_server.md new file mode 100644 index 0000000000..a1ee86cb19 --- /dev/null +++ b/docs/identity_server.md @@ -0,0 +1,106 @@ +# Identity server + + + +* [Introduction](#introduction) +* [Implementation](#implementation) +* [Related MSCs](#related-mscs) +* [Steps and requirements](#steps-and-requirements) +* [Screens](#screens) + * [Settings](#settings) + * [Discovery screen](#discovery-screen) + * [Set identity server screen](#set-identity-server-screen) +* [Ref:](#ref:) + + + +Issue: #607 +PR: #1354 + +## Introduction +Identity servers support contact discovery on Matrix by letting people look up Third Party Identifiers to see if the owner has publicly linked them with their Matrix ID. + +## Implementation + +The current implementation was Inspired by the code from Riot-Android. + +Difference though (list not exhaustive): +- Only API v2 is supported (see https://matrix.org/docs/spec/identity_service/latest) +- Homeserver has to be up to date to support binding (Versions.isLoginAndRegistrationSupportedBySdk() has to return true) +- The SDK managed the session and client secret when binding ThreePid. Those data are not exposed to the client. +- The SDK supports incremental sendAttempt (this is not used by Element) +- The "Continue" button is now under the information, and not as the same place that the checkbox +- The app can cancel a binding. Current data are erased from DB. +- The API (IdentityService) is improved. +- A new DB to store data related to the identity server management. + +Missing features (list not exhaustive): +- Invite by 3Pid (will be in a dedicated PR) +- Add email or phone to account (not P1, can be done on Element-Web) +- List email and phone of the account (could be done in a dedicated PR) +- Search contact (not P1) +- Logout from identity server when user sign out or deactivate his account. + +## Related MSCs +The list can be found here: https://matrix.org/blog/2019/09/27/privacy-improvements-in-synapse-1-4-and-riot-1-4 + +## Steps and requirements + +- Only one identity server by account can be set. The user's choice is stored in account data with key `m.identity_server`. But every clients will managed its own token to log in to the identity server +```json +{ + "type": "m.identity_server", + "content": { + "base_url": "https://matrix.org" + } +} +``` +- The accepted terms are stored in the account data: +```json +{ + "type": "m.accepted_terms", + "content": { + "accepted": [ + "https://vector.im/identity-server-privacy-notice-1" + ] + } +} +``` + +- Default identity server URL, from Wellknown data is proposed to the user. +- Identity server can be set +- Identity server can be changed on another user's device, so when the change is detected (thanks to account data sync) Element should properly disconnect from a previous identity server (I think it was not the case in Riot-Android, where we keep the token forever) +- Registration to the identity server is managed with an openId token +- Terms of service can be accepted when configuring the identity server. +- Terms of service can be accepted after, if they change. +- Identity server can be modified +- Identity server can be disconnected with a warning dialog, with special content if there are current bound 3pid on this identity server. +- Email can be bound +- Email can be unbound +- Phone can be bound +- Phone can be unbound +- Look up can be performed, to get matrixIds from local contact book (phone and email): Android permission correctly handled (not done yet) +- Look up pepper can be updated if it is rotated on the identity server +- Invitation using 3PID can be done (See #548) (not done yet) +- Homeserver access-token will never be sent to an identity server +- When user sign-out: logout from the identity server if any. +- When user deactivate account: logout from the identity server if any. + +## Screens + +### Settings + +Identity server settings can be accessed from the internal setting of the application, both from "Discovery" section and from identity detail section. + +### Discovery screen + +This screen displays the identity server configuration and the binding of the user's ThreePid (email and msisdn). This is the main screen of the feature. + +### Set identity server screen + +This screen is a form to set a new identity server URL + +## Ref: +- https://matrix.org/blog/2019/09/27/privacy-improvements-in-synapse-1-4-and-riot-1-4 is a good summary of the role of an identity server and the proper way to configure and use it in respect to the privacy and the consent of the user. +- API documentation: https://matrix.org/docs/spec/identity_service/latest +- vector.im TOS: https://vector.im/identity-server-privacy-notice diff --git a/docs/installing_from_ci.md b/docs/installing_from_ci.md new file mode 100644 index 0000000000..01fb4afef2 --- /dev/null +++ b/docs/installing_from_ci.md @@ -0,0 +1,52 @@ +## Installing from CI + + + + * [Installing from Buildkite](#installing-from-buildkite) + * [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 Buildkite + +The script `./tools/install/installFromBuildkite.sh` can be used, but Builkite will be removed soon. See next section. + +### Installing from GitHub + +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/jitsi.md b/docs/jitsi.md new file mode 100644 index 0000000000..d6c93c49aa --- /dev/null +++ b/docs/jitsi.md @@ -0,0 +1,96 @@ +# Jitsi in Element Android + + + +* [Native Jitsi SDK](#native-jitsi-sdk) + * [How to build the Jitsi Meet SDK](#how-to-build-the-jitsi-meet-sdk) + * [Jitsi version](#jitsi-version) + * [Run the build script](#run-the-build-script) + * [Link with the new generated library](#link-with-the-new-generated-library) + * [Sanity tests](#sanity-tests) + * [Export the build library](#export-the-build-library) + + + +Native Jitsi support has been added to Element Android by the PR [#1914](https://github.com/vector-im/element-android/pull/1914). The description of the PR contains some documentation about the behaviour in each possible room configuration. + +Also, ensure to have a look on [the documentation from Element Web](https://github.com/vector-im/element-web/blob/develop/docs/jitsi.md) + +The official documentation about how to integrate the Jitsi SDK in an Android app is available here: https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-android-sdk. + +## Native Jitsi SDK + +The Jitsi SDK is built by ourselves with the flag LIBRE_BUILD, to be able to be integrated on the F-Droid version of Element Android. + +The generated maven repository is then host in the project https://github.com/vector-im/jitsi_libre_maven + +### How to build the Jitsi Meet SDK + +#### Jitsi version + +Update the script `./tools/jitsi/build_jisti_libs.sh` with the tag of the project `https://github.com/jitsi/jitsi-meet`. + +Latest tag can be found from this page: https://github.com/jitsi/jitsi-meet-release-notes/blob/master/CHANGELOG-MOBILE-SDKS.md + +Currently we are building the version with the tag `android-sdk-3.10.0`. + +#### Run the build script + +At the root of the Element Android, run the following script: + +```shell script +./tools/jitsi/build_jisti_libs.sh +``` + +It will build the Jitsi Meet Android library and put every generated files in the folder `/tmp/jitsi` + +#### Link with the new generated library + +- Update the file `./build.gradle` to use the previously created local Maven repository. Currently we have this line: + +```groovy +url "https://github.com/vector-im/jitsi_libre_maven/raw/master/android-sdk-3.10.0" +``` + +You can uncomment and update the line starting with `// url "file://...` and comment the line starting with `url`, to test the library using the locally generated Maven repository. + +- Update the dependency of the Jitsi Meet library in the file `./vector/build.gradle`. Currently we have this line: + +```groovy +implementation('org.jitsi.react:jitsi-meet-sdk:3.10.0') +``` + +- Update the dependency of the WebRTC library in the file `./vector/build.gradle`. Currently we have this line: + +```groovy +implementation('com.facebook.react:react-native-webrtc:1.92.1-jitsi-9093212@aar') +``` + +- Perform a gradle sync and build the project +- Perform test + +#### Sanity tests + +In order to validate that the upgrade of the Jitsi and WebRTC dependency does not break anything, the following sanity tests have to be performed, using two devices: +- Make 1-1 audio call (so using WebRTC) +- Make 1-1 video call (so using WebRTC) +- Create and join a conference call with audio only (so using Jitsi library). Leave the conference. Join it again. +- Create and join a conference call with audio and video (so using Jitsi library) Leave the conference. Join it again. + +#### Export the build library + +If all the tests are passed, you can export the generated Jitsi library to our Maven repository. + +- Clone the project https://github.com/vector-im/jitsi_libre_maven. +- Create a new folder with the version name. +- Copy every generated files form `/tmp/jitsi` to the folder you have just created. +- Commit and push the change on https://github.com/vector-im/jitsi_libre_maven. +- Update the file `./build.gradle` to use the previously created Maven repository. Currently we have this line: + +```groovy +url "https://github.com/vector-im/jitsi_libre_maven/raw/master/android-sdk-3.10.0" +``` + +- Build the project and perform the sanity tests again. + +- Create a PR for project Element Android and add a changelog file `.misc` to notify about the library upgrade. diff --git a/docs/nightly_build.md b/docs/nightly_build.md new file mode 100644 index 0000000000..77cc676c7f --- /dev/null +++ b/docs/nightly_build.md @@ -0,0 +1,54 @@ +# 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 Gplay variant. It is signed using a dedicated signature, and has a dedicated appId (`im.vector.app.nightly`), so it can be installed along with the production version of Element Android. The only other difference compared to Element Android is a different app icon background. 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. + +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 Element Android, and fallback to Element 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`. + +*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 + +Provide your email to the Android team, who will add it to the list "External testers" on Firebase. You will then receive an invite on the provided email. + +Follow the instructions on the email to install the latest nightly build. This is not clear yet if new nightly build will be automatically installed or not. + +## 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..7f6ac86b8a --- /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-android-reviewers](https://github.com/orgs/vector-im/teams/element-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-android-reviewers](https://github.com/orgs/vector-im/teams/element-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/screenshot_testing.md b/docs/screenshot_testing.md new file mode 100644 index 0000000000..93b91cdf67 --- /dev/null +++ b/docs/screenshot_testing.md @@ -0,0 +1,72 @@ +# Screenshot testing + + + +* [Overview](#overview) +* [Setup](#setup) +* [Recording](#recording) +* [Verifying](#verifying) +* [Contributing](#contributing) +* [Example](#example) + + + +## Overview + +- Screenshot tests are tests which record the content of a rendered screen and verify subsequent runs to check if the screen renders differently. +- Element uses [Paparazzi](https://github.com/cashapp/paparazzi) to render, record and verify android layouts. +- The screenshot verification occurs on every pull request as part of the `tests.yml` workflow. + +## Setup + +- Install Git LFS through your package manager of choice (`brew install git-lfs` | `yay -S git-lfs`). +- Install the Git LFS hooks into the project. + +```bash +# with element-android as the current working directory +git lfs install --local +``` + +- If installed correctly, `git push` and `git pull` will now include LFS content. + +## Recording + +- `./gradlew recordScreenshots` +- Paparazzi will generate images in `${module}/src/test/snapshots`, which will need to be committed to the repository using Git LFS. + +## Verifying + +- `./gradlew verifyScreenshots` +- In the case of failure, Paparazzi will generate images in `${module}/out/failure`. The images will show the expected and actual screenshots along with a delta of the two images. + +## Contributing + +- When creating a test, the file (and class) name names must include `ScreenshotTest`, eg `ItemScreenshotTest`. +- After creating the new test, record and commit the newly rendered screens. +- `./tools/validate_lfs` can be ran to ensure everything is working correctly with Git LFS, the CI also runs this check. + +## Example + +```kotlin +class PaparazziExampleScreenshotTest { + + @get:Rule + val paparazzi = Paparazzi( + deviceConfig = PIXEL_3, + theme = "Theme.Vector.Light", + ) + + @Test + fun `example paparazzi test`() { + // Inflate the layout + val view = paparazzi.inflate(R.layout.item_radio) + + // Bind data to the view + view.findViewById(R.id.actionTitle).text = paparazzi.resources.getString(R.string.room_settings_all_messages) + view.findViewById(R.id.radioIcon).setImageResource(R.drawable.ic_radio_on) + + // Record the bound view + paparazzi.snapshot(view) + } +} +``` diff --git a/docs/ui-tests.md b/docs/ui-tests.md new file mode 100644 index 0000000000..d0b9db6818 --- /dev/null +++ b/docs/ui-tests.md @@ -0,0 +1,193 @@ +# Automate user interface tests + +Element Android ensures that some fundamental flows are properly working by running automated user interface tests. +Ui tests are using the android [Espresso](https://developer.android.com/training/testing/espresso) library. + +Tests can be run on a real device, or on a virtual device (such as the emulator in Android Studio). + +Currently the test are covering a small set of application flows: + - Registration + - Self verification via emoji + - Self verification via passphrase + + + +* [Prerequisites:](#prerequisites:) +* [Run the tests](#run-the-tests) + * [From the source code](#from-the-source-code) + * [From command line](#from-command-line) +* [Recipes](#recipes) + * [Wait for initial sync](#wait-for-initial-sync) + * [Accessing current activity](#accessing-current-activity) + * [Interact with other session](#interact-with-other-session) + * [Contributing to the UiAllScreensSanityTest](#contributing-to-the-uiallscreenssanitytest) + + + +## Prerequisites: + +Out of the box, the tests use one of the homeservers (located at http://localhost:8080) of the "Demo Federation of Homeservers" (https://github.com/matrix-org/synapse#running-a-demo-federation-of-synapses). + +You first need to follow instructions to set up Synapse in development mode at https://github.com/matrix-org/synapse#synapse-development. If you have already installed all dependencies, the steps are: + +```shell script +$ git clone https://github.com/matrix-org/synapse.git +$ cd synapse +$ virtualenv -p python3 env +$ source env/bin/activate +(env) $ python -m pip install --no-use-pep517 -e . +``` + +Every time you want to launch these test homeservers, type: + +```shell script +$ source env/bin/activate +(env) $ demo/start.sh --no-rate-limit +``` + +**Emulator/Device set up** + +When running the test via android studio on a device, you have to disable system animations in order for the test to work properly. + +First, ensure developer mode is enabled: + +- To enable developer options, tap the **Build Number** option 7 times. You can find this option in one of the following locations, depending on your Android version: + + - Android 9 (API level 28) and higher: **Settings > About Phone > Build Number** + - Android 8.0.0 (API level 26) and Android 8.1.0 (API level 26): **Settings > System > About Phone > Build Number** + - Android 7.1 (API level 25) and lower: **Settings > About Phone > Build Number** + +On your device, under **Settings > Developer options**, disable the following 3 settings: + +- Window animation scale +- Transition animation scale +- Animator duration scale + +## Run the tests + +Once Synapse is running, and an emulator is running, you can run the UI tests. + +### From the source code + +Click on the green arrow in front of each test. Clicking on the arrow in front of the test class, or from the package directory does not always work (Tests not found issue). + +### From command line + +````shell script +./gradlew vector:connectedGplayDebugAndroidTest +```` + +To run all the tests from the `vector` module. + +In case of trouble, you can try to uninstall the previous installed test APK first with this command: + +```shell script +adb uninstall im.vector.app.debug.test +``` +## Recipes + +We added some specific Espresso IdlingResources, and other utilities for matrix related tests + +### Wait for initial sync + +```kotlin +// Wait for initial sync and check room list is there +withIdlingResource(initialSyncIdlingResource(uiSession)) { + onView(withId(R.id.roomListContainer)) + .check(matches(isDisplayed())) +} +``` + +### Accessing current activity + +```kotlin + val activity = EspressoHelper.getCurrentActivity()!! + val uiSession = (activity as HomeActivity).activeSessionHolder.getActiveSession() +``` + +### Interact with other session + +It's possible to create a session via the SDK, and then use this session to interact with the one that the emulator is using (to check verifications for example) + +```kotlin +@Before +fun initAccount() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + val matrix = Matrix.getInstance(context) + val userName = "foobar_${System.currentTimeMillis()}" + existingSession = createAccountAndSync(matrix, userName, password, true) +} +``` + +### Contributing to the UiAllScreensSanityTest + +The `UiAllScreensSanityTest` makes use of the Robot pattern in order to model pages, components and interactions. +Each Robot aims to return the UI back to its original state after the interaction, allowing for a reusable and consistent DSL. + +```kotlin +// launches and closes settings after executing the block +elementRobot.settings { + // whilst in the settings, launches and closes the advanced settings sub screen + advancedSettings { + // crawls all the pages within the advanced settings + crawl() + } +} + +// enables developer mode by navigating to the settings, enabling the toggle and then returning to the starting point to execute the block +// on block completion the Robot disables developer mode by navigating back to the settings and finally returning to the original starting point +elementRobot.withDeveloperMode { + // the same starting point as the example above + settings { + advancedSettings { crawlDeveloperOptions() } + } +} +``` + +The Robots used in the example above... + +```kotlin +class ElementRobot { + fun settings(block: SettingsRobot.() -> Unit) { + // double check we're where we think we are + waitUntilViewVisible(withId(R.id.bottomNavigationView)) + + // navigate to the settings + openDrawer() + clickOn(R.id.homeDrawerHeaderSettingsView) + + // execute the robot with the context of the settings screen + block(SettingsRobot()) + + // close the settings and ensure we're back at the starting point + pressBack() + waitUntilViewVisible(withId(R.id.bottomNavigationView)) + } + + fun withDeveloperMode(block: ElementRobot.() -> Unit) { + settings { toggleDeveloperMode() } + block() + settings { toggleDeveloperMode() } + } +} + +class SettingsRobot { + fun toggleDeveloperMode() { + advancedSettings { + toggleDeveloperMode() + } + } + + fun advancedSettings(block: SettingsAdvancedRobot.() -> Unit) { + clickOn(R.string.settings_advanced_settings) + block(SettingsAdvancedRobot()) + pressBack() + } +} + +class SettingsAdvancedRobot { + fun toggleDeveloperMode() { + clickOn(R.string.settings_developer_mode_summary) + } +} +``` diff --git a/docs/unit_testing.md b/docs/unit_testing.md new file mode 100644 index 0000000000..95b78c7f5f --- /dev/null +++ b/docs/unit_testing.md @@ -0,0 +1,351 @@ +# Table of Contents + + + +* [Overview](#overview) + * [Best Practices](#best-practices) +* [Project Conventions](#project-conventions) + * [Setup](#setup) + * [Naming](#naming) + * [Format](#format) + * [Assertions](#assertions) + * [Constants](#constants) + * [Mocking](#mocking) + * [Fakes](#fakes) + * [Fixtures](#fixtures) + * [Examples](#examples) + * [Extensions used to streamline the test setup](#extensions-used-to-streamline-the-test-setup) + * [Fakes and Fixtures](#fakes-and-fixtures) + + + +## Overview + +Unit tests are a mechanism to validate our code executes the way we expect. They help to inform the design of our systems by requiring testability and +understanding, they describe the inner workings without relying on inline comments and protect from unexpected regressions. + +However, unit tests are not a magical solution to solve all our problems and come at a cost. Unreliable and hard to maintain tests often end up ignored, deleted +or worse, provide a false sense of security. + +### Best Practices + +Tests can be written in many ways, the main rule is to keep them simple and maintainable. Some ways to help achieve this are... + +- Break out logic into single units (following the Single Responsibility Principle) to reduce test complexity. +- Favour pure functions, avoiding mutable state. +- Prefer dependency injection to static calls to allow for simpler test setup. +- Write concise tests with a single function under test, clearly showing the inputs and expected output. +- Create separate test cases instead of changing parameters and grouping multiple assertions within a single test to help trace back failure causes (with the + exception of parameterised tests). +- Assert against entire models instead of subsets of properties to capture any possible changes within the test scope. +- Avoid invoking logic from production instances other than the class under test to guard from unrelated changes. +- Always inject `Dispatchers` and `Clock` instances and provide fake implementations for tests to avoid non deterministic results. + +## Project Conventions + +#### Setup + +- Test file and class name should be the class under test with the Test suffix, created in a `test` sourceset, with the same package name as the class under + test. +- Dependencies of the class are instantiated inline, junit will recreate the test class for each test run. +- A line break between the dependencies and class under test helps clarify the instance being tested. + +```kotlin + +class MyClassTest { + + private val fakeUppercaser = FakeUppercaser() + + // line break between the class under test and its dependencies + private val myClass = MyClass(fakeUppercaser.instance) +} + +``` + +#### Naming + +- Test names use the `Gherkin` format, `given, when, then` mapping to the input, logic under test and expected result. + - `given` - Uniqueness about the environment or dependencies in which the test case is running. _"given device is android 12 and supports dark mode"_ + - `when` - The action/function under test. _"when reading dark mode status"_ + - `then` - The expected result from the combination of _given_ and _when_. _"then returns dark mode enabled"_ +- Test names are written using kotlin back ticks to enable sentences _ish_. + +```kotlin +@Test +fun `given a lowercase label, when uppercasing, then returns label uppercased` +``` + +When the input is given directly to the _when_, this can also be represented as... + +```kotlin +@Test +fun `when uppercasing a lowercase label, then returns label uppercased` +``` + +Multiple given or returns statements can be used in the name although it could be a sign that the logic being tested does too much. + +--- + +#### Format + +- Test bodies are broken into sections through the use of blank lines where the sections correspond to the test name. +- Sections can span multiple lines. + +```kotlin +// comments are for illustrative purposes +/* given */ val lowercaseLabel = "hello world" + +/* when */ val result = textUppercaser.uppercase(lowercaseLabel) + +/* then */ result shouldBeEqualTo "HELLO WORLD" +``` + +- Functions extracted from test bodies are placed beneath all the unit tests. + +--- + +#### Assertions + +- Assertions against test results are made using [Kluent's](https://github.com/MarkusAmshove/Kluent) _fluent_ api. +- Typically `shouldBeEqualTo`is the main assertion to use for asserting function return values as by project convention we assert against entire objects or + lists. + +```kotlin +val result = listOf("hello", "world") + +// Fail +result shouldBeEqualTo listOf("hello") +``` + +```kotlin +data class Person(val age: Int, val name: String) + +val result = Person(age = 100, name = "Gandalf") + +// Avoid +result.age shouldBeEqualTo 100 + +// Prefer +result shouldBeEqualTo Person(age = 100, "Gandalf") +``` + +- Exception throwing can be asserted against using `assertFailsWith`. +- When asserting reusable exceptions, include the message to distinguish between them. + +```kotlin +assertFailsWith(message = "Details about error") { + // when section of the test + codeUnderTest() +} +``` + +--- + +#### Constants + +- Reusable values are extracted to file level immutable properties or constants. +- These can be parameters or expected results. +- The naming convention is to prefix with `A` or `AN` for better matching with the test name. + +```kotlin +private const val A_LOWERCASE_LABEL = "hello" + +class MyTest { + @Test + fun `when uppercasing a lowercase label, then returns label uppercased`() { + val result = TextUppercaser().uppercase(A_LOWERCASE_LABEL) + ... + } +} +``` + +--- + +#### Mocking + +- In order to provide different behaviour for dependencies within tests our main method is through mocking, using [Mockk](https://mockk.io/). +- We avoid using relaxed mocks in favour of explicitly declaring mock behaviour through the _Fake_ convention. There are exceptions when mocking framework + classes which would require a lot of boilerplate. +- Using `Spy` is discouraged as it inherently requires real instances, which we are avoiding in our tests. There are exceptions such as `VectorFeatures` which + acts like a `Fixture` in release builds. + +--- + +#### Fakes + +- Fakes are reusable instances of classes purely for testing purposes. They provide functions to replace the functions of the interface/class they're faking + with test specific values. +- When faking an interface, the _Fake_ can be written using delegation or by stubbing +- All Fakes currently reside in the same package `${package}.test.fakes` + +```kotlin +// Delegating to a mock +class FakeClock : Clock by mockk() { + fun givenEpoch(epoch: Long) { + every { epochMillis() } returns epoch + } +} + +// Stubbing the interface +class FakeClock(private val epoch: Long) : Clock { + override fun epochMillis() = epoch +} +``` + +It's currently more common for fakes to fake class behaviour, we achieve this by wrapping and exposing a mock instance. + +```kotlin +class FakeCursor { + val instance = mockk() + fun givenEmpty() { + every { instance.count } returns 0 + every { instance.moveToFirst() } returns false + } +} + +val fakeCursor = FakeCursor().apply { givenEmpty() } +``` + +#### Fixtures + +- Fixtures are a reusable wrappers around data models. They provide default values to make creating instances as easy as possible, with the option to override + specific parameters when needed. +- Are namespaced within an `object`. +- Reduces the _find usages_ noise when searching for usages of the origin class construction. +- All Fixtures currently reside in the same package `${package}.test.fixtures`. + +```kotlin +object ContentAttachmentDataFixture { + fun aContentAttachmentData( + type: ContentAttachmentData.Type.TEXT, + mimeType: String? = null + ) = ContentAttachmentData(type, mimeType) +} +``` + +- Fixtures can also be used to manage specific combinations of parameters + +```kotlin +fun aContentAttachmentAudioData() = aContentAttachmentData( + type = ContentAttachmentData.Type.AUDIO, + mimeType = "audio/mp3", +) +``` + +--- + +### Examples + +##### Extensions used to streamline the test setup + +```kotlin +class CircularCacheTest { + + @Test + fun `when putting more than cache size then cache is limited to cache size`() { + val (cache, internalData) = createIntCache(cacheSize = 3) + + cache.putInOrder(1, 1, 1, 1, 1, 1) + + internalData shouldBeEqualTo arrayOf(1, 1, 1) + } +} + +private fun createIntCache(cacheSize: Int): Pair, Array> { + var internalData: Array? = null + val factory: (Int) -> Array = { + Array(it) { null }.also { array -> internalData = array } + } + return CircularCache(cacheSize, factory) to internalData!! +} + +private fun CircularCache.putInOrder(vararg values: Int) { + values.forEach { put(it) } +} +``` + +##### Fakes and Fixtures + +```kotlin +class LateInitUserPropertiesFactoryTest { + + private val fakeActiveSessionDataSource = FakeActiveSessionDataSource() + private val fakeVectorStore = FakeVectorStore() + private val fakeContext = FakeContext() + private val fakeSession = FakeSession().also { + it.givenVectorStore(fakeVectorStore.instance) + } + + private val lateInitUserProperties = LateInitUserPropertiesFactory( + fakeActiveSessionDataSource.instance, + fakeContext.instance + ) + + @Test + fun `given no active session, when creating properties, then returns null`() { + val result = lateInitUserProperties.createUserProperties() + + result shouldBeEqualTo null + } + + @Test + fun `given a teams use case set on an active session, when creating properties, then includes the remapped WorkMessaging selection`() { + fakeVectorStore.givenUseCase(FtueUseCase.TEAMS) + fakeActiveSessionDataSource.setActiveSession(fakeSession) + + val result = lateInitUserProperties.createUserProperties() + + result shouldBeEqualTo UserProperties( + ftueUseCaseSelection = UserProperties.FtueUseCaseSelection.WorkMessaging + ) + } +} + ``` + +##### ViewModel + +- `ViewModels` tend to be one of the most complex areas to unit test due to their position as a coordinator of data flows and bridge between domains. +- As the project uses a slightly tweaked`MvRx`, our API for the `ViewModel` is simplified down to `input - ViewModel.handle(Action)` + and `output Flows - ViewModel.viewEvents & ViewModel.stateFlow`. A `ViewModel` test asserter has been created to further simplify the process. + +```kotlin +class ViewModelTest { + + private var initialState = ViewState.Empty + + @get:Rule + val mavericksTestRule = MavericksTestRule(testDispatcher = UnconfinedTestDispatcher()) + + @Test + fun `when handling MyAction, then emits Loading and Content states`() { + val viewModel = ViewModel(initialState) + val test = viewModel.test() // must be invoked before interacting with the VM + + viewModel.handle(MyAction) + + test + .assertViewStates(initialState, State.Loading, State.Content()) + .assertNoEvents() + .finish() + } +} +``` + +- `ViewModels` often emit multiple states which are copies of the previous state, the `test` extension `assertStatesChanges` allows only the difference to be + supplied. + +```kotlin +data class ViewState(val name: String? = null, val age: Int? = null) +val initialState = ViewState() +val viewModel = ViewModel(initialState) +val test = viewModel.test() + +viewModel.handle(ChangeNameAction("Gandalf")) + +test + .assertStatesChanges( + initialState, + { copy(name = "Gandalf") }, + ) + .finish() +``` From 49db79ecad2f554717a7f1366b837c9fd18f7dae Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 18 Jan 2023 09:12:35 +0100 Subject: [PATCH 05/96] Update doc --- docs/_developer_onboarding.md | 75 ++++--- docs/analytics.md | 19 +- docs/danger.md | 2 +- docs/database_migration_test.md | 55 ----- docs/design.md | 2 + docs/flipper.md | 2 + docs/identity_server.md | 106 ---------- docs/installing_from_ci.md | 7 +- docs/jitsi.md | 96 --------- docs/nightly_build.md | 8 +- docs/pull_request.md | 4 +- docs/ui-tests.md | 193 ------------------ docs/unit_testing.md | 351 -------------------------------- docs/usefulLinks.md | 22 -- 14 files changed, 60 insertions(+), 882 deletions(-) delete mode 100644 docs/database_migration_test.md delete mode 100644 docs/identity_server.md delete mode 100644 docs/jitsi.md delete mode 100644 docs/ui-tests.md delete mode 100644 docs/unit_testing.md delete mode 100644 docs/usefulLinks.md diff --git a/docs/_developer_onboarding.md b/docs/_developer_onboarding.md index 3081d36a44..9c1dc5da93 100644 --- a/docs/_developer_onboarding.md +++ b/docs/_developer_onboarding.md @@ -10,9 +10,9 @@ * [Sync](#sync) * [The Android project](#the-android-project) * [Application](#application) - * [Anvil](#anvil) - * [Node](#node) - * [Other frameworks](#other-frameworks) + * [Global architecture](#global-architecture) + * [Jetpack Compose](#jetpack-compose) + * [Template](#template) * [Push](#push) * [Dependencies management](#dependencies-management) * [Test](#test) @@ -103,31 +103,43 @@ Here is the current module dependency graph: ### Application -(Note: to update) +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. -This is the UI part of the project. +The application is responsible to store the session credentials though. -There are two variants of the application: `Gplay` and `Fdroid`. +#### Global architecture -The main difference is about using Firebase on `Gplay` variant, to have Push from Google Services. `FDroid` variant cannot contain closed source dependency. +Main libraries and frameworks used in this application: -`Fdroid` is using background polling to lack the missing of Pushed. Now a solution using UnifiedPush has ben added to the project. See refer to [the dedicated documentation](./unifiedpush.md) for more details. +- 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) -#### Anvil +Some patterns are inspired by [Circuit](https://slackhq.github.io/circuit/) -TODO +**Note**: No more Android Architecture Component ViewModel, no more Mavericks, and of course no more Epoxy! -##### Node +#### Jetpack Compose -TODO +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 -#### Other frameworks +About Preview +- https://alexzh.com/jetpack-compose-preview/ -- Dependency injection is managed by [Dagger](https://dagger.dev/) (SDK) and [Hilt](https://developer.android.com/training/dependency-injection/hilt-android) (App); +#### 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 -Please see the dedicated documentation for more details. +**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: @@ -137,19 +149,23 @@ This is the classical scenario: ### Dependencies management -TODO Update -All the dependencies are declared in `build.gradle` files. But some versions are declared in [this dedicated file](../dependencies.gradle). +We are using [Gradle version catalog](https://docs.gradle.org/current/userguide/platforms.html#sub:central-declaration-of-dependencies) on this project. -When adding a new dependency, you will have to update the file [dependencies_groups.gradle](../dependencies_groups.gradle) to allow the dependency to be downloaded from the artifact repository. Sometimes sub-dependencies need to be added too, until the project can compile. +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. -dependencies_group, gradle files, Dependabot, etc. +**Note** Dependabot does not support yet Gradle verrsion catalog. This is tracked by [this issue](https://github.com/dependabot/dependabot-core/issues/3121). ### Test -Please refer to [this dedicated document](./ui-tests.md). +We have 3 tests frameworks in place: -TODO add link to the dedicated screenshot test documentation +- 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 @@ -167,17 +183,16 @@ because automatic tag (= class name) will not be available on the release versio Also generally it is recommended to provide the `Throwable` to the Timber log functions. -Last point, not that `Timber.v` function may have no effect on some devices. Prefer using `Timber.d` and up. +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 report can contain: +Bug reports can contain: - a screenshot of the current application state - the application logs from up to 15 application starts - the logcat logs -- the key share history (crypto data) 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. @@ -185,13 +200,13 @@ Rageshake can be very useful to get logs from a release version of the applicati ### Tips -- Element Android has a `developer mode` in the `Settings/Advanced settings`. Other useful options are available here; -- 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; -- Type `/devtools` in a Room composer to access a developer menu. There are some other entry points. Developer mode has to be enabled; -- 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; -- Using logcat, filtering with `onResume` 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. +- 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. +- 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! diff --git a/docs/analytics.md b/docs/analytics.md index 9b2c176912..b3f592c227 100644 --- a/docs/analytics.md +++ b/docs/analytics.md @@ -2,23 +2,10 @@ -* [Solution](#solution) -* [How to add a new Event](#how-to-add-a-new-event) -* [Forks of Element](#forks-of-element) +* [TODO](#todo) -## Solution +## TODO -Element is using PostHog to send analytics event. -We ask for the user to give consent before sending any analytics data. - -## How to add a new Event - -The analytics plan is shared between all Element clients. To add an Event, please open a PR to this project: https://github.com/matrix-org/matrix-analytics-events - -Then, once the PR has been merged, you can run the tool `import_analytic_plan.sh` to import the plan to Element, and then you can use the new Event. Note that this tool is run by Github action once a week. - -## Forks of Element - -Analytics on forks are disabled by default. Please refer to AnalyticsConfig and there implementation to setup analytics on your project. +There is no analytics in the project yet. diff --git a/docs/danger.md b/docs/danger.md index 34baa62e9e..e6fa74dec2 100644 --- a/docs/danger.md +++ b/docs/danger.md @@ -25,7 +25,7 @@ Here are the checks that Danger does so far: - 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 +- 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 diff --git a/docs/database_migration_test.md b/docs/database_migration_test.md deleted file mode 100644 index f7844abde8..0000000000 --- a/docs/database_migration_test.md +++ /dev/null @@ -1,55 +0,0 @@ - - -* [Testing database migration](#testing-database-migration) - * [Creating a reference database](#creating-a-reference-database) - * [Testing](#testing) - - - -## Testing database migration - -### Creating a reference database - -Databases are encrypted, the key to decrypt is needed to setup the test. -A special build property must be enabled to extract it. - -Set `vector.debugPrivateData=true` in `~/.gradle/gradle.properties` (to avoid committing by mistake) - -Launch the app in your emulator, login and use the app to fill up the database. - -Save the key for the tested database -``` -RealmKeysUtils W Database key for alias `session_db_fe9f212a611ccf6dea1141777065ed0a`: 935a6dfa0b0fc5cce1414194ed190.... -RealmKeysUtils W Database key for alias `crypto_module_fe9f212a611ccf6dea1141777065ed0a`: 7b9a21a8a311e85d75b069a343..... -``` - - -Use the [Device File Explorer](https://developer.android.com/studio/debug/device-file-explorer) to extrat the database file from the emulator. - -Go to `data/data/im.vector.app.debug/files//` -Pick the database you want to test (name can be found in SessionRealmConfigurationFactory): - - crypto_store.realm for crypto - - disk_store.realm for session - - etc... - -Download the file on your disk - -### Testing - -Copy the file in `src/AndroidTest/assets` - -see `CryptoSanityMigrationTest` or `RealmSessionStoreMigration43Test` for sample tests. - -There are already some databases in the assets folder. -The existing test will properly detect schema changes, and fail with such errors if a migration is missing: - -``` -io.realm.exceptions.RealmMigrationNeededException: Migration is required due to the following errors: -- Property 'CryptoMetadataEntity.foo' has been added. -``` - -If you want to test properly more complex database migration (dynamic transforms) ensure that the database contains -the entity you want to migrate. - -You can explore the database with [realm studio](https://www.mongodb.com/docs/realm/studio/) if needed. - diff --git a/docs/design.md b/docs/design.md index f390725560..c46ee0e84d 100644 --- a/docs/design.md +++ b/docs/design.md @@ -26,6 +26,8 @@ +**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 diff --git a/docs/flipper.md b/docs/flipper.md index 495425ce5f..3ede93b212 100644 --- a/docs/flipper.md +++ b/docs/flipper.md @@ -12,6 +12,8 @@ +# TODO Documentation to update when working on #40 + ## Introduction [Flipper](https://fbflipper.com) is a powerful tool from Meta, which allow to inspect the running application details and states from your computer. diff --git a/docs/identity_server.md b/docs/identity_server.md deleted file mode 100644 index a1ee86cb19..0000000000 --- a/docs/identity_server.md +++ /dev/null @@ -1,106 +0,0 @@ -# Identity server - - - -* [Introduction](#introduction) -* [Implementation](#implementation) -* [Related MSCs](#related-mscs) -* [Steps and requirements](#steps-and-requirements) -* [Screens](#screens) - * [Settings](#settings) - * [Discovery screen](#discovery-screen) - * [Set identity server screen](#set-identity-server-screen) -* [Ref:](#ref:) - - - -Issue: #607 -PR: #1354 - -## Introduction -Identity servers support contact discovery on Matrix by letting people look up Third Party Identifiers to see if the owner has publicly linked them with their Matrix ID. - -## Implementation - -The current implementation was Inspired by the code from Riot-Android. - -Difference though (list not exhaustive): -- Only API v2 is supported (see https://matrix.org/docs/spec/identity_service/latest) -- Homeserver has to be up to date to support binding (Versions.isLoginAndRegistrationSupportedBySdk() has to return true) -- The SDK managed the session and client secret when binding ThreePid. Those data are not exposed to the client. -- The SDK supports incremental sendAttempt (this is not used by Element) -- The "Continue" button is now under the information, and not as the same place that the checkbox -- The app can cancel a binding. Current data are erased from DB. -- The API (IdentityService) is improved. -- A new DB to store data related to the identity server management. - -Missing features (list not exhaustive): -- Invite by 3Pid (will be in a dedicated PR) -- Add email or phone to account (not P1, can be done on Element-Web) -- List email and phone of the account (could be done in a dedicated PR) -- Search contact (not P1) -- Logout from identity server when user sign out or deactivate his account. - -## Related MSCs -The list can be found here: https://matrix.org/blog/2019/09/27/privacy-improvements-in-synapse-1-4-and-riot-1-4 - -## Steps and requirements - -- Only one identity server by account can be set. The user's choice is stored in account data with key `m.identity_server`. But every clients will managed its own token to log in to the identity server -```json -{ - "type": "m.identity_server", - "content": { - "base_url": "https://matrix.org" - } -} -``` -- The accepted terms are stored in the account data: -```json -{ - "type": "m.accepted_terms", - "content": { - "accepted": [ - "https://vector.im/identity-server-privacy-notice-1" - ] - } -} -``` - -- Default identity server URL, from Wellknown data is proposed to the user. -- Identity server can be set -- Identity server can be changed on another user's device, so when the change is detected (thanks to account data sync) Element should properly disconnect from a previous identity server (I think it was not the case in Riot-Android, where we keep the token forever) -- Registration to the identity server is managed with an openId token -- Terms of service can be accepted when configuring the identity server. -- Terms of service can be accepted after, if they change. -- Identity server can be modified -- Identity server can be disconnected with a warning dialog, with special content if there are current bound 3pid on this identity server. -- Email can be bound -- Email can be unbound -- Phone can be bound -- Phone can be unbound -- Look up can be performed, to get matrixIds from local contact book (phone and email): Android permission correctly handled (not done yet) -- Look up pepper can be updated if it is rotated on the identity server -- Invitation using 3PID can be done (See #548) (not done yet) -- Homeserver access-token will never be sent to an identity server -- When user sign-out: logout from the identity server if any. -- When user deactivate account: logout from the identity server if any. - -## Screens - -### Settings - -Identity server settings can be accessed from the internal setting of the application, both from "Discovery" section and from identity detail section. - -### Discovery screen - -This screen displays the identity server configuration and the binding of the user's ThreePid (email and msisdn). This is the main screen of the feature. - -### Set identity server screen - -This screen is a form to set a new identity server URL - -## Ref: -- https://matrix.org/blog/2019/09/27/privacy-improvements-in-synapse-1-4-and-riot-1-4 is a good summary of the role of an identity server and the proper way to configure and use it in respect to the privacy and the consent of the user. -- API documentation: https://matrix.org/docs/spec/identity_service/latest -- vector.im TOS: https://vector.im/identity-server-privacy-notice diff --git a/docs/installing_from_ci.md b/docs/installing_from_ci.md index 01fb4afef2..634ee905ab 100644 --- a/docs/installing_from_ci.md +++ b/docs/installing_from_ci.md @@ -2,7 +2,6 @@ - * [Installing from Buildkite](#installing-from-buildkite) * [Installing from GitHub](#installing-from-github) * [Create a GitHub token](#create-a-github-token) * [Provide artifact URL](#provide-artifact-url) @@ -13,12 +12,10 @@ Installing APK build by the CI is possible -### Installing from Buildkite - -The script `./tools/install/installFromBuildkite.sh` can be used, but Builkite will be removed soon. See next section. - ### 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 diff --git a/docs/jitsi.md b/docs/jitsi.md deleted file mode 100644 index d6c93c49aa..0000000000 --- a/docs/jitsi.md +++ /dev/null @@ -1,96 +0,0 @@ -# Jitsi in Element Android - - - -* [Native Jitsi SDK](#native-jitsi-sdk) - * [How to build the Jitsi Meet SDK](#how-to-build-the-jitsi-meet-sdk) - * [Jitsi version](#jitsi-version) - * [Run the build script](#run-the-build-script) - * [Link with the new generated library](#link-with-the-new-generated-library) - * [Sanity tests](#sanity-tests) - * [Export the build library](#export-the-build-library) - - - -Native Jitsi support has been added to Element Android by the PR [#1914](https://github.com/vector-im/element-android/pull/1914). The description of the PR contains some documentation about the behaviour in each possible room configuration. - -Also, ensure to have a look on [the documentation from Element Web](https://github.com/vector-im/element-web/blob/develop/docs/jitsi.md) - -The official documentation about how to integrate the Jitsi SDK in an Android app is available here: https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-android-sdk. - -## Native Jitsi SDK - -The Jitsi SDK is built by ourselves with the flag LIBRE_BUILD, to be able to be integrated on the F-Droid version of Element Android. - -The generated maven repository is then host in the project https://github.com/vector-im/jitsi_libre_maven - -### How to build the Jitsi Meet SDK - -#### Jitsi version - -Update the script `./tools/jitsi/build_jisti_libs.sh` with the tag of the project `https://github.com/jitsi/jitsi-meet`. - -Latest tag can be found from this page: https://github.com/jitsi/jitsi-meet-release-notes/blob/master/CHANGELOG-MOBILE-SDKS.md - -Currently we are building the version with the tag `android-sdk-3.10.0`. - -#### Run the build script - -At the root of the Element Android, run the following script: - -```shell script -./tools/jitsi/build_jisti_libs.sh -``` - -It will build the Jitsi Meet Android library and put every generated files in the folder `/tmp/jitsi` - -#### Link with the new generated library - -- Update the file `./build.gradle` to use the previously created local Maven repository. Currently we have this line: - -```groovy -url "https://github.com/vector-im/jitsi_libre_maven/raw/master/android-sdk-3.10.0" -``` - -You can uncomment and update the line starting with `// url "file://...` and comment the line starting with `url`, to test the library using the locally generated Maven repository. - -- Update the dependency of the Jitsi Meet library in the file `./vector/build.gradle`. Currently we have this line: - -```groovy -implementation('org.jitsi.react:jitsi-meet-sdk:3.10.0') -``` - -- Update the dependency of the WebRTC library in the file `./vector/build.gradle`. Currently we have this line: - -```groovy -implementation('com.facebook.react:react-native-webrtc:1.92.1-jitsi-9093212@aar') -``` - -- Perform a gradle sync and build the project -- Perform test - -#### Sanity tests - -In order to validate that the upgrade of the Jitsi and WebRTC dependency does not break anything, the following sanity tests have to be performed, using two devices: -- Make 1-1 audio call (so using WebRTC) -- Make 1-1 video call (so using WebRTC) -- Create and join a conference call with audio only (so using Jitsi library). Leave the conference. Join it again. -- Create and join a conference call with audio and video (so using Jitsi library) Leave the conference. Join it again. - -#### Export the build library - -If all the tests are passed, you can export the generated Jitsi library to our Maven repository. - -- Clone the project https://github.com/vector-im/jitsi_libre_maven. -- Create a new folder with the version name. -- Copy every generated files form `/tmp/jitsi` to the folder you have just created. -- Commit and push the change on https://github.com/vector-im/jitsi_libre_maven. -- Update the file `./build.gradle` to use the previously created Maven repository. Currently we have this line: - -```groovy -url "https://github.com/vector-im/jitsi_libre_maven/raw/master/android-sdk-3.10.0" -``` - -- Build the project and perform the sanity tests again. - -- Create a PR for project Element Android and add a changelog file `.misc` to notify about the library upgrade. diff --git a/docs/nightly_build.md b/docs/nightly_build.md index 77cc676c7f..9abd59a67b 100644 --- a/docs/nightly_build.md +++ b/docs/nightly_build.md @@ -10,19 +10,17 @@ ## Configuration -The nightly build will contain what's on develop, in release mode, for Gplay variant. It is signed using a dedicated signature, and has a dedicated appId (`im.vector.app.nightly`), so it can be installed along with the production version of Element Android. The only other difference compared to Element Android is a different app icon background. 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. +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 Element Android, and fallback to Element 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`. +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 -Provide your email to the Android team, who will add it to the list "External testers" on Firebase. You will then receive an invite on the provided email. - -Follow the instructions on the email to install the latest nightly build. This is not clear yet if new nightly build will be automatically installed or not. +Click on this link and follow the instruction: [https://appdistribution.firebase.dev/i/7de2dbc61e7fb2a6](https://appdistribution.firebase.dev/i/7de2dbc61e7fb2a6) ## Build nightly manually diff --git a/docs/pull_request.md b/docs/pull_request.md index 7f6ac86b8a..2c517039e2 100644 --- a/docs/pull_request.md +++ b/docs/pull_request.md @@ -86,13 +86,13 @@ Exceptions can occur: 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-android-reviewers](https://github.com/orgs/vector-im/teams/element-android-reviewers) team as a reviewer. +- 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-android-reviewers](https://github.com/orgs/vector-im/teams/element-android-reviewers) or any member directly. +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 diff --git a/docs/ui-tests.md b/docs/ui-tests.md deleted file mode 100644 index d0b9db6818..0000000000 --- a/docs/ui-tests.md +++ /dev/null @@ -1,193 +0,0 @@ -# Automate user interface tests - -Element Android ensures that some fundamental flows are properly working by running automated user interface tests. -Ui tests are using the android [Espresso](https://developer.android.com/training/testing/espresso) library. - -Tests can be run on a real device, or on a virtual device (such as the emulator in Android Studio). - -Currently the test are covering a small set of application flows: - - Registration - - Self verification via emoji - - Self verification via passphrase - - - -* [Prerequisites:](#prerequisites:) -* [Run the tests](#run-the-tests) - * [From the source code](#from-the-source-code) - * [From command line](#from-command-line) -* [Recipes](#recipes) - * [Wait for initial sync](#wait-for-initial-sync) - * [Accessing current activity](#accessing-current-activity) - * [Interact with other session](#interact-with-other-session) - * [Contributing to the UiAllScreensSanityTest](#contributing-to-the-uiallscreenssanitytest) - - - -## Prerequisites: - -Out of the box, the tests use one of the homeservers (located at http://localhost:8080) of the "Demo Federation of Homeservers" (https://github.com/matrix-org/synapse#running-a-demo-federation-of-synapses). - -You first need to follow instructions to set up Synapse in development mode at https://github.com/matrix-org/synapse#synapse-development. If you have already installed all dependencies, the steps are: - -```shell script -$ git clone https://github.com/matrix-org/synapse.git -$ cd synapse -$ virtualenv -p python3 env -$ source env/bin/activate -(env) $ python -m pip install --no-use-pep517 -e . -``` - -Every time you want to launch these test homeservers, type: - -```shell script -$ source env/bin/activate -(env) $ demo/start.sh --no-rate-limit -``` - -**Emulator/Device set up** - -When running the test via android studio on a device, you have to disable system animations in order for the test to work properly. - -First, ensure developer mode is enabled: - -- To enable developer options, tap the **Build Number** option 7 times. You can find this option in one of the following locations, depending on your Android version: - - - Android 9 (API level 28) and higher: **Settings > About Phone > Build Number** - - Android 8.0.0 (API level 26) and Android 8.1.0 (API level 26): **Settings > System > About Phone > Build Number** - - Android 7.1 (API level 25) and lower: **Settings > About Phone > Build Number** - -On your device, under **Settings > Developer options**, disable the following 3 settings: - -- Window animation scale -- Transition animation scale -- Animator duration scale - -## Run the tests - -Once Synapse is running, and an emulator is running, you can run the UI tests. - -### From the source code - -Click on the green arrow in front of each test. Clicking on the arrow in front of the test class, or from the package directory does not always work (Tests not found issue). - -### From command line - -````shell script -./gradlew vector:connectedGplayDebugAndroidTest -```` - -To run all the tests from the `vector` module. - -In case of trouble, you can try to uninstall the previous installed test APK first with this command: - -```shell script -adb uninstall im.vector.app.debug.test -``` -## Recipes - -We added some specific Espresso IdlingResources, and other utilities for matrix related tests - -### Wait for initial sync - -```kotlin -// Wait for initial sync and check room list is there -withIdlingResource(initialSyncIdlingResource(uiSession)) { - onView(withId(R.id.roomListContainer)) - .check(matches(isDisplayed())) -} -``` - -### Accessing current activity - -```kotlin - val activity = EspressoHelper.getCurrentActivity()!! - val uiSession = (activity as HomeActivity).activeSessionHolder.getActiveSession() -``` - -### Interact with other session - -It's possible to create a session via the SDK, and then use this session to interact with the one that the emulator is using (to check verifications for example) - -```kotlin -@Before -fun initAccount() { - val context = InstrumentationRegistry.getInstrumentation().targetContext - val matrix = Matrix.getInstance(context) - val userName = "foobar_${System.currentTimeMillis()}" - existingSession = createAccountAndSync(matrix, userName, password, true) -} -``` - -### Contributing to the UiAllScreensSanityTest - -The `UiAllScreensSanityTest` makes use of the Robot pattern in order to model pages, components and interactions. -Each Robot aims to return the UI back to its original state after the interaction, allowing for a reusable and consistent DSL. - -```kotlin -// launches and closes settings after executing the block -elementRobot.settings { - // whilst in the settings, launches and closes the advanced settings sub screen - advancedSettings { - // crawls all the pages within the advanced settings - crawl() - } -} - -// enables developer mode by navigating to the settings, enabling the toggle and then returning to the starting point to execute the block -// on block completion the Robot disables developer mode by navigating back to the settings and finally returning to the original starting point -elementRobot.withDeveloperMode { - // the same starting point as the example above - settings { - advancedSettings { crawlDeveloperOptions() } - } -} -``` - -The Robots used in the example above... - -```kotlin -class ElementRobot { - fun settings(block: SettingsRobot.() -> Unit) { - // double check we're where we think we are - waitUntilViewVisible(withId(R.id.bottomNavigationView)) - - // navigate to the settings - openDrawer() - clickOn(R.id.homeDrawerHeaderSettingsView) - - // execute the robot with the context of the settings screen - block(SettingsRobot()) - - // close the settings and ensure we're back at the starting point - pressBack() - waitUntilViewVisible(withId(R.id.bottomNavigationView)) - } - - fun withDeveloperMode(block: ElementRobot.() -> Unit) { - settings { toggleDeveloperMode() } - block() - settings { toggleDeveloperMode() } - } -} - -class SettingsRobot { - fun toggleDeveloperMode() { - advancedSettings { - toggleDeveloperMode() - } - } - - fun advancedSettings(block: SettingsAdvancedRobot.() -> Unit) { - clickOn(R.string.settings_advanced_settings) - block(SettingsAdvancedRobot()) - pressBack() - } -} - -class SettingsAdvancedRobot { - fun toggleDeveloperMode() { - clickOn(R.string.settings_developer_mode_summary) - } -} -``` diff --git a/docs/unit_testing.md b/docs/unit_testing.md deleted file mode 100644 index 95b78c7f5f..0000000000 --- a/docs/unit_testing.md +++ /dev/null @@ -1,351 +0,0 @@ -# Table of Contents - - - -* [Overview](#overview) - * [Best Practices](#best-practices) -* [Project Conventions](#project-conventions) - * [Setup](#setup) - * [Naming](#naming) - * [Format](#format) - * [Assertions](#assertions) - * [Constants](#constants) - * [Mocking](#mocking) - * [Fakes](#fakes) - * [Fixtures](#fixtures) - * [Examples](#examples) - * [Extensions used to streamline the test setup](#extensions-used-to-streamline-the-test-setup) - * [Fakes and Fixtures](#fakes-and-fixtures) - - - -## Overview - -Unit tests are a mechanism to validate our code executes the way we expect. They help to inform the design of our systems by requiring testability and -understanding, they describe the inner workings without relying on inline comments and protect from unexpected regressions. - -However, unit tests are not a magical solution to solve all our problems and come at a cost. Unreliable and hard to maintain tests often end up ignored, deleted -or worse, provide a false sense of security. - -### Best Practices - -Tests can be written in many ways, the main rule is to keep them simple and maintainable. Some ways to help achieve this are... - -- Break out logic into single units (following the Single Responsibility Principle) to reduce test complexity. -- Favour pure functions, avoiding mutable state. -- Prefer dependency injection to static calls to allow for simpler test setup. -- Write concise tests with a single function under test, clearly showing the inputs and expected output. -- Create separate test cases instead of changing parameters and grouping multiple assertions within a single test to help trace back failure causes (with the - exception of parameterised tests). -- Assert against entire models instead of subsets of properties to capture any possible changes within the test scope. -- Avoid invoking logic from production instances other than the class under test to guard from unrelated changes. -- Always inject `Dispatchers` and `Clock` instances and provide fake implementations for tests to avoid non deterministic results. - -## Project Conventions - -#### Setup - -- Test file and class name should be the class under test with the Test suffix, created in a `test` sourceset, with the same package name as the class under - test. -- Dependencies of the class are instantiated inline, junit will recreate the test class for each test run. -- A line break between the dependencies and class under test helps clarify the instance being tested. - -```kotlin - -class MyClassTest { - - private val fakeUppercaser = FakeUppercaser() - - // line break between the class under test and its dependencies - private val myClass = MyClass(fakeUppercaser.instance) -} - -``` - -#### Naming - -- Test names use the `Gherkin` format, `given, when, then` mapping to the input, logic under test and expected result. - - `given` - Uniqueness about the environment or dependencies in which the test case is running. _"given device is android 12 and supports dark mode"_ - - `when` - The action/function under test. _"when reading dark mode status"_ - - `then` - The expected result from the combination of _given_ and _when_. _"then returns dark mode enabled"_ -- Test names are written using kotlin back ticks to enable sentences _ish_. - -```kotlin -@Test -fun `given a lowercase label, when uppercasing, then returns label uppercased` -``` - -When the input is given directly to the _when_, this can also be represented as... - -```kotlin -@Test -fun `when uppercasing a lowercase label, then returns label uppercased` -``` - -Multiple given or returns statements can be used in the name although it could be a sign that the logic being tested does too much. - ---- - -#### Format - -- Test bodies are broken into sections through the use of blank lines where the sections correspond to the test name. -- Sections can span multiple lines. - -```kotlin -// comments are for illustrative purposes -/* given */ val lowercaseLabel = "hello world" - -/* when */ val result = textUppercaser.uppercase(lowercaseLabel) - -/* then */ result shouldBeEqualTo "HELLO WORLD" -``` - -- Functions extracted from test bodies are placed beneath all the unit tests. - ---- - -#### Assertions - -- Assertions against test results are made using [Kluent's](https://github.com/MarkusAmshove/Kluent) _fluent_ api. -- Typically `shouldBeEqualTo`is the main assertion to use for asserting function return values as by project convention we assert against entire objects or - lists. - -```kotlin -val result = listOf("hello", "world") - -// Fail -result shouldBeEqualTo listOf("hello") -``` - -```kotlin -data class Person(val age: Int, val name: String) - -val result = Person(age = 100, name = "Gandalf") - -// Avoid -result.age shouldBeEqualTo 100 - -// Prefer -result shouldBeEqualTo Person(age = 100, "Gandalf") -``` - -- Exception throwing can be asserted against using `assertFailsWith`. -- When asserting reusable exceptions, include the message to distinguish between them. - -```kotlin -assertFailsWith(message = "Details about error") { - // when section of the test - codeUnderTest() -} -``` - ---- - -#### Constants - -- Reusable values are extracted to file level immutable properties or constants. -- These can be parameters or expected results. -- The naming convention is to prefix with `A` or `AN` for better matching with the test name. - -```kotlin -private const val A_LOWERCASE_LABEL = "hello" - -class MyTest { - @Test - fun `when uppercasing a lowercase label, then returns label uppercased`() { - val result = TextUppercaser().uppercase(A_LOWERCASE_LABEL) - ... - } -} -``` - ---- - -#### Mocking - -- In order to provide different behaviour for dependencies within tests our main method is through mocking, using [Mockk](https://mockk.io/). -- We avoid using relaxed mocks in favour of explicitly declaring mock behaviour through the _Fake_ convention. There are exceptions when mocking framework - classes which would require a lot of boilerplate. -- Using `Spy` is discouraged as it inherently requires real instances, which we are avoiding in our tests. There are exceptions such as `VectorFeatures` which - acts like a `Fixture` in release builds. - ---- - -#### Fakes - -- Fakes are reusable instances of classes purely for testing purposes. They provide functions to replace the functions of the interface/class they're faking - with test specific values. -- When faking an interface, the _Fake_ can be written using delegation or by stubbing -- All Fakes currently reside in the same package `${package}.test.fakes` - -```kotlin -// Delegating to a mock -class FakeClock : Clock by mockk() { - fun givenEpoch(epoch: Long) { - every { epochMillis() } returns epoch - } -} - -// Stubbing the interface -class FakeClock(private val epoch: Long) : Clock { - override fun epochMillis() = epoch -} -``` - -It's currently more common for fakes to fake class behaviour, we achieve this by wrapping and exposing a mock instance. - -```kotlin -class FakeCursor { - val instance = mockk() - fun givenEmpty() { - every { instance.count } returns 0 - every { instance.moveToFirst() } returns false - } -} - -val fakeCursor = FakeCursor().apply { givenEmpty() } -``` - -#### Fixtures - -- Fixtures are a reusable wrappers around data models. They provide default values to make creating instances as easy as possible, with the option to override - specific parameters when needed. -- Are namespaced within an `object`. -- Reduces the _find usages_ noise when searching for usages of the origin class construction. -- All Fixtures currently reside in the same package `${package}.test.fixtures`. - -```kotlin -object ContentAttachmentDataFixture { - fun aContentAttachmentData( - type: ContentAttachmentData.Type.TEXT, - mimeType: String? = null - ) = ContentAttachmentData(type, mimeType) -} -``` - -- Fixtures can also be used to manage specific combinations of parameters - -```kotlin -fun aContentAttachmentAudioData() = aContentAttachmentData( - type = ContentAttachmentData.Type.AUDIO, - mimeType = "audio/mp3", -) -``` - ---- - -### Examples - -##### Extensions used to streamline the test setup - -```kotlin -class CircularCacheTest { - - @Test - fun `when putting more than cache size then cache is limited to cache size`() { - val (cache, internalData) = createIntCache(cacheSize = 3) - - cache.putInOrder(1, 1, 1, 1, 1, 1) - - internalData shouldBeEqualTo arrayOf(1, 1, 1) - } -} - -private fun createIntCache(cacheSize: Int): Pair, Array> { - var internalData: Array? = null - val factory: (Int) -> Array = { - Array(it) { null }.also { array -> internalData = array } - } - return CircularCache(cacheSize, factory) to internalData!! -} - -private fun CircularCache.putInOrder(vararg values: Int) { - values.forEach { put(it) } -} -``` - -##### Fakes and Fixtures - -```kotlin -class LateInitUserPropertiesFactoryTest { - - private val fakeActiveSessionDataSource = FakeActiveSessionDataSource() - private val fakeVectorStore = FakeVectorStore() - private val fakeContext = FakeContext() - private val fakeSession = FakeSession().also { - it.givenVectorStore(fakeVectorStore.instance) - } - - private val lateInitUserProperties = LateInitUserPropertiesFactory( - fakeActiveSessionDataSource.instance, - fakeContext.instance - ) - - @Test - fun `given no active session, when creating properties, then returns null`() { - val result = lateInitUserProperties.createUserProperties() - - result shouldBeEqualTo null - } - - @Test - fun `given a teams use case set on an active session, when creating properties, then includes the remapped WorkMessaging selection`() { - fakeVectorStore.givenUseCase(FtueUseCase.TEAMS) - fakeActiveSessionDataSource.setActiveSession(fakeSession) - - val result = lateInitUserProperties.createUserProperties() - - result shouldBeEqualTo UserProperties( - ftueUseCaseSelection = UserProperties.FtueUseCaseSelection.WorkMessaging - ) - } -} - ``` - -##### ViewModel - -- `ViewModels` tend to be one of the most complex areas to unit test due to their position as a coordinator of data flows and bridge between domains. -- As the project uses a slightly tweaked`MvRx`, our API for the `ViewModel` is simplified down to `input - ViewModel.handle(Action)` - and `output Flows - ViewModel.viewEvents & ViewModel.stateFlow`. A `ViewModel` test asserter has been created to further simplify the process. - -```kotlin -class ViewModelTest { - - private var initialState = ViewState.Empty - - @get:Rule - val mavericksTestRule = MavericksTestRule(testDispatcher = UnconfinedTestDispatcher()) - - @Test - fun `when handling MyAction, then emits Loading and Content states`() { - val viewModel = ViewModel(initialState) - val test = viewModel.test() // must be invoked before interacting with the VM - - viewModel.handle(MyAction) - - test - .assertViewStates(initialState, State.Loading, State.Content()) - .assertNoEvents() - .finish() - } -} -``` - -- `ViewModels` often emit multiple states which are copies of the previous state, the `test` extension `assertStatesChanges` allows only the difference to be - supplied. - -```kotlin -data class ViewState(val name: String? = null, val age: Int? = null) -val initialState = ViewState() -val viewModel = ViewModel(initialState) -val test = viewModel.test() - -viewModel.handle(ChangeNameAction("Gandalf")) - -test - .assertStatesChanges( - initialState, - { copy(name = "Gandalf") }, - ) - .finish() -``` 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 From f071b2c081edd92269c7163a64785f23f464a123 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 19 Jan 2023 11:53:31 +0100 Subject: [PATCH 06/96] This doc will be added in another PR. --- docs/screenshot_testing.md | 72 -------------------------------------- 1 file changed, 72 deletions(-) delete mode 100644 docs/screenshot_testing.md diff --git a/docs/screenshot_testing.md b/docs/screenshot_testing.md deleted file mode 100644 index 93b91cdf67..0000000000 --- a/docs/screenshot_testing.md +++ /dev/null @@ -1,72 +0,0 @@ -# Screenshot testing - - - -* [Overview](#overview) -* [Setup](#setup) -* [Recording](#recording) -* [Verifying](#verifying) -* [Contributing](#contributing) -* [Example](#example) - - - -## Overview - -- Screenshot tests are tests which record the content of a rendered screen and verify subsequent runs to check if the screen renders differently. -- Element uses [Paparazzi](https://github.com/cashapp/paparazzi) to render, record and verify android layouts. -- The screenshot verification occurs on every pull request as part of the `tests.yml` workflow. - -## Setup - -- Install Git LFS through your package manager of choice (`brew install git-lfs` | `yay -S git-lfs`). -- Install the Git LFS hooks into the project. - -```bash -# with element-android as the current working directory -git lfs install --local -``` - -- If installed correctly, `git push` and `git pull` will now include LFS content. - -## Recording - -- `./gradlew recordScreenshots` -- Paparazzi will generate images in `${module}/src/test/snapshots`, which will need to be committed to the repository using Git LFS. - -## Verifying - -- `./gradlew verifyScreenshots` -- In the case of failure, Paparazzi will generate images in `${module}/out/failure`. The images will show the expected and actual screenshots along with a delta of the two images. - -## Contributing - -- When creating a test, the file (and class) name names must include `ScreenshotTest`, eg `ItemScreenshotTest`. -- After creating the new test, record and commit the newly rendered screens. -- `./tools/validate_lfs` can be ran to ensure everything is working correctly with Git LFS, the CI also runs this check. - -## Example - -```kotlin -class PaparazziExampleScreenshotTest { - - @get:Rule - val paparazzi = Paparazzi( - deviceConfig = PIXEL_3, - theme = "Theme.Vector.Light", - ) - - @Test - fun `example paparazzi test`() { - // Inflate the layout - val view = paparazzi.inflate(R.layout.item_radio) - - // Bind data to the view - view.findViewById(R.id.actionTitle).text = paparazzi.resources.getString(R.string.room_settings_all_messages) - view.findViewById(R.id.radioIcon).setImageResource(R.drawable.ic_radio_on) - - // Record the bound view - paparazzi.snapshot(view) - } -} -``` From 4810464af981c9050283341ad43bafdb61d283d3 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 19 Jan 2023 18:10:05 +0100 Subject: [PATCH 07/96] Add some notes about architecture --- docs/_developer_onboarding.md | 42 +++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/docs/_developer_onboarding.md b/docs/_developer_onboarding.md index 9c1dc5da93..0ae4a138c3 100644 --- a/docs/_developer_onboarding.md +++ b/docs/_developer_onboarding.md @@ -86,7 +86,7 @@ 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 of the application; +- `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: @@ -96,6 +96,9 @@ A few details about some modules: - `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: @@ -107,6 +110,22 @@ This Android project mainly handle the application layer of the whole software. 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: @@ -117,18 +136,16 @@ Main libraries and frameworks used in this application: Some patterns are inspired by [Circuit](https://slackhq.github.io/circuit/) -**Note**: No more Android Architecture Component ViewModel, no more Mavericks, and of course no more Epoxy! +Here are the main points: -#### Jetpack Compose - -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/ +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 @@ -190,6 +207,7 @@ Last point, note that `Timber.v` function may have no effect on some devices. Pr 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 From 59615ec99b0e084f9d75e34c7268347b7b0493ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Feb 2023 22:12:36 +0000 Subject: [PATCH 08/96] Bump plugin.serialization from 1.8.0 to 1.8.10 Bumps [plugin.serialization](https://github.com/JetBrains/kotlin) from 1.8.0 to 1.8.10. - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/v1.8.10/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.8.0...v1.8.10) --- updated-dependencies: - dependency-name: plugin.serialization dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- libraries/matrix/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 { From 9aa03fe2d41fe87222e3ef4eb88fa55f83ab9509 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 3 Feb 2023 11:25:11 +0100 Subject: [PATCH 09/96] Remove unwanted doc. --- docs/flipper.md | 60 ------------------------------------------------- 1 file changed, 60 deletions(-) delete mode 100644 docs/flipper.md diff --git a/docs/flipper.md b/docs/flipper.md deleted file mode 100644 index 3ede93b212..0000000000 --- a/docs/flipper.md +++ /dev/null @@ -1,60 +0,0 @@ -# Flipper - - - -* [Introduction](#introduction) -* [Setup](#setup) - * [Troubleshoot](#troubleshoot) - * [No device found issue](#no-device-found-issue) - * [Diagnostic Activity](#diagnostic-activity) - * [Other](#other) -* [Links](#links) - - - -# TODO Documentation to update when working on #40 - -## Introduction - -[Flipper](https://fbflipper.com) is a powerful tool from Meta, which allow to inspect the running application details and states from your computer. - -Flipper is configured in the Element Android project to let the developers be able to: -- inspect all the Realm databases content; -- do layout inspection; -- see the crash logs; -- see the logcat; -- see all the network requests; -- see all the SharedPreferences; -- take screenshots and record videos of the device; -- and more! - -## Setup - -- Install Flipper on your computer. Follow instructions here: https://fbflipper.com/docs/getting-started/index/ -- Run the debug version of Element on an emulator or on a real device. - -### Troubleshoot - -#### No device found issue - -The configuration of the Flipper application has to be updated. The issue has been asked and answered here: https://stackoverflow.com/questions/71744103/android-emulator-unable-to-connect-to-flipper/72608113#72608113 - -#### Diagnostic Activity - -Flipper comes with a Diagnostic Activity that you can start from command line using: - -```shell -adb shell am start -n im.vector.app.debug/com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity -``` - -It provides some log which can help to figure out what's going on client side. - -#### Other - -https://fbflipper.com/docs/getting-started/troubleshooting/android/ may help. - -## Links - -- Official Flipper website: https://fbflipper.com -- Realm Plugin for Flipper: https://github.com/kamgurgul/Flipper-Realm -- Dedicated Matrix room: https://matrix.to/#/#unifiedpush:matrix.org From 616f71b440ba9f91c3a0290b2c4a03181fc1f00e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 3 Feb 2023 12:54:04 +0100 Subject: [PATCH 10/96] Knit --- docs/_developer_onboarding.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_developer_onboarding.md b/docs/_developer_onboarding.md index 0ae4a138c3..14edae584a 100644 --- a/docs/_developer_onboarding.md +++ b/docs/_developer_onboarding.md @@ -10,8 +10,8 @@ * [Sync](#sync) * [The Android project](#the-android-project) * [Application](#application) - * [Global architecture](#global-architecture) * [Jetpack Compose](#jetpack-compose) + * [Global architecture](#global-architecture) * [Template](#template) * [Push](#push) * [Dependencies management](#dependencies-management) From 35e204ca48078090a17da658a410bcf3585244f3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 25 Jan 2023 15:13:31 +0100 Subject: [PATCH 11/96] Cleanup --- .../messages/actionlist/model/TimelineItemAction.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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..88e931408c 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 @@ -25,9 +25,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) } From 78544357e8d6ddf5ddfdcb6f6522a9f49b6b71d3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 25 Jan 2023 16:09:31 +0100 Subject: [PATCH 12/96] Create ExtendedColors to define more colors. --- .../timeline/components/MessageEventBubble.kt | 26 ++----------- .../android/libraries/designsystem/Color.kt | 6 --- .../libraries/designsystem/ExtendedColors.kt | 39 +++++++++++++++++++ .../android/libraries/designsystem/Theme.kt | 17 +++++++- 4 files changed, 59 insertions(+), 29 deletions(-) create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/ExtendedColors.kt 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..b8922be78c 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 @@ -30,13 +30,7 @@ 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.LocalExtendedColors private val BUBBLE_RADIUS = 16.dp @@ -88,24 +82,12 @@ fun MessageEventBubble( } val backgroundBubbleColor = if (isHighlighted) { - if (LocalIsDarkTheme.current) { - MessageHighlightDark - } else { - MessageHighlightLight - } + LocalExtendedColors.current.messageHighlightedBackground } else { if (isMine) { - if (LocalIsDarkTheme.current) { - SystemGrey5Dark - } else { - SystemGrey5Light - } + LocalExtendedColors.current.messageFromMeBackground } else { - if (LocalIsDarkTheme.current) { - SystemGrey6Dark - } else { - SystemGrey6Light - } + LocalExtendedColors.current.messageFromOtherBackground } } val bubbleShape = bubbleShape() 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/ExtendedColors.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/ExtendedColors.kt new file mode 100644 index 0000000000..9cdda9cb73 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/ExtendedColors.kt @@ -0,0 +1,39 @@ +/* + * 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 + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color + +@Immutable +data class ExtendedColors( + val messageFromMeBackground: Color, + val messageFromOtherBackground: Color, + val messageHighlightedBackground: Color, +) + +internal val extendedColorsLight = ExtendedColors( + messageFromMeBackground = SystemGrey5Light, + messageFromOtherBackground = SystemGrey6Light, + messageHighlightedBackground = Azure, +) + +internal val extendedColorsDark = ExtendedColors( + messageFromMeBackground = SystemGrey5Dark, + messageFromOtherBackground = SystemGrey6Dark, + messageHighlightedBackground = Azure, +) 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.kt index d2168f644e..09bb595599 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.kt @@ -27,6 +27,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.SideEffect import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import com.google.accompanist.systemuicontroller.rememberSystemUiController @@ -64,9 +65,18 @@ private val LightColorScheme = lightColorScheme( onSurface = Color(0xFF1C1B1F), */ ) + @Suppress("CompositionLocalAllowlist") val LocalIsDarkTheme = compositionLocalOf { error("Not defined") } +val LocalExtendedColors = staticCompositionLocalOf { + ExtendedColors( + messageFromMeBackground = Color.Unspecified, + messageFromOtherBackground = Color.Unspecified, + messageHighlightedBackground = Color.Unspecified, + ) +} + @Composable fun ElementXTheme( darkTheme: Boolean = isSystemInDarkTheme(), @@ -95,7 +105,12 @@ fun ElementXTheme( ) } - CompositionLocalProvider(LocalIsDarkTheme provides darkTheme) { + val extendedColors = if (darkTheme) extendedColorsDark else extendedColorsLight + + CompositionLocalProvider( + LocalIsDarkTheme provides darkTheme, + LocalExtendedColors provides extendedColors, + ) { MaterialTheme( colorScheme = colorScheme, typography = Typography, From e4cc733f4a07a9fa621950beecdcc4083dd0ee6b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 26 Jan 2023 15:15:51 +0100 Subject: [PATCH 13/96] First draft of full custom theme. ElementTheme everywhere Create ElementButton --- .../io/element/android/x/MainActivity.kt | 10 +- .../android/x/component/ShowkaseButton.kt | 4 +- .../login/changeserver/ChangeServerView.kt | 19 ++- .../features/login/root/LoginRootScreen.kt | 17 ++- .../android/features/messages/MessagesView.kt | 8 +- .../messages/actionlist/ActionListView.kt | 6 +- .../textcomposer/MessageComposerView.kt | 4 +- .../messages/timeline/TimelineView.kt | 10 +- .../timeline/components/MessageEventBubble.kt | 12 +- .../components/TimelineItemImageView.kt | 4 +- .../components/TimelineItemInformativeView.kt | 6 +- .../components/TimelineItemReactionsView.kt | 12 +- .../timeline/components/html/HtmlDocument.kt | 42 +++--- .../features/onboarding/OnBoardingScreen.kt | 29 ++--- .../rageshake/bugreport/BugReportView.kt | 10 +- .../android/features/roomlist/RoomListView.kt | 12 +- .../roomlist/components/RoomListTopBar.kt | 28 ++-- .../roomlist/components/RoomSummaryRow.kt | 8 +- .../libraries/designsystem/ColorUtil.kt | 6 +- .../libraries/designsystem/ExtendedColors.kt | 39 ------ .../android/libraries/designsystem/Theme.kt | 120 ------------------ .../android/libraries/designsystem/Type.kt | 42 +----- .../components/LabelledCheckbox.kt | 3 +- .../designsystem/components/ProgressDialog.kt | 8 +- .../components/dialogs/ConfirmationDialog.kt | 25 +++- .../components/dialogs/ErrorDialog.kt | 21 ++- .../preferences/PreferenceCategory.kt | 6 +- .../preferences/PreferenceScreen.kt | 6 +- .../components/preferences/PreferenceSlide.kt | 9 +- .../preferences/PreferenceSwitch.kt | 7 +- .../components/preferences/PreferenceText.kt | 4 +- .../designsystem/theme/ColorsDark.kt | 53 ++++++++ .../designsystem/theme/ColorsLight.kt | 54 ++++++++ .../designsystem/theme/ContentColor.kt | 42 ++++++ .../designsystem/theme/ElementColors.kt | 118 +++++++++++++++++ .../ElementSpaces.kt} | 26 ++-- .../designsystem/theme/ElementTheme.kt | 84 ++++++++++++ .../designsystem/theme/ElementTypography.kt | 118 +++++++++++++++++ .../theme/components/ElementButton.kt | 66 ++++++++++ .../theme/components/ElementCheckbox.kt | 55 ++++++++ .../theme/components/ElementMediumAppBar.kt | 56 ++++++++ .../theme/components/ElementScaffold.kt | 57 +++++++++ .../theme/components/ElementSlider.kt | 63 +++++++++ .../theme/components/ElementSurface.kt | 52 ++++++++ .../theme/components/ElementTopAppBar.kt | 56 ++++++++ .../matrix/ui/components/MatrixUserHeader.kt | 4 +- .../matrix/ui/components/MatrixUserRow.kt | 4 +- .../android/tests/uitests/ShowkaseButton.kt | 3 +- .../android/tests/uitests/ScreenshotTest.kt | 7 +- .../tests/uitests/TypographyTestPreview.kt | 4 +- 50 files changed, 1082 insertions(+), 377 deletions(-) delete mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/ExtendedColors.kt delete mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/Theme.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorsDark.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorsLight.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ContentColor.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementColors.kt rename libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/{components/VectorButton.kt => theme/ElementSpaces.kt} (52%) create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementTheme.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementTypography.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementButton.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementCheckbox.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementMediumAppBar.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementScaffold.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementSlider.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementSurface.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementTopAppBar.kt 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..eb249d5b75 100644 --- a/app/src/main/kotlin/io/element/android/x/MainActivity.kt +++ b/app/src/main/kotlin/io/element/android/x/MainActivity.kt @@ -19,15 +19,14 @@ package io.element.android.x import android.os.Bundle import androidx.activity.compose.setContent 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.components.ElementSurface +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 { + ElementSurface( 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..7a9ffe9791 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.ElementButton @Composable internal fun ShowkaseButton( @@ -36,7 +36,7 @@ internal fun ShowkaseButton( modifier: Modifier = Modifier, ) { if (isVisible) { - Button( + ElementButton( modifier = modifier .padding(top = 32.dp, start = 16.dp), onClick = onClick 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..a8ea2de0fb 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 @@ -33,12 +33,9 @@ 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 @@ -57,6 +54,9 @@ 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.theme.ElementTheme +import io.element.android.libraries.designsystem.theme.components.ElementButton +import io.element.android.libraries.designsystem.theme.components.ElementSurface import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag @@ -66,9 +66,8 @@ fun ChangeServerView( modifier: Modifier = Modifier, onChangeServerSuccess: () -> Unit = {}, ) { - Surface( + ElementSurface( modifier = modifier, - color = MaterialTheme.colorScheme.background, ) { val eventSink = state.eventSink val scrollState = rememberScrollState() @@ -92,7 +91,7 @@ fun ChangeServerView( .size(width = 81.dp, height = 73.dp) .align(Alignment.CenterHorizontally) .background( - color = MaterialTheme.colorScheme.surfaceVariant, + color = ElementTheme.colors.surfaceVariant, shape = RoundedCornerShape(32.dp) ) ) { @@ -124,7 +123,7 @@ fun ChangeServerView( .padding(top = 16.dp), textAlign = TextAlign.Center, fontSize = 16.sp, - color = MaterialTheme.colorScheme.secondary + color = ElementTheme.colors.secondary ) var homeserverFieldState by textFieldState(stateValue = state.homeserver) OutlinedTextField( @@ -155,12 +154,12 @@ fun ChangeServerView( state.homeserver, state.changeServerAction.error ), - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall, + color = ElementTheme.colors.error, + style = ElementTheme.typography.bodySmall, modifier = Modifier.padding(start = 16.dp) ) } - Button( + ElementButton( onClick = { eventSink(ChangeServerEvents.Submit) }, enabled = state.submitEnabled, modifier = Modifier 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..1511648c74 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 @@ -32,14 +32,11 @@ 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 @@ -60,6 +57,9 @@ 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.theme.ElementTheme +import io.element.android.libraries.designsystem.theme.components.ElementButton +import io.element.android.libraries.designsystem.theme.components.ElementSurface import io.element.android.libraries.matrix.core.SessionId import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag @@ -74,9 +74,8 @@ fun LoginRootScreen( onLoginWithSuccess: (SessionId) -> Unit = {}, ) { val eventSink = state.eventSink - Surface( + ElementSurface( modifier = modifier, - color = MaterialTheme.colorScheme.background, ) { Box( modifier = Modifier @@ -125,7 +124,7 @@ fun LoginRootScreen( keyboardType = KeyboardType.Uri, ), ) - Button( + ElementButton( onClick = onChangeServer, modifier = Modifier .align(Alignment.CenterEnd) @@ -195,14 +194,14 @@ fun LoginRootScreen( if (state.loggedInState is LoggedInState.ErrorLoggingIn) { Text( text = loginError(state.formState, state.loggedInState.failure), - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall, + color = ElementTheme.colors.error, + style = ElementTheme.typography.bodySmall, modifier = Modifier.padding(start = 16.dp) ) } } // Submit - Button( + ElementButton( onClick = { eventSink(LoginRootEvents.Submit) }, enabled = state.submitEnabled, modifier = Modifier 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 706dc48c43..5baad6bc9d 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 @@ -21,6 +21,7 @@ package io.element.android.features.messages +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -64,6 +65,9 @@ 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.ElementTheme +import io.element.android.libraries.designsystem.theme.components.ElementScaffold +import io.element.android.libraries.designsystem.theme.components.ElementTopAppBar import io.element.android.libraries.designsystem.utils.LogCompositions import kotlinx.coroutines.launch import timber.log.Timber @@ -101,7 +105,7 @@ fun MessagesView( state.eventSink(MessagesEvents.HandleAction(action, messageEvent)) } - Scaffold( + ElementScaffold( modifier = modifier, contentWindowInsets = WindowInsets.statusBars, topBar = { @@ -172,7 +176,7 @@ fun MessagesViewTopBar( modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, ) { - TopAppBar( + ElementTopAppBar( modifier = modifier, navigationIcon = { IconButton(onClick = onBackPressed) { 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 53df8d4b40..43414e732c 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,7 +29,6 @@ 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 @@ -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.ElementTheme import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch @@ -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) ElementTheme.colors.error else Color.Unspecified, ) }, icon = { VectorIcon( resourceId = action.icon, - tint = if (action.destructive) MaterialTheme.colors.error else LocalContentColor.current, + tint = if (action.destructive) ElementTheme.colors.error else LocalContentColor.current, ) } ) 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 a73c724448..fdcf3037b6 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 @@ -42,7 +42,6 @@ import androidx.compose.material.icons.filled.ArrowDownward import androidx.compose.material3.CircularProgressIndicator 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 @@ -79,6 +78,7 @@ import io.element.android.features.messages.timeline.model.content.TimelineItemT import io.element.android.features.messages.timeline.model.content.TimelineItemUnknownContent 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.ElementTheme import io.element.android.libraries.designsystem.utils.PairCombinedPreviewParameter import io.element.android.libraries.matrix.core.EventId import kotlinx.collections.immutable.ImmutableList @@ -267,7 +267,7 @@ private fun MessageSenderInformation( } Text( text = sender, - style = MaterialTheme.typography.titleMedium, + style = ElementTheme.typography.titleMedium, modifier = Modifier .alignBy(LastBaseline) ) @@ -326,8 +326,8 @@ internal fun BoxScope.TimelineScrollHelper( modifier = Modifier .align(Alignment.BottomCenter) .size(40.dp), - containerColor = MaterialTheme.colorScheme.surfaceVariant, - contentColor = MaterialTheme.colorScheme.onSurfaceVariant + containerColor = ElementTheme.colors.surfaceVariant, + contentColor = ElementTheme.colors.onSurfaceVariant ) { Icon(Icons.Default.ArrowDownward, "") } @@ -345,7 +345,7 @@ internal fun TimelineLoadingMoreIndicator() { ) { CircularProgressIndicator( strokeWidth = 2.dp, - color = MaterialTheme.colorScheme.primary + color = ElementTheme.colors.primary ) } } 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 b8922be78c..ddf2a66105 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,14 +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.LocalExtendedColors +import io.element.android.libraries.designsystem.theme.components.ElementSurface +import io.element.android.libraries.designsystem.theme.ElementTheme private val BUBBLE_RADIUS = 16.dp @@ -82,16 +82,16 @@ fun MessageEventBubble( } val backgroundBubbleColor = if (isHighlighted) { - LocalExtendedColors.current.messageHighlightedBackground + ElementTheme.colors.messageHighlightedBackground } else { if (isMine) { - LocalExtendedColors.current.messageFromMeBackground + ElementTheme.colors.messageFromMeBackground } else { - LocalExtendedColors.current.messageFromOtherBackground + ElementTheme.colors.messageFromOtherBackground } } val bubbleShape = bubbleShape() - Surface( + ElementSurface( modifier = modifier .widthIn(min = 80.dp) .offsetForItem() 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 e6548a71e8..cce7e21896 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 @@ -22,7 +22,6 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable @@ -34,6 +33,7 @@ import androidx.compose.ui.platform.LocalContext import coil.compose.AsyncImage import coil.request.ImageRequest import io.element.android.features.messages.timeline.model.content.TimelineItemImageContent +import io.element.android.libraries.designsystem.theme.ElementTheme @Composable fun TimelineItemImageView( @@ -60,7 +60,7 @@ fun TimelineItemImageView( AsyncImage( model = model, contentDescription = null, - placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant), + placeholder = ColorPainter(ElementTheme.colors.surfaceVariant), contentScale = ContentScale.Crop, onSuccess = { isLoading.value = false }, ) 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..40499798ee 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 @@ -21,7 +21,6 @@ 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.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -30,6 +29,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import io.element.android.libraries.designsystem.theme.ElementTheme @Composable fun TimelineItemInformativeView( @@ -44,14 +44,14 @@ fun TimelineItemInformativeView( ) { Icon( imageVector = icon, - tint = MaterialTheme.colorScheme.secondary, + tint = ElementTheme.colors.secondary, contentDescription = iconDescription, modifier = Modifier.size(16.dp) ) Spacer(modifier = Modifier.width(4.dp)) Text( fontStyle = FontStyle.Italic, - color = MaterialTheme.colorScheme.secondary, + color = ElementTheme.colors.secondary, fontSize = 14.sp, text = text ) 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..4a8ac852b0 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 @@ -23,8 +23,6 @@ import androidx.compose.foundation.layout.padding 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 @@ -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.ElementSurface +import io.element.android.libraries.designsystem.theme.ElementTheme @Composable fun TimelineItemReactionsView( @@ -54,10 +54,10 @@ fun TimelineItemReactionsView( @Composable fun MessagesReactionButton(reaction: AggregatedReaction, modifier: Modifier = Modifier) { - Surface( + ElementSurface( modifier = modifier, - color = MaterialTheme.colorScheme.surfaceVariant, - border = BorderStroke(2.dp, MaterialTheme.colorScheme.background), + color = ElementTheme.colors.surfaceVariant, + border = BorderStroke(2.dp, ElementTheme.colors.background), shape = RoundedCornerShape(corner = CornerSize(12.dp)), ) { Row( @@ -66,7 +66,7 @@ fun MessagesReactionButton(reaction: AggregatedReaction, modifier: Modifier = Mo ) { Text(text = reaction.key, fontSize = 12.sp) Spacer(modifier = Modifier.width(4.dp)) - Text(text = reaction.count, color = MaterialTheme.colorScheme.secondary, fontSize = 12.sp) + Text(text = reaction.count, color = ElementTheme.colors.secondary, fontSize = 12.sp) } } } 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..092f503b67 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 @@ -25,10 +25,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.InlineTextContent 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 @@ -49,6 +46,9 @@ 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.ElementSurface +import io.element.android.libraries.designsystem.theme.ElementColors +import io.element.android.libraries.designsystem.theme.ElementTheme import io.element.android.libraries.matrix.permalink.PermalinkData import io.element.android.libraries.matrix.permalink.PermalinkParser import kotlinx.collections.immutable.persistentMapOf @@ -214,7 +214,7 @@ private fun HtmlInline( ) { Box(modifier) { val styledText = buildAnnotatedString { - appendInlineElement(element, MaterialTheme.colorScheme) + appendInlineElement(element, ElementTheme.colors) } HtmlText( text = styledText, @@ -232,7 +232,7 @@ private fun HtmlPreformatted( ) { val isCode = pre.firstElementChild()?.normalName() == "code" val backgroundColor = - if (isCode) MaterialTheme.colorScheme.codeBackground() else Color.Unspecified + if (isCode) ElementTheme.colors.codeBackground() else Color.Unspecified Box( modifier .background(color = backgroundColor) @@ -255,7 +255,7 @@ private fun HtmlParagraph( ) { Box(modifier) { val styledText = buildAnnotatedString { - appendInlineChildrenElements(paragraph.childNodes(), MaterialTheme.colorScheme) + appendInlineChildrenElements(paragraph.childNodes(), ElementTheme.colors) } HtmlText( text = styledText, onClick = onTextClicked, @@ -272,7 +272,7 @@ private fun HtmlBlockquote( onTextClicked: () -> Unit = {}, onTextLongClicked: () -> Unit = {}, ) { - val color = MaterialTheme.colorScheme.onBackground + val color = ElementTheme.colors.onBackground Box( modifier = modifier .drawBehind { @@ -287,7 +287,7 @@ private fun HtmlBlockquote( ) { val text = buildAnnotatedString { withStyle(style = SpanStyle(fontStyle = FontStyle.Italic)) { - appendInlineChildrenElements(blockquote.childNodes(), MaterialTheme.colorScheme) + appendInlineChildrenElements(blockquote.childNodes(), ElementTheme.colors) } } HtmlText( @@ -306,19 +306,19 @@ private fun HtmlHeading( onTextLongClicked: () -> Unit = {}, ) { val style = when (heading.normalName()) { - "h1" -> MaterialTheme.typography.headlineLarge.copy(fontSize = 30.sp) - "h2" -> MaterialTheme.typography.headlineLarge.copy(fontSize = 26.sp) - "h3" -> MaterialTheme.typography.headlineMedium.copy(fontSize = 22.sp) - "h4" -> MaterialTheme.typography.headlineMedium.copy(fontSize = 18.sp) - "h5" -> MaterialTheme.typography.headlineSmall.copy(fontSize = 14.sp) - "h6" -> MaterialTheme.typography.headlineSmall.copy(fontSize = 12.sp) + "h1" -> ElementTheme.typography.headlineLarge.copy(fontSize = 30.sp) + "h2" -> ElementTheme.typography.headlineLarge.copy(fontSize = 26.sp) + "h3" -> ElementTheme.typography.headlineMedium.copy(fontSize = 22.sp) + "h4" -> ElementTheme.typography.headlineMedium.copy(fontSize = 18.sp) + "h5" -> ElementTheme.typography.headlineSmall.copy(fontSize = 14.sp) + "h6" -> ElementTheme.typography.headlineSmall.copy(fontSize = 12.sp) else -> { return } } Box(modifier) { val text = buildAnnotatedString { - appendInlineChildrenElements(heading.childNodes(), MaterialTheme.colorScheme) + appendInlineChildrenElements(heading.childNodes(), ElementTheme.colors) } HtmlText( text = text, @@ -340,11 +340,11 @@ private fun HtmlMxReply( ) { val blockquote = mxReply.childNodes().firstOrNull() ?: return val shape = RoundedCornerShape(12.dp) - Surface( + ElementSurface( modifier = modifier .padding(bottom = 4.dp) .offset(x = -(8.dp)), - color = MaterialTheme.colorScheme.background, + color = ElementTheme.colors.background, shape = shape, ) { val text = buildAnnotatedString { @@ -354,7 +354,7 @@ private fun HtmlMxReply( withStyle( style = SpanStyle( fontSize = 12.sp, - color = MaterialTheme.colorScheme.secondary + color = ElementTheme.colors.secondary ) ) { append(blockquoteNode.text()) @@ -462,13 +462,13 @@ private fun HtmlListItems( } } -private fun ColorScheme.codeBackground(): Color { +private fun ElementColors.codeBackground(): Color { return background.copy(alpha = 0.3f) } private fun AnnotatedString.Builder.appendInlineChildrenElements( childNodes: List, - colors: ColorScheme + colors: ElementColors ) { for (node in childNodes) { when (node) { @@ -482,7 +482,7 @@ private fun AnnotatedString.Builder.appendInlineChildrenElements( } } -private fun AnnotatedString.Builder.appendInlineElement(element: Element, colors: ColorScheme) { +private fun AnnotatedString.Builder.appendInlineElement(element: Element, colors: ElementColors) { when (element.normalName()) { "br" -> { append('\n') 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..684d7a02ce 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 @@ -27,8 +27,6 @@ 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 @@ -49,7 +47,9 @@ 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.ElementTheme +import io.element.android.libraries.designsystem.theme.components.ElementButton +import io.element.android.libraries.designsystem.theme.components.ElementSurface import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import kotlinx.coroutines.delay @@ -67,9 +67,9 @@ fun OnBoardingScreen( val carrouselState = remember { SplashCarouselStateFactory().create() } val nbOfPages = carrouselState.items.size var key by remember { mutableStateOf(false) } - Surface( + ElementSurface( modifier = modifier, - color = MaterialTheme.colorScheme.background, + color = ElementTheme.colors.background, ) { Box( modifier = Modifier @@ -109,20 +109,7 @@ fun OnBoardingScreen( .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), + ElementButton( onClick = { onSignIn() }, @@ -131,7 +118,9 @@ fun OnBoardingScreen( .align(CenterHorizontally) .testTag(TestTags.onBoardingSignIn) .padding(top = 16.dp) - ) + ) { + Text(text = stringResource(id = StringR.string.login_splash_submit)) + } } } } 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..3fd7810b73 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,12 +26,9 @@ 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 @@ -54,6 +51,8 @@ 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.theme.components.ElementButton +import io.element.android.libraries.designsystem.theme.components.ElementSurface import io.element.android.libraries.designsystem.utils.LogCompositions import io.element.android.libraries.ui.strings.R as StringR @@ -73,9 +72,8 @@ fun BugReportView( } return } - Surface( + ElementSurface( modifier = modifier, - color = MaterialTheme.colorScheme.background, ) { Box( modifier = Modifier @@ -185,7 +183,7 @@ fun BugReportView( } } // Submit - Button( + ElementButton( onClick = { eventSink(BugReportEvents.SendBugReport) }, enabled = state.submitEnabled, modifier = Modifier 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..8aff881f24 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 @@ -24,7 +24,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 +42,7 @@ 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.theme.components.ElementScaffold import io.element.android.libraries.designsystem.utils.LogCompositions import io.element.android.libraries.matrix.core.RoomId import io.element.android.libraries.matrix.core.UserId @@ -117,7 +117,7 @@ fun RoomListView( } } - Scaffold( + ElementScaffold( modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { RoomListTopBar( @@ -125,11 +125,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) 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..a2549a8f9f 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 @@ -29,7 +29,6 @@ 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 @@ -54,6 +53,9 @@ 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.ElementTheme +import io.element.android.libraries.designsystem.theme.components.ElementMediumAppBar +import io.element.android.libraries.designsystem.theme.components.ElementTopAppBar 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 +66,8 @@ fun RoomListTopBar( filter: String, onFilterChanged: (String) -> Unit, onOpenSettings: () -> Unit, - scrollBehavior: TopAppBarScrollBehavior + scrollBehavior: TopAppBarScrollBehavior, + modifier: Modifier = Modifier, ) { LogCompositions( tag = "RoomListScreen", @@ -87,6 +90,7 @@ fun RoomListTopBar( onFilterChanged = onFilterChanged, onCloseClicked = ::closeFilter, scrollBehavior = scrollBehavior, + modifier = modifier, ) } else { DefaultRoomListTopBar( @@ -96,6 +100,7 @@ fun RoomListTopBar( searchWidgetStateIsOpened = true }, scrollBehavior = scrollBehavior, + modifier = modifier, ) } } @@ -110,7 +115,7 @@ fun SearchRoomListTopBar( ) { var filterState by textFieldState(stateValue = text) val focusRequester = remember { FocusRequester() } - TopAppBar( + ElementTopAppBar( modifier = modifier .nestedScroll(scrollBehavior.nestedScrollConnection), title = { @@ -129,7 +134,7 @@ fun SearchRoomListTopBar( placeholder = { Text( text = "Search", - color = MaterialTheme.colorScheme.onBackground.copy(alpha = ContentAlpha.medium) + color = ElementTheme.colors.onBackground.copy(alpha = ContentAlpha.medium) ) }, singleLine = true, @@ -143,15 +148,15 @@ fun SearchRoomListTopBar( Icon( imageVector = Icons.Default.Close, contentDescription = "clear", - tint = MaterialTheme.colorScheme.onBackground + tint = ElementTheme.colors.onBackground ) } } }, colors = TextFieldDefaults.textFieldColors( - textColor = MaterialTheme.colorScheme.onBackground, + textColor = ElementTheme.colors.onBackground, containerColor = Color.Transparent, - cursorColor = MaterialTheme.colorScheme.onBackground.copy(alpha = ContentAlpha.medium), + cursorColor = ElementTheme.colors.onBackground.copy(alpha = ContentAlpha.medium), focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, disabledIndicatorColor = Color.Transparent @@ -167,7 +172,7 @@ fun SearchRoomListTopBar( Icon( imageVector = Icons.Default.ArrowBack, contentDescription = "close", - tint = MaterialTheme.colorScheme.onBackground + tint = ElementTheme.colors.onBackground ) } }, @@ -182,10 +187,11 @@ private fun DefaultRoomListTopBar( matrixUser: MatrixUser?, onOpenSettings: () -> Unit, onSearchClicked: () -> Unit, - scrollBehavior: TopAppBarScrollBehavior + scrollBehavior: TopAppBarScrollBehavior, + modifier: Modifier = Modifier, ) { - MediumTopAppBar( - modifier = Modifier + ElementMediumAppBar( + 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..05494f95c3 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 @@ -31,7 +31,6 @@ import androidx.compose.foundation.layout.padding 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 @@ -54,6 +53,7 @@ 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 private val minHeight = 72.dp @@ -117,7 +117,7 @@ internal fun DefaultRoomSummaryRow( Text( modifier = Modifier.placeholder(room.isPlaceholder, shape = TextPlaceholderShape), text = room.lastMessage?.toString().orEmpty(), - color = MaterialTheme.colorScheme.secondary, + color = ElementTheme.colors.secondary, fontSize = 14.sp, maxLines = 1, overflow = TextOverflow.Ellipsis @@ -132,11 +132,11 @@ internal fun DefaultRoomSummaryRow( modifier = Modifier.placeholder(room.isPlaceholder, shape = TextPlaceholderShape), fontSize = 12.sp, text = room.timestamp ?: "", - color = MaterialTheme.colorScheme.secondary, + color = ElementTheme.colors.secondary, ) Spacer(Modifier.size(4.dp)) val unreadIndicatorColor = - if (room.hasUnread) MaterialTheme.colorScheme.primary else Color.Transparent + if (room.hasUnread) ElementTheme.colors.primary else Color.Transparent Box( modifier = Modifier .size(12.dp) 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..def4c49e89 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 @@ -16,15 +16,15 @@ package io.element.android.libraries.designsystem -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color +import io.element.android.libraries.designsystem.theme.ElementTheme @Composable fun Boolean.toEnabledColor(): Color { return if (this) { - MaterialTheme.colorScheme.primary + ElementTheme.colors.primary } else { - MaterialTheme.colorScheme.secondary + ElementTheme.colors.secondary } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/ExtendedColors.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/ExtendedColors.kt deleted file mode 100644 index 9cdda9cb73..0000000000 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/ExtendedColors.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 - -import androidx.compose.runtime.Immutable -import androidx.compose.ui.graphics.Color - -@Immutable -data class ExtendedColors( - val messageFromMeBackground: Color, - val messageFromOtherBackground: Color, - val messageHighlightedBackground: Color, -) - -internal val extendedColorsLight = ExtendedColors( - messageFromMeBackground = SystemGrey5Light, - messageFromOtherBackground = SystemGrey6Light, - messageHighlightedBackground = Azure, -) - -internal val extendedColorsDark = ExtendedColors( - messageFromMeBackground = SystemGrey5Dark, - messageFromOtherBackground = SystemGrey6Dark, - messageHighlightedBackground = Azure, -) 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.kt deleted file mode 100644 index 09bb595599..0000000000 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/Theme.kt +++ /dev/null @@ -1,120 +0,0 @@ -/* - * 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 - -import android.os.Build -import androidx.compose.foundation.isSystemInDarkTheme -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.SideEffect -import androidx.compose.runtime.compositionLocalOf -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, -) - -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") } - -val LocalExtendedColors = staticCompositionLocalOf { - ExtendedColors( - messageFromMeBackground = Color.Unspecified, - messageFromOtherBackground = Color.Unspecified, - messageHighlightedBackground = Color.Unspecified, - ) -} - -@Composable -fun ElementXTheme( - darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ - dynamicColor: Boolean = false, - content: @Composable () -> Unit -) { - 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 - } - val systemUiController = rememberSystemUiController() - val useDarkIcons = !darkTheme - - SideEffect { - systemUiController.setStatusBarColor( - color = colorScheme.background - ) - systemUiController.setSystemBarsColor( - color = Color.Transparent, - darkIcons = useDarkIcons - ) - } - - val extendedColors = if (darkTheme) extendedColorsDark else extendedColorsLight - - CompositionLocalProvider( - LocalIsDarkTheme provides darkTheme, - LocalExtendedColors provides extendedColors, - ) { - MaterialTheme( - colorScheme = colorScheme, - typography = Typography, - content = content - ) - } -} 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/Type.kt index 6e97bfa4fb..50d5b7d1fd 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/Type.kt @@ -23,48 +23,8 @@ 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/LabelledCheckbox.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledCheckbox.kt index f1ad5738ac..90646930ec 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 @@ -24,6 +24,7 @@ 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.theme.components.ElementCheckbox @Composable fun LabelledCheckbox( @@ -37,7 +38,7 @@ fun LabelledCheckbox( modifier = modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { - Checkbox( + ElementCheckbox( checked = checked, onCheckedChange = onCheckedChange, enabled = enabled, 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..5b7229cd87 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 @@ -23,7 +23,6 @@ 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 @@ -32,6 +31,7 @@ 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.theme.ElementTheme @Composable fun ProgressDialog( @@ -48,19 +48,19 @@ fun ProgressDialog( modifier = modifier .fillMaxWidth() .background( - color = MaterialTheme.colorScheme.onBackground, + color = ElementTheme.colors.onBackground, shape = RoundedCornerShape(8.dp) ) ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { CircularProgressIndicator( modifier = Modifier.padding(16.dp), - color = MaterialTheme.colorScheme.background + color = ElementTheme.colors.background ) if (!text.isNullOrBlank()) { Text( text = text, - color = MaterialTheme.colorScheme.background, + color = ElementTheme.colors.background, modifier = Modifier.padding(16.dp) ) } 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..4d36007f1f 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,18 @@ 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.AlertDialogDefaults import androidx.compose.material3.Text 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.theme.components.ElementButton +import io.element.android.libraries.designsystem.theme.ElementTheme import io.element.android.libraries.ui.strings.R as StringR @Composable @@ -43,6 +48,12 @@ fun ConfirmationDialog( onCancelClicked: () -> Unit = {}, onThirdButtonClicked: () -> Unit = {}, onDismiss: () -> Unit = {}, + shape: Shape = AlertDialogDefaults.shape, + containerColor: Color = ElementTheme.colors.surfaceVariant, + iconContentColor: Color = ElementTheme.colors.onSurfaceVariant, + titleContentColor: Color = ElementTheme.colors.onSurfaceVariant, + textContentColor: Color = ElementTheme.colors.onSurfaceVariant, + tonalElevation: Dp = AlertDialogDefaults.TonalElevation, ) { AlertDialog( modifier = modifier, @@ -59,7 +70,7 @@ fun ConfirmationDialog( horizontalArrangement = Arrangement.Center ) { Column { - Button( + ElementButton( modifier = Modifier.fillMaxWidth(), onClick = { onCancelClicked() @@ -67,7 +78,7 @@ fun ConfirmationDialog( Text(cancelText) } if (thirdButtonText != null) { - Button( + ElementButton( modifier = Modifier.fillMaxWidth(), onClick = { onThirdButtonClicked() @@ -83,7 +94,7 @@ fun ConfirmationDialog( modifier = Modifier.padding(all = 8.dp), horizontalArrangement = Arrangement.Center ) { - Button( + ElementButton( modifier = Modifier.fillMaxWidth(), onClick = { onSubmitClicked() @@ -93,6 +104,12 @@ fun ConfirmationDialog( } } }, + shape = shape, + containerColor = containerColor, + iconContentColor = iconContentColor, + titleContentColor = titleContentColor, + textContentColor = textContentColor, + tonalElevation = tonalElevation, ) } 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..c1ca38b11c 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,18 @@ 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.AlertDialogDefaults import androidx.compose.material3.Text 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.theme.components.ElementButton +import io.element.android.libraries.designsystem.theme.ElementTheme import io.element.android.libraries.ui.strings.R as StringR @Composable @@ -37,6 +42,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 = ElementTheme.colors.surfaceVariant, + iconContentColor: Color = ElementTheme.colors.onSurfaceVariant, + titleContentColor: Color = ElementTheme.colors.onSurfaceVariant, + textContentColor: Color = ElementTheme.colors.onSurfaceVariant, + tonalElevation: Dp = AlertDialogDefaults.TonalElevation, ) { AlertDialog( modifier = modifier, @@ -52,7 +63,7 @@ fun ErrorDialog( modifier = Modifier.padding(all = 8.dp), horizontalArrangement = Arrangement.Center ) { - Button( + ElementButton( modifier = Modifier.fillMaxWidth(), onClick = { onDismiss() @@ -62,6 +73,12 @@ fun ErrorDialog( } } }, + shape = shape, + containerColor = containerColor, + iconContentColor = iconContentColor, + titleContentColor = titleContentColor, + textContentColor = textContentColor, + tonalElevation = tonalElevation, ) } 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..65560e57f7 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 @@ -21,12 +21,12 @@ 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.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.theme.ElementTheme @Composable fun PreferenceCategory( @@ -40,12 +40,12 @@ fun PreferenceCategory( ) { Divider( modifier = Modifier.padding(horizontal = 16.dp), - color = MaterialTheme.colorScheme.secondary, + color = ElementTheme.colors.secondary, thickness = 1.dp ) Text( modifier = Modifier.padding(top = 8.dp, start = 56.dp), - style = MaterialTheme.typography.titleSmall, + style = ElementTheme.typography.titleSmall, text = title ) content() 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..948daace12 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 @@ -42,6 +42,8 @@ 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.theme.components.ElementScaffold +import io.element.android.libraries.designsystem.theme.components.ElementTopAppBar @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -51,7 +53,7 @@ fun PreferenceView( onBackPressed: () -> Unit = {}, content: @Composable ColumnScope.() -> Unit, ) { - Scaffold( + ElementScaffold( modifier = modifier .fillMaxSize() .systemBarsPadding() @@ -85,7 +87,7 @@ fun PreferenceTopAppBar( modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, ) { - TopAppBar( + ElementTopAppBar( modifier = modifier, navigationIcon = { IconButton(onClick = onBackPressed) { 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..244f257944 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 @@ -23,7 +23,6 @@ import androidx.compose.foundation.layout.Row 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 @@ -32,6 +31,8 @@ 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.theme.ElementTheme +import io.element.android.libraries.designsystem.theme.components.ElementSlider import io.element.android.libraries.designsystem.toEnabledColor @Composable @@ -61,19 +62,19 @@ fun PreferenceSlide( ) { Text( modifier = Modifier.fillMaxWidth(), - style = MaterialTheme.typography.bodyLarge, + style = ElementTheme.typography.bodyLarge, color = enabled.toEnabledColor(), text = title ) summary?.let { Text( modifier = Modifier.fillMaxWidth(), - style = MaterialTheme.typography.bodyMedium, + style = ElementTheme.typography.bodyMedium, color = enabled.toEnabledColor(), text = summary ) } - Slider( + ElementSlider( value = value, steps = steps, onValueChange = onValueChange, 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..47e32f409d 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 @@ -25,7 +25,6 @@ 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 @@ -33,6 +32,8 @@ 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.theme.ElementTheme +import io.element.android.libraries.designsystem.theme.components.ElementCheckbox import io.element.android.libraries.designsystem.toEnabledColor @Composable @@ -61,11 +62,11 @@ fun PreferenceSwitch( ) Text( modifier = Modifier.weight(1f), - style = MaterialTheme.typography.bodyLarge, + style = ElementTheme.typography.bodyLarge, color = enabled.toEnabledColor(), text = title ) - Checkbox( + ElementCheckbox( modifier = Modifier.padding(end = preferencePaddingEnd), checked = isChecked, enabled = enabled, 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..6f4c1598b7 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 @@ -24,7 +24,6 @@ import androidx.compose.foundation.layout.fillMaxWidth 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 @@ -32,6 +31,7 @@ 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.theme.ElementTheme @Composable fun PreferenceText( @@ -56,7 +56,7 @@ fun PreferenceText( modifier = Modifier .weight(1f) .padding(end = preferencePaddingEnd), - style = MaterialTheme.typography.bodyLarge, + style = ElementTheme.typography.bodyLarge, text = title ) } 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..19c7c2f49a --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorsDark.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. + */ + +package io.element.android.libraries.designsystem.theme + +import androidx.compose.ui.graphics.Color +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 + +fun elementColorsDark() = ElementColors( + primary = Color.White, + onPrimary = Color.Black, + secondary = DarkGrey, + text = Color.White, + background = Color.Black, + onBackground = Color.White, + surfaceVariant = SystemGrey5Dark, + onSurfaceVariant = Color.White, + messageFromMeBackground = SystemGrey5Dark, + messageFromOtherBackground = SystemGrey6Dark, + messageHighlightedBackground = Azure, + success = Color.Green, + error = Color.Red, + isLight = false, +) + +/* +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, +) + */ 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..6d7c7f62ea --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorsLight.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 + +import androidx.compose.ui.graphics.Color +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 + +fun elementColorsLight() = ElementColors( + primary = Color.Black, + onPrimary = Color.White, + secondary = LightGrey, + text = Color.Black, + background = Color.White, + onBackground = Color.Black, + surfaceVariant = SystemGrey5Light, + onSurfaceVariant = Color.Black, + messageFromMeBackground = SystemGrey5Light, + messageFromOtherBackground = SystemGrey6Light, + messageHighlightedBackground = Azure, + success = Color.Green, + error = Color.Red, + isLight = true, +) + +/* +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, + + */ diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ContentColor.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ContentColor.kt new file mode 100644 index 0000000000..56df033d27 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ContentColor.kt @@ -0,0 +1,42 @@ +/* + * 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.components.color + +import androidx.compose.material3.LocalContentColor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.takeOrElse +import io.element.android.libraries.designsystem.theme.ElementColors +import io.element.android.libraries.designsystem.theme.ElementTheme + +@Composable +@ReadOnlyComposable +fun elementContentColorFor(backgroundColor: Color): Color { + return ElementTheme.colors.elementContentColorFor(backgroundColor).takeOrElse { + LocalContentColor.current + } +} + +fun ElementColors.elementContentColorFor(backgroundColor: Color): Color { + return when (backgroundColor) { + primary -> onPrimary + surfaceVariant -> onSurfaceVariant + background -> onBackground + else -> Color.Unspecified + } +} 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..38da8e7816 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementColors.kt @@ -0,0 +1,118 @@ +/* + * 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( + primary: Color, + onPrimary: Color, + secondary: Color, + text: Color, + background: Color, + onBackground: Color, + surfaceVariant: Color, + onSurfaceVariant: Color, + messageFromMeBackground: Color, + messageFromOtherBackground: Color, + messageHighlightedBackground: Color, + success: Color, + error: Color, + isLight: Boolean, +) { + var primary by mutableStateOf(primary) + private set + var onPrimary by mutableStateOf(onPrimary) + private set + var secondary by mutableStateOf(secondary) + private set + var text by mutableStateOf(text) + private set + var success by mutableStateOf(success) + private set + var error by mutableStateOf(error) + private set + var background by mutableStateOf(background) + private set + var onBackground by mutableStateOf(onBackground) + private set + var surfaceVariant by mutableStateOf(surfaceVariant) + private set + var onSurfaceVariant by mutableStateOf(onSurfaceVariant) + private set + 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( + primary: Color = this.primary, + onPrimary: Color = this.onPrimary, + secondary: Color = this.secondary, + text: Color = this.text, + background: Color = this.background, + onBackground: Color = this.onBackground, + surfaceVariant: Color = this.surfaceVariant, + onSurfaceVariant: Color = this.onSurfaceVariant, + messageFromMeBackground: Color = this.messageFromMeBackground, + messageFromOtherBackground: Color = this.messageFromOtherBackground, + messageHighlightedBackground: Color = this.messageHighlightedBackground, + success: Color = this.success, + error: Color = this.error, + isLight: Boolean = this.isLight, + ) = ElementColors( + primary = primary, + onPrimary = onPrimary, + secondary = secondary, + text = text, + background = background, + onBackground = onBackground, + surfaceVariant = surfaceVariant, + onSurfaceVariant = onSurfaceVariant, + messageFromMeBackground = messageFromMeBackground, + messageFromOtherBackground = messageFromOtherBackground, + messageHighlightedBackground = messageHighlightedBackground, + success = success, + error = error, + isLight = isLight, + ) + + fun updateColorsFrom(other: ElementColors) { + primary = other.primary + onPrimary = other.onPrimary + secondary = other.secondary + text = other.text + success = other.success + background = other.background + onBackground = other.onBackground + surfaceVariant = other.surfaceVariant + onSurfaceVariant = other.onSurfaceVariant + messageFromMeBackground = other.messageFromMeBackground + messageFromOtherBackground = other.messageFromOtherBackground + messageHighlightedBackground = other.messageHighlightedBackground + error = other.error + isLight = other.isLight + } +} 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/ElementSpaces.kt similarity index 52% 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/ElementSpaces.kt index 928e83cfa4..e5f2236cf0 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/ElementSpaces.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,14 @@ * limitations under the License. */ -package io.element.android.libraries.designsystem.components +package io.element.android.libraries.designsystem.theme -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp -@Composable -fun VectorButton(text: String, enabled: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { - Button( - onClick = onClick, - enabled = enabled, - modifier = modifier - ) { - Text(text = text) - } -} +data class ElementSpaces( + val small: Dp = 4.dp, + val medium: Dp = 8.dp, + val large: Dp = 16.dp, + val extraLarge: Dp = 40.dp, +) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementTheme.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementTheme.kt new file mode 100644 index 0000000000..5281888b17 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementTheme.kt @@ -0,0 +1,84 @@ +/* + * 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.foundation.isSystemInDarkTheme +import androidx.compose.material.ProvideTextStyle +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color +import com.google.accompanist.systemuicontroller.rememberSystemUiController + +/** + * Inspired from https://medium.com/@lucasyujideveloper/54cbcbde1ace + */ +object ElementTheme { + val colors: ElementColors + @Composable + @ReadOnlyComposable + get() = LocalColors.current + + val typography: ElementTypography + @Composable + @ReadOnlyComposable + get() = LocalTypography.current + + val spaces: ElementSpaces + @Composable + @ReadOnlyComposable + get() = LocalSpaces.current +} + +/* Global variables (application level) */ +val LocalSpaces = staticCompositionLocalOf { ElementSpaces() } +val LocalColors = staticCompositionLocalOf { elementColorsLight() } +val LocalTypography = staticCompositionLocalOf { ElementTypography() } + +@Composable +fun ElementTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + lightColors: ElementColors = elementColorsLight(), + darkColors: ElementColors = elementColorsDark(), + typography: ElementTypography = ElementTheme.typography, + spaces: ElementSpaces = ElementTheme.spaces, + content: @Composable () -> Unit, +) { + val systemUiController = rememberSystemUiController() + val useDarkIcons = !darkTheme + val currentColor = remember { if (darkTheme) darkColors else lightColors } + SideEffect { + systemUiController.setStatusBarColor( + color = currentColor.background + ) + systemUiController.setSystemBarsColor( + color = Color.Transparent, + darkIcons = useDarkIcons + ) + } + val rememberedColors = remember { currentColor.copy() }.apply { updateColorsFrom(currentColor) } + CompositionLocalProvider( + LocalColors provides rememberedColors, + LocalSpaces provides spaces, + LocalTypography provides typography, + ) { + ProvideTextStyle(typography.body1, 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..57d0beaa36 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementTypography.kt @@ -0,0 +1,118 @@ +/* + * 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 + +@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 +) + +data class ElementTypography( + val h1: TextStyle = h1Default, + val body1: TextStyle = body1Default, + val bodySmall: TextStyle = bodySmallDefault, + val bodyMedium: TextStyle = bodyMediumDefault, + val bodyLarge: TextStyle = bodyLargeDefault, + val headlineSmall: TextStyle = headlineSmallDefault, + val headlineMedium: TextStyle = headlineMediumDefault, + val headlineLarge: TextStyle = headlineLargeDefault, + val titleSmall: TextStyle = titleSmallDefault, + val titleMedium: TextStyle = titleMediumDefault, +) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementButton.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementButton.kt new file mode 100644 index 0000000000..d9a536b4db --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementButton.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.theme.components + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material.ContentAlpha +import androidx.compose.material.ProvideTextStyle +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.compositeOver +import io.element.android.libraries.designsystem.components.color.elementContentColorFor +import io.element.android.libraries.designsystem.theme.ElementTheme + +@Composable +fun ElementButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + containerColor: Color = ElementTheme.colors.primary, + content: @Composable RowScope.() -> Unit +) { + Button( + colors = ButtonDefaults.buttonColors( + containerColor = containerColor, + contentColor = elementContentColorFor(backgroundColor = containerColor), + disabledContainerColor = containerColor + .copy(alpha = 0.12f) + .compositeOver(containerColor), + disabledContentColor = elementContentColorFor(backgroundColor = containerColor) + .copy(alpha = ContentAlpha.disabled) + ), + // TODO shape = ButtonShape, + // TODO elevation = ButtonDefaults.elevation( + // defaultElevation = ElementTheme.elevation.default, + // pressedElevation = ElementTheme.elevation.pressed + // /* disabledElevation = 0.dp */ + // ), + onClick = onClick, + modifier = modifier, + enabled = enabled, + content = { + ProvideTextStyle( + value = ElementTheme.typography.body1 + ) { + content() + } + } + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementCheckbox.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementCheckbox.kt new file mode 100644 index 0000000000..058313d40d --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementCheckbox.kt @@ -0,0 +1,55 @@ +/* + * 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.Checkbox +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.graphics.Color +import io.element.android.libraries.designsystem.theme.ElementTheme + +@Composable +fun ElementCheckbox( + checked: Boolean, + onCheckedChange: ((Boolean) -> Unit)?, + modifier: Modifier = Modifier, + enabled: Boolean = true, + colors: CheckboxColors = CheckboxDefaults.colors( + checkedColor = ElementTheme.colors.primary, + uncheckedColor = Color.Gray, // TODO ElementTheme.colors. + checkmarkColor = Color.Gray, // TODO ElementTheme.colors. + disabledCheckedColor = Color.Gray // TODO ElementTheme.colors. + .copy(alpha = 0.2F), + disabledUncheckedColor = Color.Gray // TODO ElementTheme.colors. + .copy(alpha = 0.2F), + disabledIndeterminateColor = Color.Gray // TODO ElementTheme.colors. + ), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } +) { + Checkbox( + checked = checked, + onCheckedChange = onCheckedChange, + modifier = modifier, + enabled = enabled, + colors = colors, + interactionSource = interactionSource, + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementMediumAppBar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementMediumAppBar.kt new file mode 100644 index 0000000000..0d3a0a134d --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementMediumAppBar.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.RowScope +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MediumTopAppBar +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 +import io.element.android.libraries.designsystem.theme.ElementTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ElementMediumAppBar( + title: @Composable () -> Unit, + modifier: Modifier = Modifier, + navigationIcon: @Composable () -> Unit = {}, + actions: @Composable RowScope.() -> Unit = {}, + windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, + colors: TopAppBarColors = TopAppBarDefaults.mediumTopAppBarColors( + containerColor = ElementTheme.colors.background, + scrolledContainerColor = ElementTheme.colors.background, + navigationIconContentColor = ElementTheme.colors.onBackground, + titleContentColor = ElementTheme.colors.onBackground, + actionIconContentColor = ElementTheme.colors.onBackground, + ), + scrollBehavior: TopAppBarScrollBehavior? = null +) { + 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/ElementScaffold.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementScaffold.kt new file mode 100644 index 0000000000..a244bb3fa7 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementScaffold.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.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FabPosition +import androidx.compose.material3.Scaffold +import androidx.compose.material3.ScaffoldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import io.element.android.libraries.designsystem.components.color.elementContentColorFor +import io.element.android.libraries.designsystem.theme.ElementTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ElementScaffold( + modifier: Modifier = Modifier, + topBar: @Composable () -> Unit = {}, + bottomBar: @Composable () -> Unit = {}, + snackbarHost: @Composable () -> Unit = {}, + floatingActionButton: @Composable () -> Unit = {}, + floatingActionButtonPosition: FabPosition = FabPosition.End, + containerColor: Color = ElementTheme.colors.background, + contentColor: Color = elementContentColorFor(containerColor), + contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets, + content: @Composable (PaddingValues) -> Unit +) { + 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/ElementSlider.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementSlider.kt new file mode 100644 index 0000000000..318aeb2b41 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementSlider.kt @@ -0,0 +1,63 @@ +/* + * 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.Slider +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 io.element.android.libraries.designsystem.theme.ElementTheme + +@Composable +fun ElementSlider( + 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( + thumbColor = ElementTheme.colors.primary, + activeTrackColor = ElementTheme.colors.primary, + activeTickColor = ElementTheme.colors.primary, + inactiveTrackColor = ElementTheme.colors.primary, + inactiveTickColor = ElementTheme.colors.primary, + disabledThumbColor = ElementTheme.colors.primary, + disabledActiveTrackColor = ElementTheme.colors.primary, + disabledActiveTickColor = ElementTheme.colors.primary, + disabledInactiveTrackColor = ElementTheme.colors.primary, + disabledInactiveTickColor = ElementTheme.colors.primary, + ), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } +) { + Slider( + value = value, + onValueChange = onValueChange, + modifier = modifier, + enabled = enabled, + valueRange = valueRange, + steps = steps, + onValueChangeFinished = onValueChangeFinished, + colors = colors, + interactionSource = interactionSource, + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementSurface.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementSurface.kt new file mode 100644 index 0000000000..0039eb0ee3 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementSurface.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.foundation.BorderStroke +import androidx.compose.material3.Surface +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 +import io.element.android.libraries.designsystem.components.color.elementContentColorFor +import io.element.android.libraries.designsystem.theme.ElementTheme + +@Composable +fun ElementSurface( + modifier: Modifier = Modifier, + shape: Shape = RectangleShape, + color: Color = ElementTheme.colors.surfaceVariant, + contentColor: Color = elementContentColorFor(color), + tonalElevation: Dp = 0.dp, + shadowElevation: Dp = 0.dp, + border: BorderStroke? = null, + content: @Composable () -> Unit +) { + 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/ElementTopAppBar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementTopAppBar.kt new file mode 100644 index 0000000000..7b7be7dfd1 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementTopAppBar.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.RowScope +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBar +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 +import io.element.android.libraries.designsystem.theme.ElementTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ElementTopAppBar( + title: @Composable () -> Unit, + modifier: Modifier = Modifier, + navigationIcon: @Composable () -> Unit = {}, + actions: @Composable RowScope.() -> Unit = {}, + windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, + colors: TopAppBarColors = TopAppBarDefaults.smallTopAppBarColors( + containerColor = ElementTheme.colors.background, + scrolledContainerColor = ElementTheme.colors.background, + navigationIconContentColor = ElementTheme.colors.onBackground, + titleContentColor = ElementTheme.colors.onBackground, + actionIconContentColor = ElementTheme.colors.onBackground, + ), + scrollBehavior: TopAppBarScrollBehavior? = null +) { + TopAppBar( + title = title, + modifier = modifier, + navigationIcon = navigationIcon, + actions = actions, + windowInsets = windowInsets, + colors = colors, + scrollBehavior = scrollBehavior, + ) +} 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..94cc41c0f4 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 @@ -23,7 +23,6 @@ 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.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -36,6 +35,7 @@ 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.theme.ElementTheme 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 @@ -71,7 +71,7 @@ fun MatrixUserHeader( Spacer(modifier = Modifier.height(4.dp)) Text( text = matrixUser.id.value, - color = MaterialTheme.colorScheme.secondary, + color = ElementTheme.colors.secondary, fontSize = 14.sp, maxLines = 1, overflow = TextOverflow.Ellipsis 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..e813fc8b50 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 @@ -23,7 +23,6 @@ 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.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -35,6 +34,7 @@ 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.theme.ElementTheme 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 @@ -74,7 +74,7 @@ fun MatrixUserRow( if (matrixUser.username.isNullOrEmpty().not()) { Text( text = matrixUser.id.value, - color = MaterialTheme.colorScheme.secondary, + color = ElementTheme.colors.secondary, fontSize = 14.sp, maxLines = 1, overflow = TextOverflow.Ellipsis 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..7a89559b42 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 @@ -32,6 +32,7 @@ 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.theme.components.ElementButton @Composable fun ShowkaseButton( @@ -41,7 +42,7 @@ fun ShowkaseButton( var isShowkaseButtonVisible by remember { mutableStateOf(BuildConfig.DEBUG) } if (isShowkaseButtonVisible) { - Button( + ElementButton( modifier = modifier .padding(top = 32.dp), onClick = onClick 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..bcd78869cf 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 @@ -24,7 +24,6 @@ import androidx.activity.OnBackPressedDispatcherOwner import androidx.activity.compose.LocalOnBackPressedDispatcherOwner import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration @@ -36,7 +35,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 @@ -101,8 +100,8 @@ class ScreenshotTest { override fun getOnBackPressedDispatcher() = OnBackPressedDispatcher() } ) { - ElementXTheme(darkTheme = (theme == "dark")) { - Box(modifier = Modifier.background(MaterialTheme.colorScheme.background)) { + ElementTheme(darkTheme = (theme == "dark")) { + Box(modifier = Modifier.background(ElementTheme.colors.background)) { componentTestPreview.Content() } } diff --git a/tests/uitests/src/test/kotlin/io/element/android/tests/uitests/TypographyTestPreview.kt b/tests/uitests/src/test/kotlin/io/element/android/tests/uitests/TypographyTestPreview.kt index 80f27c25a0..6f3b4f4bbf 100644 --- a/tests/uitests/src/test/kotlin/io/element/android/tests/uitests/TypographyTestPreview.kt +++ b/tests/uitests/src/test/kotlin/io/element/android/tests/uitests/TypographyTestPreview.kt @@ -19,11 +19,11 @@ package io.element.android.tests.uitests import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.BasicText -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.airbnb.android.showkase.models.ShowkaseBrowserTypography import com.airbnb.android.showkase.ui.padding4x +import io.element.android.libraries.designsystem.theme.ElementTheme import java.util.Locale class TypographyTestPreview( @@ -39,7 +39,7 @@ class TypographyTestPreview( .fillMaxWidth() .padding(padding4x), style = showkaseBrowserTypography.textStyle.copy( - color = MaterialTheme.colorScheme.onBackground + color = ElementTheme.colors.onBackground ) ) } From c54471b5e0e4a1f9727cb5da4c6d5f7228075eb8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 26 Jan 2023 16:32:10 +0100 Subject: [PATCH 14/96] Add some aliases and fixes placeholder color. --- .../roomlist/components/RoomSummaryRow.kt | 31 ++++++++++++++----- .../designsystem/theme/ColorAliases.kt | 29 +++++++++++++++++ 2 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt 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 05494f95c3..9693d17a26 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 @@ -54,6 +54,11 @@ 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.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 +100,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 +114,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 = ElementTheme.colors.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 = ElementTheme.colors.secondary, + color = ElementTheme.colors.roomListRoomMessage, fontSize = 14.sp, maxLines = 1, overflow = TextOverflow.Ellipsis @@ -132,11 +149,11 @@ internal fun DefaultRoomSummaryRow( modifier = Modifier.placeholder(room.isPlaceholder, shape = TextPlaceholderShape), fontSize = 12.sp, text = room.timestamp ?: "", - color = ElementTheme.colors.secondary, + color = ElementTheme.colors.roomListRoomMessageDate, ) Spacer(Modifier.size(4.dp)) val unreadIndicatorColor = - if (room.hasUnread) ElementTheme.colors.primary else Color.Transparent + if (room.hasUnread) ElementTheme.colors.roomListUnreadIndicator else Color.Transparent Box( modifier = Modifier .size(12.dp) 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..1325211232 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt @@ -0,0 +1,29 @@ +/* + * 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 io.element.android.libraries.designsystem.SystemGrey4Dark +import io.element.android.libraries.designsystem.SystemGrey6Light + +/** + * Room list + */ +val ElementColors.roomListRoomName get() = primary +val ElementColors.roomListRoomMessage get() = secondary +val ElementColors.roomListRoomMessageDate get() = secondary +val ElementColors.roomListUnreadIndicator get() = primary +val ElementColors.roomListPlaceHolder get() = if (isLight) SystemGrey6Light else SystemGrey4Dark From 71fa01f8b678c46a009b3ad219c3c4353c229495 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 26 Jan 2023 16:39:17 +0100 Subject: [PATCH 15/96] Theme: Add ElementModalBottomSheetLayout. --- .../messages/actionlist/ActionListView.kt | 5 +- .../ElementModalBottomSheetLayout.kt | 59 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementModalBottomSheetLayout.kt 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 43414e732c..683dd28dc8 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 @@ -44,6 +44,7 @@ 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.ElementTheme +import io.element.android.libraries.designsystem.theme.components.ElementModalBottomSheetLayout import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch @@ -73,7 +74,7 @@ fun ActionListView( } } - ModalBottomSheetLayout( + ElementModalBottomSheetLayout( modifier = modifier, sheetState = modalBottomSheetState, sheetContent = { @@ -85,7 +86,7 @@ fun ActionListView( .imePadding() ) } - ) {} + ) } @Composable diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementModalBottomSheetLayout.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementModalBottomSheetLayout.kt new file mode 100644 index 0000000000..7fbd2caf49 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementModalBottomSheetLayout.kt @@ -0,0 +1,59 @@ +/* + * 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.ModalBottomSheetLayout +import androidx.compose.material.ModalBottomSheetState +import androidx.compose.material.ModalBottomSheetValue +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 +import io.element.android.libraries.designsystem.components.color.elementContentColorFor +import io.element.android.libraries.designsystem.theme.ElementTheme + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun ElementModalBottomSheetLayout( + sheetContent: @Composable ColumnScope.() -> Unit, + modifier: Modifier = Modifier, + sheetState: ModalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden), + sheetShape: Shape = MaterialTheme.shapes.large, + sheetElevation: Dp = ModalBottomSheetDefaults.Elevation, + sheetBackgroundColor: Color = ElementTheme.colors.surfaceVariant, + sheetContentColor: Color = elementContentColorFor(sheetBackgroundColor), + scrimColor: Color = ElementTheme.colors.onSurfaceVariant.copy(alpha = 0.32f), + content: @Composable () -> Unit = {} +) { + ModalBottomSheetLayout( + sheetContent = sheetContent, + modifier = modifier, + sheetState = sheetState, + sheetShape = sheetShape, + sheetElevation = sheetElevation, + sheetBackgroundColor = sheetBackgroundColor, + sheetContentColor = sheetContentColor, + scrimColor = scrimColor, + content = content, + ) +} From e889856d1c443d667d41610b0f732d1ef987a5cd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 26 Jan 2023 16:50:16 +0100 Subject: [PATCH 16/96] Fix color error. --- .../libraries/designsystem/components/ProgressDialog.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 5b7229cd87..cda8530772 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 @@ -48,19 +48,19 @@ fun ProgressDialog( modifier = modifier .fillMaxWidth() .background( - color = ElementTheme.colors.onBackground, + color = ElementTheme.colors.surfaceVariant, shape = RoundedCornerShape(8.dp) ) ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { CircularProgressIndicator( modifier = Modifier.padding(16.dp), - color = ElementTheme.colors.background + color = ElementTheme.colors.onSurfaceVariant ) if (!text.isNullOrBlank()) { Text( text = text, - color = ElementTheme.colors.background, + color = ElementTheme.colors.onSurfaceVariant, modifier = Modifier.padding(16.dp) ) } From 7d85d8da304db01fabf9682d6943eacae741169f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 26 Jan 2023 17:51:39 +0100 Subject: [PATCH 17/96] Add ElementCircularProgressIndicator and ElementOutlinedTextField. --- .../io/element/android/x/node/RootFlowNode.kt | 3 +- .../login/changeserver/ChangeServerView.kt | 6 +- .../features/login/root/LoginRootScreen.kt | 13 +- .../messages/timeline/TimelineView.kt | 3 +- .../rageshake/bugreport/BugReportView.kt | 10 +- .../designsystem/components/ProgressDialog.kt | 3 +- .../ElementCircularProgressIndicator.kt | 53 ++++++++ .../components/ElementOutlinedTextField.kt | 117 ++++++++++++++++++ 8 files changed, 190 insertions(+), 18 deletions(-) create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementCircularProgressIndicator.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementOutlinedTextField.kt 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..24ae5ea389 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 @@ -36,6 +36,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.ElementCircularProgressIndicator import io.element.android.libraries.di.DaggerComponentOwner import io.element.android.libraries.matrix.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.core.SessionId @@ -182,7 +183,7 @@ class RootFlowNode( private fun splashNode(buildContext: BuildContext) = node(buildContext) { Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) { - CircularProgressIndicator() + ElementCircularProgressIndicator() } } } 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 a8ea2de0fb..e81e6d7d14 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 @@ -56,6 +56,8 @@ import io.element.android.libraries.designsystem.components.VectorIcon import io.element.android.libraries.designsystem.components.form.textFieldState import io.element.android.libraries.designsystem.theme.ElementTheme import io.element.android.libraries.designsystem.theme.components.ElementButton +import io.element.android.libraries.designsystem.theme.components.ElementCircularProgressIndicator +import io.element.android.libraries.designsystem.theme.components.ElementOutlinedTextField import io.element.android.libraries.designsystem.theme.components.ElementSurface import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag @@ -126,7 +128,7 @@ fun ChangeServerView( color = ElementTheme.colors.secondary ) var homeserverFieldState by textFieldState(stateValue = state.homeserver) - OutlinedTextField( + ElementOutlinedTextField( value = homeserverFieldState, modifier = Modifier .fillMaxWidth() @@ -174,7 +176,7 @@ fun ChangeServerView( } } if (state.changeServerAction is Async.Loading) { - CircularProgressIndicator( + ElementCircularProgressIndicator( modifier = Modifier.align(Alignment.Center) ) } 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 1511648c74..5dd53a1254 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 @@ -32,11 +32,9 @@ 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.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -59,13 +57,14 @@ import io.element.android.features.login.error.loginError import io.element.android.libraries.designsystem.components.form.textFieldState import io.element.android.libraries.designsystem.theme.ElementTheme import io.element.android.libraries.designsystem.theme.components.ElementButton +import io.element.android.libraries.designsystem.theme.components.ElementCircularProgressIndicator +import io.element.android.libraries.designsystem.theme.components.ElementOutlinedTextField import io.element.android.libraries.designsystem.theme.components.ElementSurface 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, @@ -112,7 +111,7 @@ fun LoginRootScreen( Box( modifier = Modifier.fillMaxWidth() ) { - OutlinedTextField( + ElementOutlinedTextField( value = state.homeserver, modifier = Modifier.fillMaxWidth(), onValueChange = { /* no op */ }, @@ -135,7 +134,7 @@ fun LoginRootScreen( } ) } - OutlinedTextField( + ElementOutlinedTextField( value = loginFieldState, modifier = Modifier .fillMaxWidth() @@ -158,7 +157,7 @@ fun LoginRootScreen( // Ensure password is hidden when user submits the form passwordVisible = false } - OutlinedTextField( + ElementOutlinedTextField( value = passwordFieldState, modifier = Modifier .fillMaxWidth() @@ -217,7 +216,7 @@ fun LoginRootScreen( } } if (state.loggedInState is LoggedInState.LoggingIn) { - CircularProgressIndicator( + ElementCircularProgressIndicator( modifier = Modifier.align(Alignment.Center) ) } 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 fdcf3037b6..d08642993e 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 @@ -79,6 +79,7 @@ import io.element.android.features.messages.timeline.model.content.TimelineItemU 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.ElementTheme +import io.element.android.libraries.designsystem.theme.components.ElementCircularProgressIndicator import io.element.android.libraries.designsystem.utils.PairCombinedPreviewParameter import io.element.android.libraries.matrix.core.EventId import kotlinx.collections.immutable.ImmutableList @@ -343,7 +344,7 @@ internal fun TimelineLoadingMoreIndicator() { .padding(8.dp), contentAlignment = Alignment.Center, ) { - CircularProgressIndicator( + ElementCircularProgressIndicator( strokeWidth = 2.dp, color = ElementTheme.colors.primary ) 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 3fd7810b73..75b0a5228e 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,9 +26,6 @@ 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.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -52,11 +49,12 @@ 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.theme.components.ElementButton +import io.element.android.libraries.designsystem.theme.components.ElementCircularProgressIndicator +import io.element.android.libraries.designsystem.theme.components.ElementOutlinedTextField import io.element.android.libraries.designsystem.theme.components.ElementSurface 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, @@ -115,7 +113,7 @@ fun BugReportView( Column( // modifier = Modifier.weight(1f), ) { - OutlinedTextField( + ElementOutlinedTextField( value = descriptionFieldState, modifier = Modifier .fillMaxWidth() @@ -195,7 +193,7 @@ fun BugReportView( } when (state.sending) { is Async.Loading -> { - CircularProgressIndicator( + ElementCircularProgressIndicator( progress = state.sendingProgress, modifier = Modifier.align(Alignment.Center) ) 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 cda8530772..ced336c0e0 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 @@ -32,6 +32,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import io.element.android.libraries.designsystem.theme.ElementTheme +import io.element.android.libraries.designsystem.theme.components.ElementCircularProgressIndicator @Composable fun ProgressDialog( @@ -53,7 +54,7 @@ fun ProgressDialog( ) ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { - CircularProgressIndicator( + ElementCircularProgressIndicator( modifier = Modifier.padding(16.dp), color = ElementTheme.colors.onSurfaceVariant ) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementCircularProgressIndicator.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementCircularProgressIndicator.kt new file mode 100644 index 0000000000..a5188bab51 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementCircularProgressIndicator.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. + */ + +package io.element.android.libraries.designsystem.theme.components + +import androidx.compose.material3.CircularProgressIndicator +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 +import io.element.android.libraries.designsystem.theme.ElementTheme + +@Composable +fun ElementCircularProgressIndicator( + progress: Float, + modifier: Modifier = Modifier, + color: Color = ProgressIndicatorDefaults.circularColor, + strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth +) { + CircularProgressIndicator( + modifier = modifier, + progress = progress, + color = color, + strokeWidth = strokeWidth, + ) +} + +@Composable +fun ElementCircularProgressIndicator( + modifier: Modifier = Modifier, + color: Color = ElementTheme.colors.primary, + strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth, +) { + CircularProgressIndicator( + modifier = modifier, + color = color, + strokeWidth = strokeWidth, + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementOutlinedTextField.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementOutlinedTextField.kt new file mode 100644 index 0000000000..04b0eb402f --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementOutlinedTextField.kt @@ -0,0 +1,117 @@ +/* + * 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.foundation.text.selection.TextSelectionColors +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.OutlinedTextField +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.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.VisualTransformation +import io.element.android.libraries.designsystem.ElementGreen +import io.element.android.libraries.designsystem.theme.ElementTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ElementOutlinedTextField( + 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( + textColor = ElementTheme.colors.primary, + disabledTextColor = ElementTheme.colors.primary.copy(alpha = 0.38f), + containerColor = Color.Transparent, + cursorColor = ElementTheme.colors.primary, + errorCursorColor = ElementTheme.colors.error, + selectionColors = TextSelectionColors( + handleColor = ElementGreen, + backgroundColor = ElementGreen.copy(alpha = 0.4f) + ), + focusedBorderColor = ElementTheme.colors.primary, + unfocusedBorderColor = ElementTheme.colors.secondary, + disabledBorderColor = ElementTheme.colors.secondary.copy(alpha = 0.12f), + errorBorderColor = ElementTheme.colors.error, + focusedLeadingIconColor = ElementTheme.colors.primary, + unfocusedLeadingIconColor = ElementTheme.colors.secondary, + disabledLeadingIconColor = ElementTheme.colors.secondary.copy(0.12f), + errorLeadingIconColor = ElementTheme.colors.error, + focusedTrailingIconColor = ElementTheme.colors.primary, + unfocusedTrailingIconColor = ElementTheme.colors.secondary, + disabledTrailingIconColor = ElementTheme.colors.secondary.copy(alpha = 0.12f), + errorTrailingIconColor = ElementTheme.colors.error, + focusedLabelColor = ElementTheme.colors.primary, + unfocusedLabelColor = ElementTheme.colors.secondary, + disabledLabelColor = ElementTheme.colors.secondary.copy(alpha = 0.12f), + errorLabelColor = ElementTheme.colors.error, + placeholderColor = ElementTheme.colors.secondary, + disabledPlaceholderColor = ElementTheme.colors.secondary.copy(alpha = 0.12f), + focusedSupportingTextColor = ElementTheme.colors.primary, + unfocusedSupportingTextColor = ElementTheme.colors.secondary, + disabledSupportingTextColor = ElementTheme.colors.primary.copy(alpha = 0.12f), + errorSupportingTextColor = ElementTheme.colors.error, + ) +) { + 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, + ) +} + From 2b7c965236ab9491706cc25c165b7c60fbc200ce Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 26 Jan 2023 18:01:27 +0100 Subject: [PATCH 18/96] Remove bad usage of ElementSurface --- .../io/element/android/x/MainActivity.kt | 4 +- .../login/changeserver/ChangeServerView.kt | 199 +++++++------- .../features/login/root/LoginRootScreen.kt | 260 +++++++++--------- .../features/onboarding/OnBoardingScreen.kt | 99 ++++--- .../rageshake/bugreport/BugReportView.kt | 229 ++++++++------- 5 files changed, 384 insertions(+), 407 deletions(-) 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 eb249d5b75..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,6 +18,7 @@ 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.ui.Modifier import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen @@ -25,7 +26,6 @@ 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.theme.components.ElementSurface import io.element.android.libraries.designsystem.theme.ElementTheme import io.element.android.libraries.di.DaggerComponentOwner import io.element.android.x.di.AppBindings @@ -41,7 +41,7 @@ class MainActivity : NodeComponentActivity() { WindowCompat.setDecorFitsSystemWindows(window, false) setContent { ElementTheme { - ElementSurface( + Box( modifier = Modifier.fillMaxSize(), ) { NodeHost(integrationPoint = appyxIntegrationPoint) { 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 e81e6d7d14..4ffeb42def 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 @@ -33,9 +33,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.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -58,7 +56,6 @@ import io.element.android.libraries.designsystem.theme.ElementTheme import io.element.android.libraries.designsystem.theme.components.ElementButton import io.element.android.libraries.designsystem.theme.components.ElementCircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.ElementOutlinedTextField -import io.element.android.libraries.designsystem.theme.components.ElementSurface import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag @@ -68,118 +65,114 @@ fun ChangeServerView( modifier: Modifier = Modifier, onChangeServerSuccess: () -> Unit = {}, ) { - ElementSurface( - modifier = modifier, + 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 = ElementTheme.colors.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 = ElementTheme.colors.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 = ElementTheme.colors.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, + ) + 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 = ElementTheme.colors.secondary + ) + var homeserverFieldState by textFieldState(stateValue = state.homeserver) + ElementOutlinedTextField( + 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) - ElementOutlinedTextField( - 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 = ElementTheme.colors.error, - style = ElementTheme.typography.bodySmall, - modifier = Modifier.padding(start = 16.dp) - ) - } - ElementButton( - 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) { - ElementCircularProgressIndicator( - modifier = Modifier.align(Alignment.Center) + color = ElementTheme.colors.error, + style = ElementTheme.typography.bodySmall, + modifier = Modifier.padding(start = 16.dp) ) } + ElementButton( + 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) { + ElementCircularProgressIndicator( + modifier = Modifier.align(Alignment.Center) + ) } } } 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 5dd53a1254..71ca4ebe15 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 @@ -18,6 +18,7 @@ package io.element.android.features.login.root +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -59,7 +60,6 @@ import io.element.android.libraries.designsystem.theme.ElementTheme import io.element.android.libraries.designsystem.theme.components.ElementButton import io.element.android.libraries.designsystem.theme.components.ElementCircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.ElementOutlinedTextField -import io.element.android.libraries.designsystem.theme.components.ElementSurface import io.element.android.libraries.matrix.core.SessionId import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag @@ -73,153 +73,149 @@ fun LoginRootScreen( onLoginWithSuccess: (SessionId) -> Unit = {}, ) { val eventSink = state.eventSink - ElementSurface( - modifier = modifier, + 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, + ) + // 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() - ) { - ElementOutlinedTextField( - value = state.homeserver, - modifier = Modifier.fillMaxWidth(), - onValueChange = { /* no op */ }, - enabled = false, - label = { - Text(text = "Server") - }, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Uri, - ), - ) - ElementButton( - onClick = onChangeServer, - modifier = Modifier - .align(Alignment.CenterEnd) - .testTag(TestTags.loginChangeServer) - .padding(top = 8.dp, end = 8.dp), - content = { - Text(text = "Change") - } - ) - } ElementOutlinedTextField( - 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 - } - ElementOutlinedTextField( - value = passwordFieldState, + ElementButton( + 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") + } + ) + } + ElementOutlinedTextField( + 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 + } + ElementOutlinedTextField( + 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 = ElementTheme.colors.error, - style = ElementTheme.typography.bodySmall, - modifier = Modifier.padding(start = 16.dp) - ) - } - } - // Submit - ElementButton( - 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) { - ElementCircularProgressIndicator( - 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 = ElementTheme.colors.error, + style = ElementTheme.typography.bodySmall, + modifier = Modifier.padding(start = 16.dp) + ) + } } + // Submit + ElementButton( + 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) { + ElementCircularProgressIndicator( + modifier = Modifier.align(Alignment.Center) + ) } } } 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 684d7a02ce..6cab7dd091 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 @@ -47,9 +47,7 @@ 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.theme.ElementTheme import io.element.android.libraries.designsystem.theme.components.ElementButton -import io.element.android.libraries.designsystem.theme.components.ElementSurface import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import kotlinx.coroutines.delay @@ -67,60 +65,55 @@ fun OnBoardingScreen( val carrouselState = remember { SplashCarouselStateFactory().create() } val nbOfPages = carrouselState.items.size var key by remember { mutableStateOf(false) } - ElementSurface( - modifier = modifier, - color = ElementTheme.colors.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(carrouselState.items[page]) + } + HorizontalPagerIndicator( + pagerState = pagerState, + modifier = Modifier + .align(CenterHorizontally) + .padding(16.dp), + ) + ElementButton( + 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), - ) - ElementButton( - onClick = { - onSignIn() - }, - enabled = true, - modifier = Modifier - .align(CenterHorizontally) - .testTag(TestTags.onBoardingSignIn) - .padding(top = 16.dp) - ) { - Text(text = stringResource(id = StringR.string.login_splash_submit)) - } + Text(text = stringResource(id = StringR.string.login_splash_submit)) } } } 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 75b0a5228e..a81f961ac3 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 @@ -51,7 +51,6 @@ import io.element.android.libraries.designsystem.components.form.textFieldState import io.element.android.libraries.designsystem.theme.components.ElementButton import io.element.android.libraries.designsystem.theme.components.ElementCircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.ElementOutlinedTextField -import io.element.android.libraries.designsystem.theme.components.ElementSurface import io.element.android.libraries.designsystem.utils.LogCompositions import io.element.android.libraries.ui.strings.R as StringR @@ -70,139 +69,135 @@ fun BugReportView( } return } - ElementSurface( - modifier = modifier, + 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, + ) + // 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), ) { - val isError = state.sending is Async.Failure - val isFormEnabled = state.sending !is Async.Loading - // Title - Text( - text = stringResource(id = StringR.string.send_bug_report), + ElementOutlinedTextField( + 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), - ) { - ElementOutlinedTextField( - 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 - ElementButton( - 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 -> { - ElementCircularProgressIndicator( - progress = state.sendingProgress, - modifier = Modifier.align(Alignment.Center) - ) - } - is Async.Failure -> ErrorDialog( - content = state.sending.error.toString(), + // Submit + ElementButton( + 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 -> { + ElementCircularProgressIndicator( + progress = state.sendingProgress, + modifier = Modifier.align(Alignment.Center) ) - else -> Unit } + is Async.Failure -> ErrorDialog( + content = state.sending.error.toString(), + ) + else -> Unit } } } From e3fed8b4f51b509ddba7d1907a785a12019e0080 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 27 Jan 2023 09:24:16 +0100 Subject: [PATCH 19/96] Optimize import --- app/src/main/kotlin/io/element/android/x/node/RootFlowNode.kt | 1 - .../io/element/android/features/login/root/LoginRootScreen.kt | 1 - .../io/element/android/features/messages/MessagesView.kt | 4 ---- .../android/features/messages/actionlist/ActionListView.kt | 1 - .../android/features/messages/timeline/TimelineView.kt | 1 - .../messages/timeline/components/MessageEventBubble.kt | 2 +- .../messages/timeline/components/TimelineItemReactionsView.kt | 2 +- .../messages/timeline/components/html/HtmlDocument.kt | 2 +- .../android/features/roomlist/components/RoomListTopBar.kt | 2 -- .../kotlin/io/element/android/libraries/designsystem/Type.kt | 2 -- .../libraries/designsystem/components/LabelledCheckbox.kt | 1 - .../libraries/designsystem/components/ProgressDialog.kt | 1 - .../designsystem/components/dialogs/ConfirmationDialog.kt | 2 +- .../libraries/designsystem/components/dialogs/ErrorDialog.kt | 2 +- .../designsystem/components/preferences/PreferenceScreen.kt | 2 -- .../designsystem/components/preferences/PreferenceSlide.kt | 1 - .../designsystem/components/preferences/PreferenceSwitch.kt | 1 - .../kotlin/io/element/android/tests/uitests/ShowkaseButton.kt | 1 - 18 files changed, 5 insertions(+), 24 deletions(-) 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 24ae5ea389..22450a1382 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 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 71ca4ebe15..a0de5b51d8 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 @@ -18,7 +18,6 @@ package io.element.android.features.login.root -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize 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 5baad6bc9d..67bba09e15 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 @@ -21,7 +21,6 @@ package io.element.android.features.messages -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -42,11 +41,9 @@ 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 @@ -65,7 +62,6 @@ 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.ElementTheme import io.element.android.libraries.designsystem.theme.components.ElementScaffold import io.element.android.libraries.designsystem.theme.components.ElementTopAppBar import io.element.android.libraries.designsystem.utils.LogCompositions 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 683dd28dc8..4adafbea3c 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,7 +29,6 @@ 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.ModalBottomSheetLayout import androidx.compose.material.ModalBottomSheetState import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.Text 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 d08642993e..8969ea94b1 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,7 +39,6 @@ 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.CircularProgressIndicator import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.Text 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 ddf2a66105..dc12908f5f 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 @@ -29,8 +29,8 @@ 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.theme.components.ElementSurface import io.element.android.libraries.designsystem.theme.ElementTheme +import io.element.android.libraries.designsystem.theme.components.ElementSurface private val BUBBLE_RADIUS = 16.dp 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 4a8ac852b0..41f4851454 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 @@ -32,8 +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.ElementSurface import io.element.android.libraries.designsystem.theme.ElementTheme +import io.element.android.libraries.designsystem.theme.components.ElementSurface @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 092f503b67..6f673345e4 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 @@ -46,9 +46,9 @@ 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.ElementSurface import io.element.android.libraries.designsystem.theme.ElementColors import io.element.android.libraries.designsystem.theme.ElementTheme +import io.element.android.libraries.designsystem.theme.components.ElementSurface 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/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 a2549a8f9f..1437c0d59a 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 @@ -29,11 +29,9 @@ 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.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 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/Type.kt index 50d5b7d1fd..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/Type.kt @@ -16,9 +16,7 @@ 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 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 90646930ec..cfe2e4b5c7 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,7 +18,6 @@ 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.runtime.Composable import androidx.compose.ui.Alignment 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 ced336c0e0..baaa7836fa 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,7 +22,6 @@ 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.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment 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 4d36007f1f..909bae4d54 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 @@ -32,8 +32,8 @@ 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.theme.components.ElementButton import io.element.android.libraries.designsystem.theme.ElementTheme +import io.element.android.libraries.designsystem.theme.components.ElementButton import io.element.android.libraries.ui.strings.R as StringR @Composable 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 c1ca38b11c..a817d333cf 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 @@ -31,8 +31,8 @@ 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.theme.components.ElementButton import io.element.android.libraries.designsystem.theme.ElementTheme +import io.element.android.libraries.designsystem.theme.components.ElementButton import io.element.android.libraries.ui.strings.R as StringR @Composable 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 948daace12..86e4f7b22a 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 @@ -32,9 +32,7 @@ import androidx.compose.material.icons.filled.ArrowBack 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 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 244f257944..0887093db8 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 @@ -23,7 +23,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Slider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment 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 47e32f409d..d3ac0b89c5 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,7 +24,6 @@ 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.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment 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 7a89559b42..4d3f073724 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,7 +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 From 9d32b05fbbf3beff3ebdf79a6053bc15da3ae475 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 27 Jan 2023 10:19:17 +0100 Subject: [PATCH 20/96] Rework Preview for a better rendering in the IDE. --- .../login/changeserver/ChangeServerView.kt | 18 +++-- .../features/login/root/LoginRootScreen.kt | 16 +++-- .../features/logout/LogoutPreferenceScreen.kt | 13 +++- .../messages/timeline/TimelineView.kt | 19 ++++-- .../components/TimelineItemInformativeView.kt | 22 +++++++ .../features/onboarding/OnBoardingScreen.kt | 2 - .../preferences/root/PreferencesRootView.kt | 11 +++- .../rageshake/bugreport/BugReportView.kt | 18 ++++- .../crash/ui/CrashDetectionScreen.kt | 11 +++- .../detection/RageshakeDetectionView.kt | 11 +++- .../preferences/RageshakePreferencesView.kt | 24 +++++-- .../android/features/roomlist/RoomListView.kt | 11 +++- .../roomlist/components/RoomSummaryRow.kt | 14 ++-- .../android/features/template/TemplateView.kt | 19 +++++- .../libraries/designsystem/ColorUtil.kt | 2 +- .../components/ClickableLinkText.kt | 4 +- .../components/LabelledCheckbox.kt | 17 ++++- .../designsystem/components/ProgressDialog.kt | 13 +++- .../designsystem/components/avatar/Avatar.kt | 11 +++- .../components/dialogs/ConfirmationDialog.kt | 13 +++- .../components/dialogs/ErrorDialog.kt | 13 +++- .../preferences/PreferenceCategory.kt | 33 ++++++++-- .../preferences/PreferenceScreen.kt | 34 +++++++++- .../components/preferences/PreferenceSlide.kt | 13 +++- .../preferences/PreferenceSwitch.kt | 14 +++- .../components/preferences/PreferenceText.kt | 16 ++++- .../designsystem/preview/ElementPreview.kt | 65 +++++++++++++++++++ .../matrix/ui/components/MatrixUserHeader.kt | 25 +++++-- .../matrix/ui/components/MatrixUserRow.kt | 14 +++- libraries/textcomposer/build.gradle.kts | 1 + .../libraries/textcomposer/TextComposer.kt | 15 ++++- .../android/tests/uitests/ShowkaseButton.kt | 13 +++- 32 files changed, 452 insertions(+), 73 deletions(-) create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreview.kt 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 4ffeb42def..1820448df8 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 @@ -52,6 +50,8 @@ 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.ElementTheme import io.element.android.libraries.designsystem.theme.components.ElementButton import io.element.android.libraries.designsystem.theme.components.ElementCircularProgressIndicator @@ -109,6 +109,7 @@ fun ChangeServerView( textAlign = TextAlign.Center, fontWeight = FontWeight.Bold, fontSize = 24.sp, + color = ElementTheme.colors.primary, ) Text( text = "A server is a home for all your data.\n" + @@ -119,7 +120,7 @@ fun ChangeServerView( .padding(top = 16.dp), textAlign = TextAlign.Center, fontSize = 16.sp, - color = ElementTheme.colors.secondary + color = ElementTheme.colors.secondary, ) var homeserverFieldState by textFieldState(stateValue = state.homeserver) ElementOutlinedTextField( @@ -177,9 +178,16 @@ fun ChangeServerView( } } -@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 a0de5b51d8..ada2a9d470 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 @@ -55,6 +53,8 @@ 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.ElementTheme import io.element.android.libraries.designsystem.theme.components.ElementButton import io.element.android.libraries.designsystem.theme.components.ElementCircularProgressIndicator @@ -99,6 +99,7 @@ fun LoginRootScreen( textAlign = TextAlign.Center, fontWeight = FontWeight.Bold, fontSize = 24.sp, + color = ElementTheme.colors.primary, ) // Form Column( @@ -219,9 +220,16 @@ fun LoginRootScreen( } } -@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/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/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 8969ea94b1..9f0cd5326a 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 @@ -77,6 +77,8 @@ import io.element.android.features.messages.timeline.model.content.TimelineItemT import io.element.android.features.messages.timeline.model.content.TimelineItemUnknownContent 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.ElementTheme import io.element.android.libraries.designsystem.theme.components.ElementCircularProgressIndicator import io.element.android.libraries.designsystem.utils.PairCombinedPreviewParameter @@ -355,13 +357,20 @@ class MessagesItemGroupPositionToMessagesTimelineItemContentProvider : TimelineItemGroupPositionProvider() to MessagesTimelineItemContentProvider() ) -@Suppress("PreviewPublic") @Preview @Composable -fun TimelineItemsPreview( - @PreviewParameter(MessagesTimelineItemContentProvider::class) - content: TimelineItemContent -) { +fun LoginRootScreenLightPreview( + @PreviewParameter(MessagesTimelineItemContentProvider::class) content: TimelineItemContent +) = ElementPreviewLight { ContentToPreview(content) } + +@Preview +@Composable +fun LoginRootScreenDarkPreview( + @PreviewParameter(MessagesTimelineItemContentProvider::class) content: TimelineItemContent +) = ElementPreviewDark { ContentToPreview(content) } + +@Composable +private fun ContentToPreview(content: TimelineItemContent) { val timelineItems = persistentListOf( // 3 items (First Middle Last) with isMine = false createMessageEvent( 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 40499798ee..b4c54194c3 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,6 +20,8 @@ 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.material.icons.Icons +import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -27,8 +29,11 @@ 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.ElementTheme @Composable @@ -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/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 6cab7dd091..e21083c0f6 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 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/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 a81f961ac3..fe9e409c00 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 @@ -48,6 +48,9 @@ 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.ElementTheme import io.element.android.libraries.designsystem.theme.components.ElementButton import io.element.android.libraries.designsystem.theme.components.ElementCircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.ElementOutlinedTextField @@ -94,6 +97,7 @@ fun BugReportView( textAlign = TextAlign.Center, fontWeight = FontWeight.Bold, fontSize = 24.sp, + color = ElementTheme.colors.primary, ) // Form Text( @@ -102,7 +106,8 @@ fun BugReportView( .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 16.dp), fontSize = 16.sp, - ) + color = ElementTheme.colors.primary, + ) var descriptionFieldState by textFieldState( stateValue = state.formState.description ) @@ -202,9 +207,16 @@ fun BugReportView( } } -@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/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/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/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/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 8aff881f24..ca75896b38 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 @@ -42,6 +42,8 @@ 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.ElementScaffold import io.element.android.libraries.designsystem.utils.LogCompositions import io.element.android.libraries.matrix.core.RoomId @@ -156,7 +158,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/RoomSummaryRow.kt b/features/roomlist/src/main/kotlin/io/element/android/features/roomlist/components/RoomSummaryRow.kt index 9693d17a26..037b3afa49 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 @@ -115,10 +115,10 @@ internal fun DefaultRoomSummaryRow( // Name Text( modifier = Modifier.placeholder( - visible = room.isPlaceholder, - shape = TextPlaceholderShape, - color = ElementTheme.colors.roomListPlaceHolder, - ), + visible = room.isPlaceholder, + shape = TextPlaceholderShape, + color = ElementTheme.colors.roomListPlaceHolder, + ), fontSize = 16.sp, fontWeight = FontWeight.SemiBold, text = room.name, @@ -146,7 +146,11 @@ 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 = ElementTheme.colors.roomListRoomMessageDate, 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..b954b4d9d7 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 @@ -22,6 +22,9 @@ 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.ElementTheme @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 = ElementTheme.colors.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/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 def4c49e89..f6a0122a80 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) { ElementTheme.colors.primary } else { - ElementTheme.colors.secondary + ElementTheme.colors.primary.copy(alpha = 0.40f) } } 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..b5b196ce51 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 @@ -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.ElementTheme 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 = ElementTheme.colors.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 cfe2e4b5c7..66a43c5a81 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 @@ -23,6 +23,9 @@ 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.ElementTheme import io.element.android.libraries.designsystem.theme.components.ElementCheckbox @Composable @@ -42,13 +45,23 @@ fun LabelledCheckbox( onCheckedChange = onCheckedChange, enabled = enabled, ) - Text(text = text) + Text( + text = text, + color = ElementTheme.colors.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 baaa7836fa..2947b04ffb 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 @@ -30,6 +30,8 @@ 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.ElementTheme import io.element.android.libraries.designsystem.theme.components.ElementCircularProgressIndicator @@ -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/avatar/Avatar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt index 1df00a7c50..0fa2c9fe1a 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 @@ -34,6 +34,8 @@ 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 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 909bae4d54..bc99927e9e 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 @@ -32,6 +32,8 @@ 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.ElementTheme import io.element.android.libraries.designsystem.theme.components.ElementButton import io.element.android.libraries.ui.strings.R as StringR @@ -113,9 +115,16 @@ fun ConfirmationDialog( ) } -@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 a817d333cf..9d72a1f1a0 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 @@ -31,6 +31,8 @@ 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.ElementTheme import io.element.android.libraries.designsystem.theme.components.ElementButton import io.element.android.libraries.ui.strings.R as StringR @@ -82,9 +84,16 @@ fun ErrorDialog( ) } -@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 65560e57f7..dfd75410d2 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,12 +20,17 @@ 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.material.icons.Icons +import androidx.compose.material.icons.filled.Announcement +import androidx.compose.material.icons.filled.BugReport import androidx.compose.material3.Divider 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.ElementTheme @Composable @@ -52,14 +57,32 @@ fun PreferenceCategory( } } -@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 86e4f7b22a..fbc5281cf2 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,7 +28,9 @@ 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 @@ -40,6 +42,8 @@ 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.ElementScaffold import io.element.android.libraries.designsystem.theme.components.ElementTopAppBar @@ -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 0887093db8..2caf1efaa3 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 @@ -30,6 +30,8 @@ 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.ElementTheme import io.element.android.libraries.designsystem.theme.components.ElementSlider import io.element.android.libraries.designsystem.toEnabledColor @@ -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 d3ac0b89c5..f89e945caf 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 @@ -31,6 +31,8 @@ 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.ElementTheme import io.element.android.libraries.designsystem.theme.components.ElementCheckbox import io.element.android.libraries.designsystem.toEnabledColor @@ -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 6f4c1598b7..824d264807 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 @@ -31,6 +31,8 @@ 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.ElementTheme @Composable @@ -57,15 +59,23 @@ fun PreferenceText( .weight(1f) .padding(end = preferencePaddingEnd), style = ElementTheme.typography.bodyLarge, - text = title + text = title, + color = ElementTheme.colors.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/preview/ElementPreview.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreview.kt new file mode 100644 index 0000000000..7a286f5314 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/ElementPreview.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.preview + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +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(ElementTheme.colors.background)) { + content() + } + } else { + content() + } + } +} + 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 94cc41c0f4..64aace9cdf 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 @@ -35,6 +35,8 @@ 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.ElementTheme import io.element.android.libraries.matrix.core.UserId import io.element.android.libraries.matrix.ui.model.MatrixUser @@ -64,8 +66,9 @@ fun MatrixUserHeader( fontWeight = FontWeight.SemiBold, text = matrixUser.getBestName(), maxLines = 1, - overflow = TextOverflow.Ellipsis - ) + overflow = TextOverflow.Ellipsis, + color = ElementTheme.colors.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 e813fc8b50..c416821152 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 @@ -34,6 +34,8 @@ 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.ElementTheme import io.element.android.libraries.matrix.core.UserId import io.element.android.libraries.matrix.ui.model.MatrixUser @@ -68,7 +70,8 @@ fun MatrixUserRow( fontWeight = FontWeight.SemiBold, text = matrixUser.getBestName(), maxLines = 1, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, + color = ElementTheme.colors.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..0f98b3e593 100644 --- a/libraries/textcomposer/build.gradle.kts +++ b/libraries/textcomposer/build.gradle.kts @@ -35,6 +35,7 @@ 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") 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..4c8a4a0d9c 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 @@ -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.ElementTheme 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 = ElementTheme.colors.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/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 4d3f073724..45ef287018 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 @@ -31,6 +31,8 @@ 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.ElementButton @Composable @@ -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() } From 01aea5a9afde15aa1887d92c7db20fb20f843fce Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 27 Jan 2023 10:38:30 +0100 Subject: [PATCH 21/96] Add some Preview --- .../theme/components/ElementButton.kt | 25 ++++++++++++++++ .../theme/components/ElementCheckbox.kt | 22 ++++++++++++++ .../components/ElementOutlinedTextField.kt | 25 ++++++++++++++++ .../theme/components/ElementSlider.kt | 30 +++++++++++++++---- 4 files changed, 97 insertions(+), 5 deletions(-) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementButton.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementButton.kt index d9a536b4db..a9abdbb541 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementButton.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementButton.kt @@ -16,16 +16,21 @@ package io.element.android.libraries.designsystem.theme.components +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.RowScope import androidx.compose.material.ContentAlpha import androidx.compose.material.ProvideTextStyle import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.compositeOver +import androidx.compose.ui.tooling.preview.Preview import io.element.android.libraries.designsystem.components.color.elementContentColorFor +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.ElementTheme @Composable @@ -64,3 +69,23 @@ fun ElementButton( } ) } + +@Preview +@Composable +fun ElementButtonsLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun ElementButtonsDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + Column { + ElementButton(onClick = {}, enabled = true) { + Text(text = "Click me! - Enabled") + } + ElementButton(onClick = {}, enabled = false) { + Text(text = "Click me! - Disabled") + } + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementCheckbox.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementCheckbox.kt index 058313d40d..6e77b67e0a 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementCheckbox.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementCheckbox.kt @@ -17,6 +17,7 @@ package io.element.android.libraries.designsystem.theme.components import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column import androidx.compose.material3.Checkbox import androidx.compose.material3.CheckboxColors import androidx.compose.material3.CheckboxDefaults @@ -24,6 +25,9 @@ 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.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.ElementTheme @Composable @@ -53,3 +57,21 @@ fun ElementCheckbox( interactionSource = interactionSource, ) } + +@Preview +@Composable +fun ElementCheckboxesLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun ElementCheckboxesDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + Column { + ElementCheckbox(onCheckedChange = {}, enabled = true, checked = true) + ElementCheckbox(onCheckedChange = {}, enabled = true, checked = false) + ElementCheckbox(onCheckedChange = {}, enabled = false, checked = true) + ElementCheckbox(onCheckedChange = {}, enabled = false, checked = false) + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementOutlinedTextField.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementOutlinedTextField.kt index 04b0eb402f..29bd7239d8 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementOutlinedTextField.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementOutlinedTextField.kt @@ -17,6 +17,7 @@ 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.foundation.text.selection.TextSelectionColors @@ -32,7 +33,10 @@ import androidx.compose.ui.graphics.Color 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.ElementGreen +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.ElementTheme @OptIn(ExperimentalMaterial3Api::class) @@ -115,3 +119,24 @@ fun ElementOutlinedTextField( ) } +@Preview +@Composable +fun ElementOutlinedTextFieldsLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun ElementOutlinedTextFieldsDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + Column { + ElementOutlinedTextField(onValueChange = {}, value = "Content", isError = false, enabled = true, readOnly = true) + ElementOutlinedTextField(onValueChange = {}, value = "Content", isError = false, enabled = true, readOnly = false) + ElementOutlinedTextField(onValueChange = {}, value = "Content", isError = false, enabled = false, readOnly = true) + ElementOutlinedTextField(onValueChange = {}, value = "Content", isError = false, enabled = false, readOnly = false) + ElementOutlinedTextField(onValueChange = {}, value = "Content", isError = true, enabled = true, readOnly = true) + ElementOutlinedTextField(onValueChange = {}, value = "Content", isError = true, enabled = true, readOnly = false) + ElementOutlinedTextField(onValueChange = {}, value = "Content", isError = true, enabled = false, readOnly = true) + ElementOutlinedTextField(onValueChange = {}, value = "Content", isError = true, enabled = false, readOnly = false) + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementSlider.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementSlider.kt index 318aeb2b41..4e2d31f5f4 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementSlider.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementSlider.kt @@ -17,12 +17,16 @@ package io.element.android.libraries.designsystem.theme.components import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column import androidx.compose.material3.Slider 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 import io.element.android.libraries.designsystem.theme.ElementTheme @Composable @@ -41,11 +45,11 @@ fun ElementSlider( activeTickColor = ElementTheme.colors.primary, inactiveTrackColor = ElementTheme.colors.primary, inactiveTickColor = ElementTheme.colors.primary, - disabledThumbColor = ElementTheme.colors.primary, - disabledActiveTrackColor = ElementTheme.colors.primary, - disabledActiveTickColor = ElementTheme.colors.primary, - disabledInactiveTrackColor = ElementTheme.colors.primary, - disabledInactiveTickColor = ElementTheme.colors.primary, + disabledThumbColor = ElementTheme.colors.primary.copy(alpha = 0.40f), + disabledActiveTrackColor = ElementTheme.colors.primary.copy(alpha = 0.40f), + disabledActiveTickColor = ElementTheme.colors.primary.copy(alpha = 0.40f), + disabledInactiveTrackColor = ElementTheme.colors.primary.copy(alpha = 0.40f), + disabledInactiveTickColor = ElementTheme.colors.primary.copy(alpha = 0.40f), ), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } ) { @@ -61,3 +65,19 @@ fun ElementSlider( interactionSource = interactionSource, ) } + +@Preview +@Composable +fun ElementSlidersLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun ElementSlidersDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + Column { + ElementSlider(onValueChange = {}, value = 0.33f, enabled = true) + ElementSlider(onValueChange = {}, value = 0.33f, enabled = false) + } +} From 2cac2b59abacf90747188ed682eb3f1727becc4d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 27 Jan 2023 10:42:18 +0100 Subject: [PATCH 22/96] Cleanup and TODO. --- .../android/features/login/changeserver/ChangeServerView.kt | 1 - .../io/element/android/features/login/root/LoginRootScreen.kt | 1 - .../element/android/features/messages/timeline/TimelineView.kt | 2 +- .../io/element/android/features/onboarding/OnBoardingScreen.kt | 1 - .../android/features/roomlist/components/RoomListTopBar.kt | 2 +- 5 files changed, 2 insertions(+), 5 deletions(-) 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 1820448df8..6a60c52f2e 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 @@ -31,7 +31,6 @@ 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.ExperimentalMaterial3Api import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue 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 ada2a9d470..91cf5f00f5 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 @@ -30,7 +30,6 @@ 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.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text 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 9f0cd5326a..17dc191493 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,7 +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.FloatingActionButton // TODO ElementFloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable 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 e21083c0f6..17922cafd2 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 @@ -24,7 +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.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect 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 1437c0d59a..8615eb1e7c 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 @@ -30,7 +30,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text -import androidx.compose.material3.TextField +import androidx.compose.material3.TextField // TODO ElementTextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable From 9f3f345088290129364f2b3181db3826019a3d8a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 27 Jan 2023 12:58:22 +0100 Subject: [PATCH 23/96] Material 1.8 --- gradle/libs.versions.toml | 2 +- libraries/elementresources/build.gradle.kts | 2 +- libraries/textcomposer/build.gradle.kts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c3b00a39a9..036a9060be 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" 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/textcomposer/build.gradle.kts b/libraries/textcomposer/build.gradle.kts index 0f98b3e593..6f15fefa42 100644 --- a/libraries/textcomposer/build.gradle.kts +++ b/libraries/textcomposer/build.gradle.kts @@ -38,6 +38,6 @@ dependencies { 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) } From cdfc13cb15f920852146c00877e07ca34a57c808 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 30 Jan 2023 15:52:25 +0100 Subject: [PATCH 24/96] Material3 only in designsystem. --- .../android/x/component/ShowkaseButton.kt | 6 +- .../android/x/node/LoggedInFlowNode.kt | 2 +- .../login/changeserver/ChangeServerView.kt | 2 +- .../features/login/root/LoginRootScreen.kt | 6 +- features/messages/build.gradle.kts | 3 + .../android/features/messages/MessagesView.kt | 6 +- .../messages/timeline/TimelineView.kt | 6 +- .../components/TimelineItemInformativeView.kt | 4 +- .../components/TimelineItemReactionsView.kt | 2 +- .../timeline/components/html/HtmlDocument.kt | 2 +- .../features/onboarding/OnBoardingScreen.kt | 2 +- .../rageshake/bugreport/BugReportView.kt | 2 +- features/roomlist/build.gradle.kts | 3 + .../android/features/roomlist/RoomListView.kt | 3 +- .../roomlist/components/RoomListTopBar.kt | 17 +-- .../roomlist/components/RoomSummaryRow.kt | 2 +- .../android/features/template/TemplateView.kt | 1 - libraries/designsystem/build.gradle.kts | 3 + .../components/ClickableLinkText.kt | 2 +- .../components/LabelledCheckbox.kt | 2 +- .../designsystem/components/ProgressDialog.kt | 2 +- .../designsystem/components/VectorIcon.kt | 3 +- .../designsystem/components/avatar/Avatar.kt | 2 +- .../components/dialogs/ConfirmationDialog.kt | 2 +- .../components/dialogs/ErrorDialog.kt | 2 +- .../preferences/PreferenceCategory.kt | 4 +- .../preferences/PreferenceScreen.kt | 6 +- .../components/preferences/PreferenceSlide.kt | 2 +- .../preferences/PreferenceSwitch.kt | 2 +- .../components/preferences/PreferenceText.kt | 2 +- .../preferences/components/PreferenceIcon.kt | 2 +- .../designsystem/theme/ContentColor.kt | 4 +- .../designsystem/theme/components/Divider.kt | 36 ++++++ .../theme/components/ElementButton.kt | 6 +- .../theme/components/ElementCheckbox.kt | 3 +- .../ElementCircularProgressIndicator.kt | 5 +- .../ElementModalBottomSheetLayout.kt | 2 +- .../components/ElementOutlinedTextField.kt | 71 ++++++----- .../theme/components/ElementScaffold.kt | 5 +- .../theme/components/ElementSlider.kt | 3 +- .../theme/components/ElementSurface.kt | 5 +- .../theme/components/ElementTopAppBar.kt | 3 +- .../theme/components/FloatingActionButton.kt | 50 ++++++++ .../designsystem/theme/components/Icon.kt | 54 +++++++++ .../theme/components/IconButton.kt | 41 +++++++ .../designsystem/theme/components/Text.kt | 113 ++++++++++++++++++ .../theme/components/TextField.kt | 87 ++++++++++++++ .../matrix/ui/components/MatrixUserHeader.kt | 2 +- .../matrix/ui/components/MatrixUserRow.kt | 2 +- .../libraries/textcomposer/TextComposer.kt | 2 +- .../kotlin/extension/DependencyHandleScope.kt | 1 - .../android/tests/uitests/ShowkaseButton.kt | 6 +- 52 files changed, 485 insertions(+), 121 deletions(-) create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Divider.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/FloatingActionButton.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Icon.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/IconButton.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Text.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt 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 7a9ffe9791..d72680b710 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.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.ElementButton +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/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/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 6a60c52f2e..d924bba818 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 @@ -31,7 +31,6 @@ 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.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue @@ -55,6 +54,7 @@ import io.element.android.libraries.designsystem.theme.ElementTheme import io.element.android.libraries.designsystem.theme.components.ElementButton import io.element.android.libraries.designsystem.theme.components.ElementCircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.ElementOutlinedTextField +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag 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 91cf5f00f5..6720826171 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 @@ -30,9 +30,6 @@ 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.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 @@ -58,6 +55,9 @@ import io.element.android.libraries.designsystem.theme.ElementTheme import io.element.android.libraries.designsystem.theme.components.ElementButton import io.element.android.libraries.designsystem.theme.components.ElementCircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.ElementOutlinedTextField +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 import io.element.android.libraries.matrix.core.SessionId import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag diff --git a/features/messages/build.gradle.kts b/features/messages/build.gradle.kts index 975c7b5c0e..5931450294 100644 --- a/features/messages/build.gradle.kts +++ b/features/messages/build.gradle.kts @@ -47,4 +47,7 @@ dependencies { testImplementation(libs.test.junit) androidTestImplementation(libs.test.junitext) ksp(libs.showkase.processor) + + // Not for components, but other classes. + implementation("androidx.compose.material3:material3") } 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 67bba09e15..466c24ebe5 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,11 +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.SnackbarHost import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -64,6 +61,9 @@ 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.ElementScaffold import io.element.android.libraries.designsystem.theme.components.ElementTopAppBar +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 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/timeline/TimelineView.kt b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/TimelineView.kt index 17dc191493..609ea8acaa 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,9 +39,6 @@ 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 // TODO ElementFloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf @@ -81,6 +78,9 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.ElementTheme import io.element.android.libraries.designsystem.theme.components.ElementCircularProgressIndicator +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 io.element.android.libraries.designsystem.utils.PairCombinedPreviewParameter import io.element.android.libraries.matrix.core.EventId import kotlinx.collections.immutable.ImmutableList 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 b4c54194c3..5ad33a72ad 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 @@ -22,8 +22,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete -import androidx.compose.material3.Icon -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -35,6 +33,8 @@ 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.ElementTheme +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.Text @Composable fun TimelineItemInformativeView( 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 41f4851454..3779968afa 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 @@ -23,7 +23,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -34,6 +33,7 @@ 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.ElementTheme import io.element.android.libraries.designsystem.theme.components.ElementSurface +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 6f673345e4..4c6b38dcbf 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 @@ -26,7 +26,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.appendInlineContent import androidx.compose.material3.LocalTextStyle -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind @@ -49,6 +48,7 @@ import io.element.android.libraries.designsystem.components.ClickableLinkText import io.element.android.libraries.designsystem.theme.ElementColors import io.element.android.libraries.designsystem.theme.ElementTheme import io.element.android.libraries.designsystem.theme.components.ElementSurface +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/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 17922cafd2..e37bf1333e 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 @@ -24,7 +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.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -45,6 +44,7 @@ import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.HorizontalPagerIndicator import com.google.accompanist.pager.rememberPagerState import io.element.android.libraries.designsystem.theme.components.ElementButton +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 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 fe9e409c00..4dc3a858a1 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,7 +26,6 @@ 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.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -54,6 +53,7 @@ import io.element.android.libraries.designsystem.theme.ElementTheme import io.element.android.libraries.designsystem.theme.components.ElementButton import io.element.android.libraries.designsystem.theme.components.ElementCircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.ElementOutlinedTextField +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 diff --git a/features/roomlist/build.gradle.kts b/features/roomlist/build.gradle.kts index fc0402eafc..08b043a17f 100644 --- a/features/roomlist/build.gradle.kts +++ b/features/roomlist/build.gradle.kts @@ -54,4 +54,7 @@ dependencies { androidTestImplementation(libs.test.junitext) ksp(libs.showkase.processor) + + // Not for components, but other classes. + implementation("androidx.compose.material3:material3") } 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 ca75896b38..14c737a848 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 @@ -78,6 +76,7 @@ fun RoomListView( ) } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun RoomListView( roomSummaries: ImmutableList, 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 8615eb1e7c..1b6b7fd13c 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,11 +27,6 @@ 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.Text -import androidx.compose.material3.TextField // TODO ElementTextField -import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -54,6 +49,10 @@ import io.element.android.libraries.designsystem.components.form.textFieldState import io.element.android.libraries.designsystem.theme.ElementTheme import io.element.android.libraries.designsystem.theme.components.ElementMediumAppBar import io.element.android.libraries.designsystem.theme.components.ElementTopAppBar +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 +import io.element.android.libraries.designsystem.theme.components.TextField 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 @@ -151,14 +150,6 @@ fun SearchRoomListTopBar( } } }, - colors = TextFieldDefaults.textFieldColors( - textColor = ElementTheme.colors.onBackground, - containerColor = Color.Transparent, - cursorColor = ElementTheme.colors.onBackground.copy(alpha = ContentAlpha.medium), - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent - ) ) }, navigationIcon = { 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 037b3afa49..580d0ebc4b 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 @@ -31,7 +31,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.ripple.rememberRipple -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -54,6 +53,7 @@ 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 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 b954b4d9d7..a98912d958 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,7 +17,6 @@ package io.element.android.features.template import androidx.compose.foundation.layout.Box -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier diff --git a/libraries/designsystem/build.gradle.kts b/libraries/designsystem/build.gradle.kts index 45430e5d82..10e51c8aec 100644 --- a/libraries/designsystem/build.gradle.kts +++ b/libraries/designsystem/build.gradle.kts @@ -31,5 +31,8 @@ android { implementation(projects.libraries.elementresources) implementation(projects.libraries.uiStrings) ksp(libs.showkase.processor) + + // This is the only module to have a (temporary) dependence to material3. + implementation("androidx.compose.material3:material3") } } 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 b5b196ce51..a8a5b94ff4 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,6 @@ 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.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -33,6 +32,7 @@ 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.ElementTheme +import io.element.android.libraries.designsystem.theme.components.Text import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.persistentMapOf 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 66a43c5a81..af3596127b 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,7 +18,6 @@ package io.element.android.libraries.designsystem.components import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -27,6 +26,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.ElementTheme import io.element.android.libraries.designsystem.theme.components.ElementCheckbox +import io.element.android.libraries.designsystem.theme.components.Text @Composable fun LabelledCheckbox( 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 2947b04ffb..e48b41937b 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,7 +22,6 @@ 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.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -34,6 +33,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.ElementTheme import io.element.android.libraries.designsystem.theme.components.ElementCircularProgressIndicator +import io.element.android.libraries.designsystem.theme.components.Text @Composable fun ProgressDialog( 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 0fa2c9fe1a..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 @@ -36,6 +35,7 @@ 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 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 bc99927e9e..903f9a07ba 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 @@ -23,7 +23,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialogDefaults -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -36,6 +35,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.ElementTheme import io.element.android.libraries.designsystem.theme.components.ElementButton +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.ui.strings.R as StringR @Composable 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 9d72a1f1a0..feba85cc73 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 @@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialogDefaults -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -35,6 +34,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.ElementTheme import io.element.android.libraries.designsystem.theme.components.ElementButton +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.ui.strings.R as StringR @Composable 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 dfd75410d2..7206fb1081 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 @@ -23,8 +23,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Announcement import androidx.compose.material.icons.filled.BugReport -import androidx.compose.material3.Divider -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview @@ -32,6 +30,8 @@ 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.ElementTheme +import io.element.android.libraries.designsystem.theme.components.Divider +import io.element.android.libraries.designsystem.theme.components.Text @Composable fun PreferenceCategory( 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 fbc5281cf2..caac7b93c2 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 @@ -32,9 +32,6 @@ 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.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -46,6 +43,9 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.components.ElementScaffold import io.element.android.libraries.designsystem.theme.components.ElementTopAppBar +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 @OptIn(ExperimentalMaterial3Api::class) @Composable 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 2caf1efaa3..6335929641 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 @@ -23,7 +23,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -34,6 +33,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.ElementTheme import io.element.android.libraries.designsystem.theme.components.ElementSlider +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.toEnabledColor @Composable 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 f89e945caf..173d427ff1 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,7 +24,6 @@ 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.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -35,6 +34,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.ElementTheme import io.element.android.libraries.designsystem.theme.components.ElementCheckbox +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.toEnabledColor @Composable 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 824d264807..b7b7662045 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 @@ -24,7 +24,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.BugReport -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -34,6 +33,7 @@ import io.element.android.libraries.designsystem.components.preferences.componen import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.ElementTheme +import io.element.android.libraries.designsystem.theme.components.Text @Composable fun PreferenceText( 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/theme/ContentColor.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ContentColor.kt index 56df033d27..1af916a109 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ContentColor.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ContentColor.kt @@ -14,15 +14,13 @@ * limitations under the License. */ -package io.element.android.libraries.designsystem.components.color +package io.element.android.libraries.designsystem.theme import androidx.compose.material3.LocalContentColor import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.takeOrElse -import io.element.android.libraries.designsystem.theme.ElementColors -import io.element.android.libraries.designsystem.theme.ElementTheme @Composable @ReadOnlyComposable 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..4889c4c473 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Divider.kt @@ -0,0 +1,36 @@ +/* + * 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.unit.Dp + +@Composable +fun Divider( + modifier: Modifier = Modifier, + thickness: Dp = DividerDefaults.Thickness, + color: Color = DividerDefaults.color, +) { + androidx.compose.material3.Divider( + modifier = modifier, + thickness = thickness, + color = color, + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementButton.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementButton.kt index a9abdbb541..e66571b2e7 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementButton.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementButton.kt @@ -20,18 +20,16 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.RowScope import androidx.compose.material.ContentAlpha import androidx.compose.material.ProvideTextStyle -import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.tooling.preview.Preview -import io.element.android.libraries.designsystem.components.color.elementContentColorFor import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.ElementTheme +import io.element.android.libraries.designsystem.theme.elementContentColorFor @Composable fun ElementButton( @@ -41,7 +39,7 @@ fun ElementButton( containerColor: Color = ElementTheme.colors.primary, content: @Composable RowScope.() -> Unit ) { - Button( + androidx.compose.material3.Button( colors = ButtonDefaults.buttonColors( containerColor = containerColor, contentColor = elementContentColorFor(backgroundColor = containerColor), diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementCheckbox.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementCheckbox.kt index 6e77b67e0a..6db3f62c8d 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementCheckbox.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementCheckbox.kt @@ -18,7 +18,6 @@ package io.element.android.libraries.designsystem.theme.components import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Column -import androidx.compose.material3.Checkbox import androidx.compose.material3.CheckboxColors import androidx.compose.material3.CheckboxDefaults import androidx.compose.runtime.Composable @@ -48,7 +47,7 @@ fun ElementCheckbox( ), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } ) { - Checkbox( + androidx.compose.material3.Checkbox( checked = checked, onCheckedChange = onCheckedChange, modifier = modifier, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementCircularProgressIndicator.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementCircularProgressIndicator.kt index a5188bab51..256745dbbf 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementCircularProgressIndicator.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementCircularProgressIndicator.kt @@ -16,7 +16,6 @@ package io.element.android.libraries.designsystem.theme.components -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ProgressIndicatorDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -31,7 +30,7 @@ fun ElementCircularProgressIndicator( color: Color = ProgressIndicatorDefaults.circularColor, strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth ) { - CircularProgressIndicator( + androidx.compose.material3.CircularProgressIndicator( modifier = modifier, progress = progress, color = color, @@ -45,7 +44,7 @@ fun ElementCircularProgressIndicator( color: Color = ElementTheme.colors.primary, strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth, ) { - CircularProgressIndicator( + 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/ElementModalBottomSheetLayout.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementModalBottomSheetLayout.kt index 7fbd2caf49..c1a5ddc2e7 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementModalBottomSheetLayout.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementModalBottomSheetLayout.kt @@ -29,7 +29,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.Dp -import io.element.android.libraries.designsystem.components.color.elementContentColorFor +import io.element.android.libraries.designsystem.theme.elementContentColorFor import io.element.android.libraries.designsystem.theme.ElementTheme @OptIn(ExperimentalMaterialApi::class) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementOutlinedTextField.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementOutlinedTextField.kt index 29bd7239d8..5e57b85507 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementOutlinedTextField.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementOutlinedTextField.kt @@ -23,8 +23,6 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.selection.TextSelectionColors import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LocalTextStyle -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.TextFieldColors import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -61,41 +59,8 @@ fun ElementOutlinedTextField( maxLines: Int = Int.MAX_VALUE, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = TextFieldDefaults.outlinedShape, - colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors( - textColor = ElementTheme.colors.primary, - disabledTextColor = ElementTheme.colors.primary.copy(alpha = 0.38f), - containerColor = Color.Transparent, - cursorColor = ElementTheme.colors.primary, - errorCursorColor = ElementTheme.colors.error, - selectionColors = TextSelectionColors( - handleColor = ElementGreen, - backgroundColor = ElementGreen.copy(alpha = 0.4f) - ), - focusedBorderColor = ElementTheme.colors.primary, - unfocusedBorderColor = ElementTheme.colors.secondary, - disabledBorderColor = ElementTheme.colors.secondary.copy(alpha = 0.12f), - errorBorderColor = ElementTheme.colors.error, - focusedLeadingIconColor = ElementTheme.colors.primary, - unfocusedLeadingIconColor = ElementTheme.colors.secondary, - disabledLeadingIconColor = ElementTheme.colors.secondary.copy(0.12f), - errorLeadingIconColor = ElementTheme.colors.error, - focusedTrailingIconColor = ElementTheme.colors.primary, - unfocusedTrailingIconColor = ElementTheme.colors.secondary, - disabledTrailingIconColor = ElementTheme.colors.secondary.copy(alpha = 0.12f), - errorTrailingIconColor = ElementTheme.colors.error, - focusedLabelColor = ElementTheme.colors.primary, - unfocusedLabelColor = ElementTheme.colors.secondary, - disabledLabelColor = ElementTheme.colors.secondary.copy(alpha = 0.12f), - errorLabelColor = ElementTheme.colors.error, - placeholderColor = ElementTheme.colors.secondary, - disabledPlaceholderColor = ElementTheme.colors.secondary.copy(alpha = 0.12f), - focusedSupportingTextColor = ElementTheme.colors.primary, - unfocusedSupportingTextColor = ElementTheme.colors.secondary, - disabledSupportingTextColor = ElementTheme.colors.primary.copy(alpha = 0.12f), - errorSupportingTextColor = ElementTheme.colors.error, - ) ) { - OutlinedTextField( + androidx.compose.material3.OutlinedTextField( value = value, onValueChange = onValueChange, modifier = modifier, @@ -115,7 +80,39 @@ fun ElementOutlinedTextField( maxLines = maxLines, interactionSource = interactionSource, shape = shape, - colors = colors, + colors = TextFieldDefaults.outlinedTextFieldColors( + textColor = ElementTheme.colors.primary, + disabledTextColor = ElementTheme.colors.primary.copy(alpha = 0.38f), + containerColor = Color.Transparent, + cursorColor = ElementTheme.colors.primary, + errorCursorColor = ElementTheme.colors.error, + selectionColors = TextSelectionColors( + handleColor = ElementGreen, + backgroundColor = ElementGreen.copy(alpha = 0.4f) + ), + focusedBorderColor = ElementTheme.colors.primary, + unfocusedBorderColor = ElementTheme.colors.secondary, + disabledBorderColor = ElementTheme.colors.secondary.copy(alpha = 0.12f), + errorBorderColor = ElementTheme.colors.error, + focusedLeadingIconColor = ElementTheme.colors.primary, + unfocusedLeadingIconColor = ElementTheme.colors.secondary, + disabledLeadingIconColor = ElementTheme.colors.secondary.copy(0.12f), + errorLeadingIconColor = ElementTheme.colors.error, + focusedTrailingIconColor = ElementTheme.colors.primary, + unfocusedTrailingIconColor = ElementTheme.colors.secondary, + disabledTrailingIconColor = ElementTheme.colors.secondary.copy(alpha = 0.12f), + errorTrailingIconColor = ElementTheme.colors.error, + focusedLabelColor = ElementTheme.colors.primary, + unfocusedLabelColor = ElementTheme.colors.secondary, + disabledLabelColor = ElementTheme.colors.secondary.copy(alpha = 0.12f), + errorLabelColor = ElementTheme.colors.error, + placeholderColor = ElementTheme.colors.secondary, + disabledPlaceholderColor = ElementTheme.colors.secondary.copy(alpha = 0.12f), + focusedSupportingTextColor = ElementTheme.colors.primary, + unfocusedSupportingTextColor = ElementTheme.colors.secondary, + disabledSupportingTextColor = ElementTheme.colors.primary.copy(alpha = 0.12f), + errorSupportingTextColor = ElementTheme.colors.error, + ), ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementScaffold.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementScaffold.kt index a244bb3fa7..7e684e4104 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementScaffold.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementScaffold.kt @@ -20,12 +20,11 @@ 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.Scaffold import androidx.compose.material3.ScaffoldDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import io.element.android.libraries.designsystem.components.color.elementContentColorFor +import io.element.android.libraries.designsystem.theme.elementContentColorFor import io.element.android.libraries.designsystem.theme.ElementTheme @OptIn(ExperimentalMaterial3Api::class) @@ -42,7 +41,7 @@ fun ElementScaffold( contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets, content: @Composable (PaddingValues) -> Unit ) { - Scaffold( + androidx.compose.material3.Scaffold( modifier = modifier, topBar = topBar, bottomBar = bottomBar, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementSlider.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementSlider.kt index 4e2d31f5f4..b0eac3e9eb 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementSlider.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementSlider.kt @@ -18,7 +18,6 @@ package io.element.android.libraries.designsystem.theme.components import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Column -import androidx.compose.material3.Slider import androidx.compose.material3.SliderColors import androidx.compose.material3.SliderDefaults import androidx.compose.runtime.Composable @@ -53,7 +52,7 @@ fun ElementSlider( ), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } ) { - Slider( + androidx.compose.material3.Slider( value = value, onValueChange = onValueChange, modifier = modifier, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementSurface.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementSurface.kt index 0039eb0ee3..7aa61cad5e 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementSurface.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementSurface.kt @@ -17,7 +17,6 @@ package io.element.android.libraries.designsystem.theme.components import androidx.compose.foundation.BorderStroke -import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -25,7 +24,7 @@ import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import io.element.android.libraries.designsystem.components.color.elementContentColorFor +import io.element.android.libraries.designsystem.theme.elementContentColorFor import io.element.android.libraries.designsystem.theme.ElementTheme @Composable @@ -39,7 +38,7 @@ fun ElementSurface( border: BorderStroke? = null, content: @Composable () -> Unit ) { - Surface( + androidx.compose.material3.Surface( modifier = modifier, shape = shape, color = color, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementTopAppBar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementTopAppBar.kt index 7b7be7dfd1..188739c222 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementTopAppBar.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementTopAppBar.kt @@ -19,7 +19,6 @@ 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.TopAppBar import androidx.compose.material3.TopAppBarColors import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior @@ -44,7 +43,7 @@ fun ElementTopAppBar( ), scrollBehavior: TopAppBarScrollBehavior? = null ) { - TopAppBar( + androidx.compose.material3.TopAppBar( title = title, modifier = modifier, navigationIcon = navigationIcon, 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/theme/components/IconButton.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/IconButton.kt new file mode 100644 index 0000000000..5af819c1f0 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/IconButton.kt @@ -0,0 +1,41 @@ +/* + * 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.IconButtonDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier + +@Composable +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, + colors = IconButtonDefaults.iconButtonColors(), + interactionSource = interactionSource, + 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..6d7a255d4b --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Text.kt @@ -0,0 +1,113 @@ +/* + * 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.text.InlineTextContent +import androidx.compose.material3.LocalTextStyle +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.unit.TextUnit + +@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: Map = mapOf(), + 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, + ) +} 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..3a16b8fedb --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt @@ -0,0 +1,87 @@ +/* + * 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.material.ContentAlpha +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LocalTextStyle +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.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.VisualTransformation +import io.element.android.libraries.designsystem.theme.ElementTheme + +@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, +) { + 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 = TextFieldDefaults.textFieldColors( + textColor = ElementTheme.colors.onBackground, + containerColor = Color.Transparent, + cursorColor = ElementTheme.colors.onBackground.copy(alpha = ContentAlpha.medium), + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent + ), + ) +} 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 64aace9cdf..a8ce4af56f 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 @@ -23,7 +23,6 @@ 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.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -38,6 +37,7 @@ 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.ElementTheme +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 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 c416821152..5b6db5b7e6 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 @@ -23,7 +23,6 @@ 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.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.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.ElementTheme +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 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 4c8a4a0d9c..ab736f4177 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,6 @@ 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.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -36,6 +35,7 @@ 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.ElementTheme +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.ui.strings.R as StringR @Composable diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index 710f603cad..49cf6dd1cd 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -38,7 +38,6 @@ fun DependencyHandlerScope.composeDependencies() { androidTestImplementation(composeBom) implementation("androidx.compose.ui:ui") implementation("androidx.compose.material:material") - implementation("androidx.compose.material3:material3") implementation("androidx.compose.material:material-icons-extended") implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.activity:activity-compose:1.6.1") 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 45ef287018..844925b2f2 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,9 +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.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 @@ -34,6 +31,9 @@ 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.ElementButton +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( From 201508678af0e0d39dca30c2f179fa28a8272d74 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 30 Jan 2023 16:11:54 +0100 Subject: [PATCH 25/96] Rename component. No need for `Element` prefix --- .../android/x/component/ShowkaseButton.kt | 4 ++-- .../io/element/android/x/node/RootFlowNode.kt | 4 ++-- .../login/changeserver/ChangeServerView.kt | 12 +++++----- .../features/login/root/LoginRootScreen.kt | 18 +++++++-------- .../android/features/messages/MessagesView.kt | 8 +++---- .../messages/actionlist/ActionListView.kt | 4 ++-- .../messages/timeline/TimelineView.kt | 4 ++-- .../timeline/components/MessageEventBubble.kt | 4 ++-- .../components/TimelineItemReactionsView.kt | 4 ++-- .../timeline/components/html/HtmlDocument.kt | 4 ++-- .../features/onboarding/OnBoardingScreen.kt | 4 ++-- .../rageshake/bugreport/BugReportView.kt | 12 +++++----- .../android/features/roomlist/RoomListView.kt | 4 ++-- .../roomlist/components/RoomListTopBar.kt | 9 ++++---- .../components/LabelledCheckbox.kt | 4 ++-- .../designsystem/components/ProgressDialog.kt | 4 ++-- .../components/dialogs/ConfirmationDialog.kt | 8 +++---- .../components/dialogs/ErrorDialog.kt | 4 ++-- .../preferences/PreferenceScreen.kt | 8 +++---- .../components/preferences/PreferenceSlide.kt | 4 ++-- .../preferences/PreferenceSwitch.kt | 4 ++-- .../{ElementButton.kt => Button.kt} | 10 ++++----- .../{ElementCheckbox.kt => Checkbox.kt} | 14 ++++++------ ...icator.kt => CircularProgressIndicator.kt} | 4 ++-- .../designsystem/theme/components/Divider.kt | 19 +++++++++++++++- ...mentMediumAppBar.kt => MediumTopAppBar.kt} | 5 ++--- ...eetLayout.kt => ModalBottomSheetLayout.kt} | 7 +++--- ...linedTextField.kt => OutlinedTextField.kt} | 22 +++++++++---------- .../{ElementScaffold.kt => Scaffold.kt} | 4 ++-- .../{ElementSlider.kt => Slider.kt} | 10 ++++----- .../{ElementSurface.kt => Surface.kt} | 4 ++-- .../{ElementTopAppBar.kt => TopAppBar.kt} | 2 +- .../android/tests/uitests/ShowkaseButton.kt | 4 ++-- 33 files changed, 125 insertions(+), 111 deletions(-) rename libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/{ElementButton.kt => Button.kt} (91%) rename libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/{ElementCheckbox.kt => Checkbox.kt} (83%) rename libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/{ElementCircularProgressIndicator.kt => CircularProgressIndicator.kt} (95%) rename libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/{ElementMediumAppBar.kt => MediumTopAppBar.kt} (95%) rename libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/{ElementModalBottomSheetLayout.kt => ModalBottomSheetLayout.kt} (95%) rename libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/{ElementOutlinedTextField.kt => OutlinedTextField.kt} (82%) rename libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/{ElementScaffold.kt => Scaffold.kt} (99%) rename libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/{ElementSlider.kt => Slider.kt} (89%) rename libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/{ElementSurface.kt => Surface.kt} (98%) rename libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/{ElementTopAppBar.kt => TopAppBar.kt} (98%) 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 d72680b710..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 @@ -23,7 +23,7 @@ import androidx.compose.material.icons.filled.Close import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import io.element.android.libraries.designsystem.theme.components.ElementButton +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 @@ -36,7 +36,7 @@ internal fun ShowkaseButton( modifier: Modifier = Modifier, ) { if (isVisible) { - ElementButton( + Button( modifier = modifier .padding(top = 32.dp, start = 16.dp), onClick = onClick 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 22450a1382..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 @@ -35,7 +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.ElementCircularProgressIndicator +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 @@ -182,7 +182,7 @@ class RootFlowNode( private fun splashNode(buildContext: BuildContext) = node(buildContext) { Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) { - ElementCircularProgressIndicator() + CircularProgressIndicator() } } } 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 d924bba818..c64b3f0625 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 @@ -51,9 +51,9 @@ 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.ElementTheme -import io.element.android.libraries.designsystem.theme.components.ElementButton -import io.element.android.libraries.designsystem.theme.components.ElementCircularProgressIndicator -import io.element.android.libraries.designsystem.theme.components.ElementOutlinedTextField +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 @@ -122,7 +122,7 @@ fun ChangeServerView( color = ElementTheme.colors.secondary, ) var homeserverFieldState by textFieldState(stateValue = state.homeserver) - ElementOutlinedTextField( + OutlinedTextField( value = homeserverFieldState, modifier = Modifier .fillMaxWidth() @@ -155,7 +155,7 @@ fun ChangeServerView( modifier = Modifier.padding(start = 16.dp) ) } - ElementButton( + Button( onClick = { eventSink(ChangeServerEvents.Submit) }, enabled = state.submitEnabled, modifier = Modifier @@ -170,7 +170,7 @@ fun ChangeServerView( } } if (state.changeServerAction is Async.Loading) { - ElementCircularProgressIndicator( + CircularProgressIndicator( modifier = Modifier.align(Alignment.Center) ) } 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 6720826171..3242197ce2 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 @@ -52,11 +52,11 @@ 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.ElementTheme -import io.element.android.libraries.designsystem.theme.components.ElementButton -import io.element.android.libraries.designsystem.theme.components.ElementCircularProgressIndicator -import io.element.android.libraries.designsystem.theme.components.ElementOutlinedTextField +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 @@ -107,7 +107,7 @@ fun LoginRootScreen( Box( modifier = Modifier.fillMaxWidth() ) { - ElementOutlinedTextField( + OutlinedTextField( value = state.homeserver, modifier = Modifier.fillMaxWidth(), onValueChange = { /* no op */ }, @@ -119,7 +119,7 @@ fun LoginRootScreen( keyboardType = KeyboardType.Uri, ), ) - ElementButton( + Button( onClick = onChangeServer, modifier = Modifier .align(Alignment.CenterEnd) @@ -130,7 +130,7 @@ fun LoginRootScreen( } ) } - ElementOutlinedTextField( + OutlinedTextField( value = loginFieldState, modifier = Modifier .fillMaxWidth() @@ -153,7 +153,7 @@ fun LoginRootScreen( // Ensure password is hidden when user submits the form passwordVisible = false } - ElementOutlinedTextField( + OutlinedTextField( value = passwordFieldState, modifier = Modifier .fillMaxWidth() @@ -196,7 +196,7 @@ fun LoginRootScreen( } } // Submit - ElementButton( + Button( onClick = { eventSink(LoginRootEvents.Submit) }, enabled = state.submitEnabled, modifier = Modifier @@ -212,7 +212,7 @@ fun LoginRootScreen( } } if (state.loggedInState is LoggedInState.LoggingIn) { - ElementCircularProgressIndicator( + CircularProgressIndicator( modifier = Modifier.align(Alignment.Center) ) } 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 466c24ebe5..e19fc869fa 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 @@ -59,11 +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.ElementScaffold -import io.element.android.libraries.designsystem.theme.components.ElementTopAppBar 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 @@ -101,7 +101,7 @@ fun MessagesView( state.eventSink(MessagesEvents.HandleAction(action, messageEvent)) } - ElementScaffold( + Scaffold( modifier = modifier, contentWindowInsets = WindowInsets.statusBars, topBar = { @@ -172,7 +172,7 @@ fun MessagesViewTopBar( modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, ) { - ElementTopAppBar( + TopAppBar( modifier = modifier, navigationIcon = { IconButton(onClick = onBackPressed) { 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 4adafbea3c..e580b48001 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 @@ -43,7 +43,7 @@ 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.ElementTheme -import io.element.android.libraries.designsystem.theme.components.ElementModalBottomSheetLayout +import io.element.android.libraries.designsystem.theme.components.ModalBottomSheetLayout import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch @@ -73,7 +73,7 @@ fun ActionListView( } } - ElementModalBottomSheetLayout( + ModalBottomSheetLayout( modifier = modifier, sheetState = modalBottomSheetState, sheetContent = { 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 609ea8acaa..13127271c2 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 @@ -77,7 +77,7 @@ 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.ElementTheme -import io.element.android.libraries.designsystem.theme.components.ElementCircularProgressIndicator +import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator 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 @@ -345,7 +345,7 @@ internal fun TimelineLoadingMoreIndicator() { .padding(8.dp), contentAlignment = Alignment.Center, ) { - ElementCircularProgressIndicator( + CircularProgressIndicator( strokeWidth = 2.dp, color = ElementTheme.colors.primary ) 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 dc12908f5f..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 @@ -30,7 +30,7 @@ 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.theme.ElementTheme -import io.element.android.libraries.designsystem.theme.components.ElementSurface +import io.element.android.libraries.designsystem.theme.components.Surface private val BUBBLE_RADIUS = 16.dp @@ -91,7 +91,7 @@ fun MessageEventBubble( } } val bubbleShape = bubbleShape() - ElementSurface( + Surface( modifier = modifier .widthIn(min = 80.dp) .offsetForItem() 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 3779968afa..22a9b306f1 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 @@ -32,7 +32,7 @@ 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.ElementTheme -import io.element.android.libraries.designsystem.theme.components.ElementSurface +import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.designsystem.theme.components.Text @Composable @@ -54,7 +54,7 @@ fun TimelineItemReactionsView( @Composable fun MessagesReactionButton(reaction: AggregatedReaction, modifier: Modifier = Modifier) { - ElementSurface( + Surface( modifier = modifier, color = ElementTheme.colors.surfaceVariant, border = BorderStroke(2.dp, ElementTheme.colors.background), 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 4c6b38dcbf..a3be7d3bd1 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 @@ -47,7 +47,7 @@ import io.element.android.libraries.designsystem.LinkColor import io.element.android.libraries.designsystem.components.ClickableLinkText import io.element.android.libraries.designsystem.theme.ElementColors import io.element.android.libraries.designsystem.theme.ElementTheme -import io.element.android.libraries.designsystem.theme.components.ElementSurface +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 @@ -340,7 +340,7 @@ private fun HtmlMxReply( ) { val blockquote = mxReply.childNodes().firstOrNull() ?: return val shape = RoundedCornerShape(12.dp) - ElementSurface( + Surface( modifier = modifier .padding(bottom = 4.dp) .offset(x = -(8.dp)), 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 e37bf1333e..dc8362563e 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 @@ -43,7 +43,7 @@ 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.theme.components.ElementButton +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 @@ -100,7 +100,7 @@ fun OnBoardingScreen( .align(CenterHorizontally) .padding(16.dp), ) - ElementButton( + Button( onClick = { onSignIn() }, 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 4dc3a858a1..0cd9ff0334 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 @@ -50,9 +50,9 @@ 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.ElementTheme -import io.element.android.libraries.designsystem.theme.components.ElementButton -import io.element.android.libraries.designsystem.theme.components.ElementCircularProgressIndicator -import io.element.android.libraries.designsystem.theme.components.ElementOutlinedTextField +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 @@ -114,7 +114,7 @@ fun BugReportView( Column( // modifier = Modifier.weight(1f), ) { - ElementOutlinedTextField( + OutlinedTextField( value = descriptionFieldState, modifier = Modifier .fillMaxWidth() @@ -182,7 +182,7 @@ fun BugReportView( } } // Submit - ElementButton( + Button( onClick = { eventSink(BugReportEvents.SendBugReport) }, enabled = state.submitEnabled, modifier = Modifier @@ -194,7 +194,7 @@ fun BugReportView( } when (state.sending) { is Async.Loading -> { - ElementCircularProgressIndicator( + CircularProgressIndicator( progress = state.sendingProgress, modifier = Modifier.align(Alignment.Center) ) 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 14c737a848..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 @@ -42,7 +42,7 @@ 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.ElementScaffold +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 @@ -118,7 +118,7 @@ fun RoomListView( } } - ElementScaffold( + Scaffold( modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { RoomListTopBar( 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 1b6b7fd13c..70b4d974c7 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 @@ -38,7 +38,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 @@ -47,12 +46,12 @@ 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.ElementTheme -import io.element.android.libraries.designsystem.theme.components.ElementMediumAppBar -import io.element.android.libraries.designsystem.theme.components.ElementTopAppBar 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 @@ -112,7 +111,7 @@ fun SearchRoomListTopBar( ) { var filterState by textFieldState(stateValue = text) val focusRequester = remember { FocusRequester() } - ElementTopAppBar( + TopAppBar( modifier = modifier .nestedScroll(scrollBehavior.nestedScrollConnection), title = { @@ -179,7 +178,7 @@ private fun DefaultRoomListTopBar( scrollBehavior: TopAppBarScrollBehavior, modifier: Modifier = Modifier, ) { - ElementMediumAppBar( + MediumTopAppBar( modifier = modifier .nestedScroll(scrollBehavior.nestedScrollConnection), title = { 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 af3596127b..cd381af63b 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 @@ -25,7 +25,7 @@ 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.ElementTheme -import io.element.android.libraries.designsystem.theme.components.ElementCheckbox +import io.element.android.libraries.designsystem.theme.components.Checkbox import io.element.android.libraries.designsystem.theme.components.Text @Composable @@ -40,7 +40,7 @@ fun LabelledCheckbox( modifier = modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { - ElementCheckbox( + Checkbox( checked = checked, onCheckedChange = onCheckedChange, enabled = enabled, 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 e48b41937b..6f8ec96118 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 @@ -32,7 +32,7 @@ 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.ElementTheme -import io.element.android.libraries.designsystem.theme.components.ElementCircularProgressIndicator +import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.Text @Composable @@ -55,7 +55,7 @@ fun ProgressDialog( ) ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { - ElementCircularProgressIndicator( + CircularProgressIndicator( modifier = Modifier.padding(16.dp), color = ElementTheme.colors.onSurfaceVariant ) 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 903f9a07ba..b4b910ac3b 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 @@ -34,7 +34,7 @@ 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.ElementTheme -import io.element.android.libraries.designsystem.theme.components.ElementButton +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 @@ -72,7 +72,7 @@ fun ConfirmationDialog( horizontalArrangement = Arrangement.Center ) { Column { - ElementButton( + Button( modifier = Modifier.fillMaxWidth(), onClick = { onCancelClicked() @@ -80,7 +80,7 @@ fun ConfirmationDialog( Text(cancelText) } if (thirdButtonText != null) { - ElementButton( + Button( modifier = Modifier.fillMaxWidth(), onClick = { onThirdButtonClicked() @@ -96,7 +96,7 @@ fun ConfirmationDialog( modifier = Modifier.padding(all = 8.dp), horizontalArrangement = Arrangement.Center ) { - ElementButton( + Button( modifier = Modifier.fillMaxWidth(), onClick = { onSubmitClicked() 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 feba85cc73..b9740c2074 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 @@ -33,7 +33,7 @@ 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.ElementTheme -import io.element.android.libraries.designsystem.theme.components.ElementButton +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 @@ -65,7 +65,7 @@ fun ErrorDialog( modifier = Modifier.padding(all = 8.dp), horizontalArrangement = Arrangement.Center ) { - ElementButton( + Button( modifier = Modifier.fillMaxWidth(), onClick = { onDismiss() 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 caac7b93c2..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 @@ -41,11 +41,11 @@ 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.ElementScaffold -import io.element.android.libraries.designsystem.theme.components.ElementTopAppBar 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 @@ -55,7 +55,7 @@ fun PreferenceView( onBackPressed: () -> Unit = {}, content: @Composable ColumnScope.() -> Unit, ) { - ElementScaffold( + Scaffold( modifier = modifier .fillMaxSize() .systemBarsPadding() @@ -89,7 +89,7 @@ fun PreferenceTopAppBar( modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, ) { - ElementTopAppBar( + TopAppBar( modifier = modifier, navigationIcon = { IconButton(onClick = onBackPressed) { 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 6335929641..315f7caeab 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 @@ -32,7 +32,7 @@ import io.element.android.libraries.designsystem.components.preferences.componen import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.ElementTheme -import io.element.android.libraries.designsystem.theme.components.ElementSlider +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 @@ -75,7 +75,7 @@ fun PreferenceSlide( text = summary ) } - ElementSlider( + Slider( value = value, steps = steps, onValueChange = onValueChange, 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 173d427ff1..629148c071 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 @@ -33,7 +33,7 @@ import io.element.android.libraries.designsystem.components.preferences.componen import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.ElementTheme -import io.element.android.libraries.designsystem.theme.components.ElementCheckbox +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 @@ -67,7 +67,7 @@ fun PreferenceSwitch( color = enabled.toEnabledColor(), text = title ) - ElementCheckbox( + Checkbox( modifier = Modifier.padding(end = preferencePaddingEnd), checked = isChecked, enabled = enabled, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementButton.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt similarity index 91% rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementButton.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt index e66571b2e7..cb49ebdcd7 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementButton.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt @@ -32,7 +32,7 @@ import io.element.android.libraries.designsystem.theme.ElementTheme import io.element.android.libraries.designsystem.theme.elementContentColorFor @Composable -fun ElementButton( +fun Button( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, @@ -70,19 +70,19 @@ fun ElementButton( @Preview @Composable -fun ElementButtonsLightPreview() = ElementPreviewLight { ContentToPreview() } +fun ButtonsLightPreview() = ElementPreviewLight { ContentToPreview() } @Preview @Composable -fun ElementButtonsDarkPreview() = ElementPreviewDark { ContentToPreview() } +fun ButtonsDarkPreview() = ElementPreviewDark { ContentToPreview() } @Composable private fun ContentToPreview() { Column { - ElementButton(onClick = {}, enabled = true) { + Button(onClick = {}, enabled = true) { Text(text = "Click me! - Enabled") } - ElementButton(onClick = {}, enabled = false) { + Button(onClick = {}, enabled = false) { Text(text = "Click me! - Disabled") } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementCheckbox.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Checkbox.kt similarity index 83% rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementCheckbox.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Checkbox.kt index 6db3f62c8d..25b1637ac8 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementCheckbox.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Checkbox.kt @@ -30,7 +30,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.ElementTheme @Composable -fun ElementCheckbox( +fun Checkbox( checked: Boolean, onCheckedChange: ((Boolean) -> Unit)?, modifier: Modifier = Modifier, @@ -59,18 +59,18 @@ fun ElementCheckbox( @Preview @Composable -fun ElementCheckboxesLightPreview() = ElementPreviewLight { ContentToPreview() } +fun CheckboxesLightPreview() = ElementPreviewLight { ContentToPreview() } @Preview @Composable -fun ElementCheckboxesDarkPreview() = ElementPreviewDark { ContentToPreview() } +fun CheckboxesDarkPreview() = ElementPreviewDark { ContentToPreview() } @Composable private fun ContentToPreview() { Column { - ElementCheckbox(onCheckedChange = {}, enabled = true, checked = true) - ElementCheckbox(onCheckedChange = {}, enabled = true, checked = false) - ElementCheckbox(onCheckedChange = {}, enabled = false, checked = true) - ElementCheckbox(onCheckedChange = {}, enabled = false, checked = false) + 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/ElementCircularProgressIndicator.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/CircularProgressIndicator.kt similarity index 95% rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementCircularProgressIndicator.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/CircularProgressIndicator.kt index 256745dbbf..38eb970828 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementCircularProgressIndicator.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/CircularProgressIndicator.kt @@ -24,7 +24,7 @@ import androidx.compose.ui.unit.Dp import io.element.android.libraries.designsystem.theme.ElementTheme @Composable -fun ElementCircularProgressIndicator( +fun CircularProgressIndicator( progress: Float, modifier: Modifier = Modifier, color: Color = ProgressIndicatorDefaults.circularColor, @@ -39,7 +39,7 @@ fun ElementCircularProgressIndicator( } @Composable -fun ElementCircularProgressIndicator( +fun CircularProgressIndicator( modifier: Modifier = Modifier, color: Color = ElementTheme.colors.primary, strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth, 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 index 4889c4c473..832f3b89d4 100644 --- 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 @@ -20,13 +20,17 @@ 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 +import io.element.android.libraries.designsystem.theme.ElementTheme @Composable fun Divider( modifier: Modifier = Modifier, thickness: Dp = DividerDefaults.Thickness, - color: Color = DividerDefaults.color, + color: Color = ElementTheme.colors.onBackground, ) { androidx.compose.material3.Divider( modifier = modifier, @@ -34,3 +38,16 @@ fun Divider( 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/ElementMediumAppBar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/MediumTopAppBar.kt similarity index 95% rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementMediumAppBar.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/MediumTopAppBar.kt index 0d3a0a134d..579583757f 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementMediumAppBar.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/MediumTopAppBar.kt @@ -19,7 +19,6 @@ 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.MediumTopAppBar import androidx.compose.material3.TopAppBarColors import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior @@ -29,7 +28,7 @@ import io.element.android.libraries.designsystem.theme.ElementTheme @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ElementMediumAppBar( +fun MediumTopAppBar( title: @Composable () -> Unit, modifier: Modifier = Modifier, navigationIcon: @Composable () -> Unit = {}, @@ -44,7 +43,7 @@ fun ElementMediumAppBar( ), scrollBehavior: TopAppBarScrollBehavior? = null ) { - MediumTopAppBar( + androidx.compose.material3.MediumTopAppBar( title = title, modifier = modifier, navigationIcon = navigationIcon, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementModalBottomSheetLayout.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheetLayout.kt similarity index 95% rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementModalBottomSheetLayout.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheetLayout.kt index c1a5ddc2e7..d3ca206832 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementModalBottomSheetLayout.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheetLayout.kt @@ -20,7 +20,6 @@ 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.ModalBottomSheetLayout import androidx.compose.material.ModalBottomSheetState import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.rememberModalBottomSheetState @@ -29,12 +28,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.Dp -import io.element.android.libraries.designsystem.theme.elementContentColorFor import io.element.android.libraries.designsystem.theme.ElementTheme +import io.element.android.libraries.designsystem.theme.elementContentColorFor @OptIn(ExperimentalMaterialApi::class) @Composable -fun ElementModalBottomSheetLayout( +fun ModalBottomSheetLayout( sheetContent: @Composable ColumnScope.() -> Unit, modifier: Modifier = Modifier, sheetState: ModalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden), @@ -45,7 +44,7 @@ fun ElementModalBottomSheetLayout( scrimColor: Color = ElementTheme.colors.onSurfaceVariant.copy(alpha = 0.32f), content: @Composable () -> Unit = {} ) { - ModalBottomSheetLayout( + androidx.compose.material.ModalBottomSheetLayout( sheetContent = sheetContent, modifier = modifier, sheetState = sheetState, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementOutlinedTextField.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/OutlinedTextField.kt similarity index 82% rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementOutlinedTextField.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/OutlinedTextField.kt index 5e57b85507..fe178bb28d 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementOutlinedTextField.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/OutlinedTextField.kt @@ -39,7 +39,7 @@ import io.element.android.libraries.designsystem.theme.ElementTheme @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ElementOutlinedTextField( +fun OutlinedTextField( value: String, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, @@ -118,22 +118,22 @@ fun ElementOutlinedTextField( @Preview @Composable -fun ElementOutlinedTextFieldsLightPreview() = ElementPreviewLight { ContentToPreview() } +fun OutlinedTextFieldsLightPreview() = ElementPreviewLight { ContentToPreview() } @Preview @Composable -fun ElementOutlinedTextFieldsDarkPreview() = ElementPreviewDark { ContentToPreview() } +fun OutlinedTextFieldsDarkPreview() = ElementPreviewDark { ContentToPreview() } @Composable private fun ContentToPreview() { Column { - ElementOutlinedTextField(onValueChange = {}, value = "Content", isError = false, enabled = true, readOnly = true) - ElementOutlinedTextField(onValueChange = {}, value = "Content", isError = false, enabled = true, readOnly = false) - ElementOutlinedTextField(onValueChange = {}, value = "Content", isError = false, enabled = false, readOnly = true) - ElementOutlinedTextField(onValueChange = {}, value = "Content", isError = false, enabled = false, readOnly = false) - ElementOutlinedTextField(onValueChange = {}, value = "Content", isError = true, enabled = true, readOnly = true) - ElementOutlinedTextField(onValueChange = {}, value = "Content", isError = true, enabled = true, readOnly = false) - ElementOutlinedTextField(onValueChange = {}, value = "Content", isError = true, enabled = false, readOnly = true) - ElementOutlinedTextField(onValueChange = {}, value = "Content", isError = true, enabled = false, readOnly = false) + 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/ElementScaffold.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Scaffold.kt similarity index 99% rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementScaffold.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Scaffold.kt index 7e684e4104..7a1ede6d70 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementScaffold.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Scaffold.kt @@ -24,12 +24,12 @@ import androidx.compose.material3.ScaffoldDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import io.element.android.libraries.designsystem.theme.elementContentColorFor import io.element.android.libraries.designsystem.theme.ElementTheme +import io.element.android.libraries.designsystem.theme.elementContentColorFor @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ElementScaffold( +fun Scaffold( modifier: Modifier = Modifier, topBar: @Composable () -> Unit = {}, bottomBar: @Composable () -> Unit = {}, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementSlider.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Slider.kt similarity index 89% rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementSlider.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Slider.kt index b0eac3e9eb..87e1757dd7 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementSlider.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Slider.kt @@ -29,7 +29,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.ElementTheme @Composable -fun ElementSlider( +fun Slider( value: Float, onValueChange: (Float) -> Unit, modifier: Modifier = Modifier, @@ -67,16 +67,16 @@ fun ElementSlider( @Preview @Composable -fun ElementSlidersLightPreview() = ElementPreviewLight { ContentToPreview() } +fun SlidersLightPreview() = ElementPreviewLight { ContentToPreview() } @Preview @Composable -fun ElementSlidersDarkPreview() = ElementPreviewDark { ContentToPreview() } +fun SlidersDarkPreview() = ElementPreviewDark { ContentToPreview() } @Composable private fun ContentToPreview() { Column { - ElementSlider(onValueChange = {}, value = 0.33f, enabled = true) - ElementSlider(onValueChange = {}, value = 0.33f, enabled = false) + 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/ElementSurface.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Surface.kt similarity index 98% rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementSurface.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Surface.kt index 7aa61cad5e..0689680972 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementSurface.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Surface.kt @@ -24,11 +24,11 @@ import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import io.element.android.libraries.designsystem.theme.elementContentColorFor import io.element.android.libraries.designsystem.theme.ElementTheme +import io.element.android.libraries.designsystem.theme.elementContentColorFor @Composable -fun ElementSurface( +fun Surface( modifier: Modifier = Modifier, shape: Shape = RectangleShape, color: Color = ElementTheme.colors.surfaceVariant, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementTopAppBar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TopAppBar.kt similarity index 98% rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementTopAppBar.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TopAppBar.kt index 188739c222..3930e2e3c1 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ElementTopAppBar.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TopAppBar.kt @@ -28,7 +28,7 @@ import io.element.android.libraries.designsystem.theme.ElementTheme @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ElementTopAppBar( +fun TopAppBar( title: @Composable () -> Unit, modifier: Modifier = Modifier, navigationIcon: @Composable () -> Unit = {}, 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 844925b2f2..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 @@ -30,7 +30,7 @@ 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.ElementButton +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 @@ -43,7 +43,7 @@ fun ShowkaseButton( var isShowkaseButtonVisible by remember { mutableStateOf(BuildConfig.DEBUG) } if (isShowkaseButtonVisible) { - ElementButton( + Button( modifier = modifier .padding(top = 32.dp), onClick = onClick From 47186c6af1d5ebdf0494b5cf69c2de81c8391c6a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Feb 2023 17:33:48 +0100 Subject: [PATCH 26/96] Fix compilation issue --- .../features/messages/actionlist/model/TimelineItemAction.kt | 1 + 1 file changed, 1 insertion(+) 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 88e931408c..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( From bbe10382b0b38dd450b8c9904cf163b7b2eb2c7f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 3 Feb 2023 10:15:30 +0100 Subject: [PATCH 27/96] ElementTheme + MaterialTheme3 --- .../login/changeserver/ChangeServerView.kt | 12 ++-- .../features/login/root/LoginRootScreen.kt | 7 ++- features/messages/build.gradle.kts | 3 - .../messages/actionlist/ActionListView.kt | 6 +- .../messages/timeline/TimelineView.kt | 9 +-- .../components/TimelineItemImageView.kt | 7 +-- .../components/TimelineItemInformativeView.kt | 6 +- .../components/TimelineItemReactionsView.kt | 8 +-- .../timeline/components/html/HtmlDocument.kt | 38 ++++++------ .../rageshake/bugreport/BugReportView.kt | 6 +- features/roomlist/build.gradle.kts | 3 - .../roomlist/components/RoomListTopBar.kt | 8 +-- .../roomlist/components/RoomSummaryRow.kt | 17 +++--- .../android/features/template/TemplateView.kt | 5 +- libraries/designsystem/build.gradle.kts | 3 - .../libraries/designsystem/ColorUtil.kt | 6 +- .../components/ClickableLinkText.kt | 4 +- .../components/LabelledCheckbox.kt | 4 +- .../designsystem/components/ProgressDialog.kt | 8 +-- .../components/dialogs/ConfirmationDialog.kt | 10 ++-- .../components/dialogs/ErrorDialog.kt | 10 ++-- .../preferences/PreferenceCategory.kt | 6 +- .../components/preferences/PreferenceSlide.kt | 6 +- .../preferences/PreferenceSwitch.kt | 3 +- .../components/preferences/PreferenceText.kt | 6 +- .../designsystem/preview/ElementPreview.kt | 3 +- .../designsystem/theme/ColorAliases.kt | 21 +++++-- .../designsystem/theme/ColorsDark.kt | 15 +---- .../designsystem/theme/ColorsLight.kt | 17 +----- .../designsystem/theme/ContentColor.kt | 40 ------------- .../designsystem/theme/ElementColors.kt | 60 ------------------- .../designsystem/theme/ElementSpaces.kt | 27 --------- .../designsystem/theme/ElementTheme.kt | 47 +++++++-------- .../designsystem/theme/ElementTypography.kt | 15 +---- .../designsystem/theme/components/Button.kt | 49 +++++++-------- .../designsystem/theme/components/Checkbox.kt | 13 +--- .../components/CircularProgressIndicator.kt | 3 +- .../designsystem/theme/components/Divider.kt | 3 +- .../theme/components/MediumTopAppBar.kt | 9 +-- .../components/ModalBottomSheetLayout.kt | 9 ++- .../theme/components/OutlinedTextField.kt | 37 +----------- .../designsystem/theme/components/Scaffold.kt | 8 +-- .../designsystem/theme/components/Slider.kt | 15 +---- .../designsystem/theme/components/Surface.kt | 8 +-- .../theme/components/TextField.kt | 12 +--- .../theme/components/TopAppBar.kt | 9 +-- .../matrix/ui/components/MatrixUserHeader.kt | 6 +- .../matrix/ui/components/MatrixUserRow.kt | 6 +- .../libraries/textcomposer/TextComposer.kt | 4 +- .../kotlin/extension/DependencyHandleScope.kt | 1 + .../android/tests/uitests/ScreenshotTest.kt | 6 +- .../tests/uitests/TypographyTestPreview.kt | 4 +- 52 files changed, 202 insertions(+), 446 deletions(-) delete mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ContentColor.kt delete mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementSpaces.kt 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 c64b3f0625..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 @@ -31,6 +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.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue @@ -50,7 +51,6 @@ 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.ElementTheme 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 @@ -86,7 +86,7 @@ fun ChangeServerView( .size(width = 81.dp, height = 73.dp) .align(Alignment.CenterHorizontally) .background( - color = ElementTheme.colors.surfaceVariant, + color = MaterialTheme.colorScheme.surfaceVariant, shape = RoundedCornerShape(32.dp) ) ) { @@ -108,7 +108,7 @@ fun ChangeServerView( textAlign = TextAlign.Center, fontWeight = FontWeight.Bold, fontSize = 24.sp, - color = ElementTheme.colors.primary, + color = MaterialTheme.colorScheme.primary, ) Text( text = "A server is a home for all your data.\n" + @@ -119,7 +119,7 @@ fun ChangeServerView( .padding(top = 16.dp), textAlign = TextAlign.Center, fontSize = 16.sp, - color = ElementTheme.colors.secondary, + color = MaterialTheme.colorScheme.secondary, ) var homeserverFieldState by textFieldState(stateValue = state.homeserver) OutlinedTextField( @@ -150,8 +150,8 @@ fun ChangeServerView( state.homeserver, state.changeServerAction.error ), - color = ElementTheme.colors.error, - style = ElementTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, modifier = Modifier.padding(start = 16.dp) ) } 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 3242197ce2..2d5381f99c 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 @@ -30,6 +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.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -98,7 +99,7 @@ fun LoginRootScreen( textAlign = TextAlign.Center, fontWeight = FontWeight.Bold, fontSize = 24.sp, - color = ElementTheme.colors.primary, + color = MaterialTheme.colorScheme.primary, ) // Form Column( @@ -189,8 +190,8 @@ fun LoginRootScreen( if (state.loggedInState is LoggedInState.ErrorLoggingIn) { Text( text = loginError(state.formState, state.loggedInState.failure), - color = ElementTheme.colors.error, - style = ElementTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, modifier = Modifier.padding(start = 16.dp) ) } diff --git a/features/messages/build.gradle.kts b/features/messages/build.gradle.kts index 5931450294..975c7b5c0e 100644 --- a/features/messages/build.gradle.kts +++ b/features/messages/build.gradle.kts @@ -47,7 +47,4 @@ dependencies { testImplementation(libs.test.junit) androidTestImplementation(libs.test.junitext) ksp(libs.showkase.processor) - - // Not for components, but other classes. - implementation("androidx.compose.material3:material3") } 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 e580b48001..f2fe33f1c7 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 @@ -32,6 +32,7 @@ import androidx.compose.material.LocalContentColor 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 @@ -42,7 +43,6 @@ 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.ElementTheme import io.element.android.libraries.designsystem.theme.components.ModalBottomSheetLayout import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch @@ -115,13 +115,13 @@ private fun SheetContent( text = { Text( text = action.title, - color = if (action.destructive) ElementTheme.colors.error else Color.Unspecified, + color = if (action.destructive) MaterialTheme.colorScheme.error else Color.Unspecified, ) }, icon = { VectorIcon( resourceId = action.icon, - tint = if (action.destructive) ElementTheme.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/timeline/TimelineView.kt b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/TimelineView.kt index 13127271c2..91b9cc6023 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,6 +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.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf @@ -269,7 +270,7 @@ private fun MessageSenderInformation( } Text( text = sender, - style = ElementTheme.typography.titleMedium, + style = MaterialTheme.typography.titleMedium, modifier = Modifier .alignBy(LastBaseline) ) @@ -328,8 +329,8 @@ internal fun BoxScope.TimelineScrollHelper( modifier = Modifier .align(Alignment.BottomCenter) .size(40.dp), - containerColor = ElementTheme.colors.surfaceVariant, - contentColor = ElementTheme.colors.onSurfaceVariant + containerColor = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant ) { Icon(Icons.Default.ArrowDownward, "") } @@ -347,7 +348,7 @@ internal fun TimelineLoadingMoreIndicator() { ) { CircularProgressIndicator( strokeWidth = 2.dp, - color = ElementTheme.colors.primary + color = MaterialTheme.colorScheme.primary ) } } 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 cce7e21896..e497a52523 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,14 +14,12 @@ * 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 +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable @@ -33,7 +31,6 @@ import androidx.compose.ui.platform.LocalContext import coil.compose.AsyncImage import coil.request.ImageRequest import io.element.android.features.messages.timeline.model.content.TimelineItemImageContent -import io.element.android.libraries.designsystem.theme.ElementTheme @Composable fun TimelineItemImageView( @@ -60,7 +57,7 @@ fun TimelineItemImageView( AsyncImage( model = model, contentDescription = null, - placeholder = ColorPainter(ElementTheme.colors.surfaceVariant), + placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant), contentScale = ContentScale.Crop, onSuccess = { isLoading.value = false }, ) 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 5ad33a72ad..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 @@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -32,7 +33,6 @@ 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.ElementTheme import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text @@ -49,14 +49,14 @@ fun TimelineItemInformativeView( ) { Icon( imageVector = icon, - tint = ElementTheme.colors.secondary, + tint = MaterialTheme.colorScheme.secondary, contentDescription = iconDescription, modifier = Modifier.size(16.dp) ) Spacer(modifier = Modifier.width(4.dp)) Text( fontStyle = FontStyle.Italic, - color = ElementTheme.colors.secondary, + color = MaterialTheme.colorScheme.secondary, fontSize = 14.sp, text = text ) 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 22a9b306f1..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 @@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.padding 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.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -31,7 +32,6 @@ 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.ElementTheme import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.designsystem.theme.components.Text @@ -56,8 +56,8 @@ fun TimelineItemReactionsView( fun MessagesReactionButton(reaction: AggregatedReaction, modifier: Modifier = Modifier) { Surface( modifier = modifier, - color = ElementTheme.colors.surfaceVariant, - border = BorderStroke(2.dp, ElementTheme.colors.background), + color = MaterialTheme.colorScheme.surfaceVariant, + border = BorderStroke(2.dp, MaterialTheme.colorScheme.background), shape = RoundedCornerShape(corner = CornerSize(12.dp)), ) { Row( @@ -66,7 +66,7 @@ fun MessagesReactionButton(reaction: AggregatedReaction, modifier: Modifier = Mo ) { Text(text = reaction.key, fontSize = 12.sp) Spacer(modifier = Modifier.width(4.dp)) - Text(text = reaction.count, color = ElementTheme.colors.secondary, fontSize = 12.sp) + Text(text = reaction.count, color = MaterialTheme.colorScheme.secondary, fontSize = 12.sp) } } } 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 a3be7d3bd1..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 @@ -25,7 +25,9 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.appendInlineContent +import androidx.compose.material3.ColorScheme import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind @@ -45,8 +47,6 @@ 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.ElementColors -import io.element.android.libraries.designsystem.theme.ElementTheme 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 @@ -214,7 +214,7 @@ private fun HtmlInline( ) { Box(modifier) { val styledText = buildAnnotatedString { - appendInlineElement(element, ElementTheme.colors) + appendInlineElement(element, MaterialTheme.colorScheme) } HtmlText( text = styledText, @@ -232,7 +232,7 @@ private fun HtmlPreformatted( ) { val isCode = pre.firstElementChild()?.normalName() == "code" val backgroundColor = - if (isCode) ElementTheme.colors.codeBackground() else Color.Unspecified + if (isCode) MaterialTheme.colorScheme.codeBackground() else Color.Unspecified Box( modifier .background(color = backgroundColor) @@ -255,7 +255,7 @@ private fun HtmlParagraph( ) { Box(modifier) { val styledText = buildAnnotatedString { - appendInlineChildrenElements(paragraph.childNodes(), ElementTheme.colors) + appendInlineChildrenElements(paragraph.childNodes(), MaterialTheme.colorScheme) } HtmlText( text = styledText, onClick = onTextClicked, @@ -272,7 +272,7 @@ private fun HtmlBlockquote( onTextClicked: () -> Unit = {}, onTextLongClicked: () -> Unit = {}, ) { - val color = ElementTheme.colors.onBackground + val color = MaterialTheme.colorScheme.onBackground Box( modifier = modifier .drawBehind { @@ -287,7 +287,7 @@ private fun HtmlBlockquote( ) { val text = buildAnnotatedString { withStyle(style = SpanStyle(fontStyle = FontStyle.Italic)) { - appendInlineChildrenElements(blockquote.childNodes(), ElementTheme.colors) + appendInlineChildrenElements(blockquote.childNodes(), MaterialTheme.colorScheme) } } HtmlText( @@ -306,19 +306,19 @@ private fun HtmlHeading( onTextLongClicked: () -> Unit = {}, ) { val style = when (heading.normalName()) { - "h1" -> ElementTheme.typography.headlineLarge.copy(fontSize = 30.sp) - "h2" -> ElementTheme.typography.headlineLarge.copy(fontSize = 26.sp) - "h3" -> ElementTheme.typography.headlineMedium.copy(fontSize = 22.sp) - "h4" -> ElementTheme.typography.headlineMedium.copy(fontSize = 18.sp) - "h5" -> ElementTheme.typography.headlineSmall.copy(fontSize = 14.sp) - "h6" -> ElementTheme.typography.headlineSmall.copy(fontSize = 12.sp) + "h1" -> MaterialTheme.typography.headlineLarge.copy(fontSize = 30.sp) + "h2" -> MaterialTheme.typography.headlineLarge.copy(fontSize = 26.sp) + "h3" -> MaterialTheme.typography.headlineMedium.copy(fontSize = 22.sp) + "h4" -> MaterialTheme.typography.headlineMedium.copy(fontSize = 18.sp) + "h5" -> MaterialTheme.typography.headlineSmall.copy(fontSize = 14.sp) + "h6" -> MaterialTheme.typography.headlineSmall.copy(fontSize = 12.sp) else -> { return } } Box(modifier) { val text = buildAnnotatedString { - appendInlineChildrenElements(heading.childNodes(), ElementTheme.colors) + appendInlineChildrenElements(heading.childNodes(), MaterialTheme.colorScheme) } HtmlText( text = text, @@ -344,7 +344,7 @@ private fun HtmlMxReply( modifier = modifier .padding(bottom = 4.dp) .offset(x = -(8.dp)), - color = ElementTheme.colors.background, + color = MaterialTheme.colorScheme.background, shape = shape, ) { val text = buildAnnotatedString { @@ -354,7 +354,7 @@ private fun HtmlMxReply( withStyle( style = SpanStyle( fontSize = 12.sp, - color = ElementTheme.colors.secondary + color = MaterialTheme.colorScheme.secondary ) ) { append(blockquoteNode.text()) @@ -462,13 +462,13 @@ private fun HtmlListItems( } } -private fun ElementColors.codeBackground(): Color { +private fun ColorScheme.codeBackground(): Color { return background.copy(alpha = 0.3f) } private fun AnnotatedString.Builder.appendInlineChildrenElements( childNodes: List, - colors: ElementColors + colors: ColorScheme ) { for (node in childNodes) { when (node) { @@ -482,7 +482,7 @@ private fun AnnotatedString.Builder.appendInlineChildrenElements( } } -private fun AnnotatedString.Builder.appendInlineElement(element: Element, colors: ElementColors) { +private fun AnnotatedString.Builder.appendInlineElement(element: Element, colors: ColorScheme) { when (element.normalName()) { "br" -> { append('\n') 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 0cd9ff0334..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,6 +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.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -49,7 +50,6 @@ 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.ElementTheme 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 @@ -97,7 +97,7 @@ fun BugReportView( textAlign = TextAlign.Center, fontWeight = FontWeight.Bold, fontSize = 24.sp, - color = ElementTheme.colors.primary, + color = MaterialTheme.colorScheme.primary, ) // Form Text( @@ -106,7 +106,7 @@ fun BugReportView( .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 16.dp), fontSize = 16.sp, - color = ElementTheme.colors.primary, + color = MaterialTheme.colorScheme.primary, ) var descriptionFieldState by textFieldState( stateValue = state.formState.description diff --git a/features/roomlist/build.gradle.kts b/features/roomlist/build.gradle.kts index 08b043a17f..fc0402eafc 100644 --- a/features/roomlist/build.gradle.kts +++ b/features/roomlist/build.gradle.kts @@ -54,7 +54,4 @@ dependencies { androidTestImplementation(libs.test.junitext) ksp(libs.showkase.processor) - - // Not for components, but other classes. - implementation("androidx.compose.material3:material3") } 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 70b4d974c7..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,6 +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.MaterialTheme import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -45,7 +46,6 @@ 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.ElementTheme 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 @@ -130,7 +130,7 @@ fun SearchRoomListTopBar( placeholder = { Text( text = "Search", - color = ElementTheme.colors.onBackground.copy(alpha = ContentAlpha.medium) + color = MaterialTheme.colorScheme.onBackground.copy(alpha = ContentAlpha.medium) ) }, singleLine = true, @@ -144,7 +144,7 @@ fun SearchRoomListTopBar( Icon( imageVector = Icons.Default.Close, contentDescription = "clear", - tint = ElementTheme.colors.onBackground + tint = MaterialTheme.colorScheme.onBackground ) } } @@ -160,7 +160,7 @@ fun SearchRoomListTopBar( Icon( imageVector = Icons.Default.ArrowBack, contentDescription = "close", - tint = ElementTheme.colors.onBackground + tint = MaterialTheme.colorScheme.onBackground ) } }, 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 580d0ebc4b..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 @@ -31,6 +31,7 @@ import androidx.compose.foundation.layout.padding 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.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -103,7 +104,7 @@ internal fun DefaultRoomSummaryRow( modifier = Modifier.placeholder( visible = room.isPlaceholder, shape = CircleShape, - color = ElementTheme.colors.roomListPlaceHolder, + color = ElementTheme.colors.roomListPlaceHolder(), ) ) Column( @@ -117,12 +118,12 @@ internal fun DefaultRoomSummaryRow( modifier = Modifier.placeholder( visible = room.isPlaceholder, shape = TextPlaceholderShape, - color = ElementTheme.colors.roomListPlaceHolder, + color = ElementTheme.colors.roomListPlaceHolder(), ), fontSize = 16.sp, fontWeight = FontWeight.SemiBold, text = room.name, - color = ElementTheme.colors.roomListRoomName, + color = MaterialTheme.roomListRoomName(), maxLines = 1, overflow = TextOverflow.Ellipsis ) @@ -131,10 +132,10 @@ internal fun DefaultRoomSummaryRow( modifier = Modifier.placeholder( visible = room.isPlaceholder, shape = TextPlaceholderShape, - color = ElementTheme.colors.roomListPlaceHolder, + color = ElementTheme.colors.roomListPlaceHolder(), ), text = room.lastMessage?.toString().orEmpty(), - color = ElementTheme.colors.roomListRoomMessage, + color = MaterialTheme.roomListRoomMessage(), fontSize = 14.sp, maxLines = 1, overflow = TextOverflow.Ellipsis @@ -149,15 +150,15 @@ internal fun DefaultRoomSummaryRow( modifier = Modifier.placeholder( visible = room.isPlaceholder, shape = TextPlaceholderShape, - color = ElementTheme.colors.roomListPlaceHolder, + color = ElementTheme.colors.roomListPlaceHolder(), ), fontSize = 12.sp, text = room.timestamp ?: "", - color = ElementTheme.colors.roomListRoomMessageDate, + color = MaterialTheme.roomListRoomMessageDate(), ) Spacer(Modifier.size(4.dp)) val unreadIndicatorColor = - if (room.hasUnread) ElementTheme.colors.roomListUnreadIndicator else Color.Transparent + if (room.hasUnread) MaterialTheme.roomListUnreadIndicator() else Color.Transparent Box( modifier = Modifier .size(12.dp) 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 a98912d958..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,13 +17,14 @@ package io.element.android.features.template import androidx.compose.foundation.layout.Box +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.ElementTheme +import io.element.android.libraries.designsystem.theme.components.Text @Composable fun TemplateView( @@ -33,7 +34,7 @@ fun TemplateView( Box(modifier, contentAlignment = Alignment.Center) { Text( "Template feature view", - color = ElementTheme.colors.primary, + color = MaterialTheme.colorScheme.primary, ) } } diff --git a/libraries/designsystem/build.gradle.kts b/libraries/designsystem/build.gradle.kts index 10e51c8aec..45430e5d82 100644 --- a/libraries/designsystem/build.gradle.kts +++ b/libraries/designsystem/build.gradle.kts @@ -31,8 +31,5 @@ android { implementation(projects.libraries.elementresources) implementation(projects.libraries.uiStrings) ksp(libs.showkase.processor) - - // This is the only module to have a (temporary) dependence to material3. - implementation("androidx.compose.material3:material3") } } 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 f6a0122a80..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 @@ -16,15 +16,15 @@ package io.element.android.libraries.designsystem +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color -import io.element.android.libraries.designsystem.theme.ElementTheme @Composable fun Boolean.toEnabledColor(): Color { return if (this) { - ElementTheme.colors.primary + MaterialTheme.colorScheme.primary } else { - ElementTheme.colors.primary.copy(alpha = 0.40f) + MaterialTheme.colorScheme.primary.copy(alpha = 0.40f) } } 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 a8a5b94ff4..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,6 +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.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -31,7 +32,6 @@ 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.ElementTheme import io.element.android.libraries.designsystem.theme.components.Text import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.persistentMapOf @@ -85,6 +85,6 @@ fun ClickableLinkText( layoutResult.value = it }, inlineContent = inlineContent, - color = ElementTheme.colors.primary, + 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 cd381af63b..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,13 +18,13 @@ package io.element.android.libraries.designsystem.components import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +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.ElementTheme import io.element.android.libraries.designsystem.theme.components.Checkbox import io.element.android.libraries.designsystem.theme.components.Text @@ -47,7 +47,7 @@ fun LabelledCheckbox( ) Text( text = text, - color = ElementTheme.colors.primary, + color = MaterialTheme.colorScheme.primary, ) } } 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 6f8ec96118..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,6 +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.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -31,7 +32,6 @@ 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.ElementTheme import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.Text @@ -50,19 +50,19 @@ fun ProgressDialog( modifier = modifier .fillMaxWidth() .background( - color = ElementTheme.colors.surfaceVariant, + color = MaterialTheme.colorScheme.surfaceVariant, shape = RoundedCornerShape(8.dp) ) ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { CircularProgressIndicator( modifier = Modifier.padding(16.dp), - color = ElementTheme.colors.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurfaceVariant ) if (!text.isNullOrBlank()) { Text( text = text, - color = ElementTheme.colors.onSurfaceVariant, + color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(16.dp) ) } 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 b4b910ac3b..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 @@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.AlertDialog 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 @@ -33,7 +34,6 @@ 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.ElementTheme 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 @@ -51,10 +51,10 @@ fun ConfirmationDialog( onThirdButtonClicked: () -> Unit = {}, onDismiss: () -> Unit = {}, shape: Shape = AlertDialogDefaults.shape, - containerColor: Color = ElementTheme.colors.surfaceVariant, - iconContentColor: Color = ElementTheme.colors.onSurfaceVariant, - titleContentColor: Color = ElementTheme.colors.onSurfaceVariant, - textContentColor: Color = ElementTheme.colors.onSurfaceVariant, + 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( 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 b9740c2074..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 @@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.AlertDialog 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 @@ -32,7 +33,6 @@ 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.ElementTheme 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 @@ -45,10 +45,10 @@ fun ErrorDialog( submitText: String = stringResource(id = StringR.string.ok), onDismiss: () -> Unit = {}, shape: Shape = AlertDialogDefaults.shape, - containerColor: Color = ElementTheme.colors.surfaceVariant, - iconContentColor: Color = ElementTheme.colors.onSurfaceVariant, - titleContentColor: Color = ElementTheme.colors.onSurfaceVariant, - textContentColor: Color = ElementTheme.colors.onSurfaceVariant, + 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( 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 7206fb1081..eeed206e5f 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 @@ -23,13 +23,13 @@ import androidx.compose.foundation.layout.padding 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.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.ElementTheme import io.element.android.libraries.designsystem.theme.components.Divider import io.element.android.libraries.designsystem.theme.components.Text @@ -45,12 +45,12 @@ fun PreferenceCategory( ) { Divider( modifier = Modifier.padding(horizontal = 16.dp), - color = ElementTheme.colors.secondary, + color = MaterialTheme.colorScheme.secondary, thickness = 1.dp ) Text( modifier = Modifier.padding(top = 8.dp, start = 56.dp), - style = ElementTheme.typography.titleSmall, + style = MaterialTheme.typography.titleSmall, text = title ) content() 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 315f7caeab..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 @@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.Row 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.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -31,7 +32,6 @@ 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.ElementTheme 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 @@ -63,14 +63,14 @@ fun PreferenceSlide( ) { Text( modifier = Modifier.fillMaxWidth(), - style = ElementTheme.typography.bodyLarge, + style = MaterialTheme.typography.bodyLarge, color = enabled.toEnabledColor(), text = title ) summary?.let { Text( modifier = Modifier.fillMaxWidth(), - style = ElementTheme.typography.bodyMedium, + style = MaterialTheme.typography.bodyMedium, color = enabled.toEnabledColor(), text = 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 629148c071..7a4177a5ae 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,6 +24,7 @@ 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.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -63,7 +64,7 @@ fun PreferenceSwitch( ) Text( modifier = Modifier.weight(1f), - style = ElementTheme.typography.bodyLarge, + style = MaterialTheme.typography.bodyLarge, color = enabled.toEnabledColor(), text = title ) 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 b7b7662045..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 @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.fillMaxWidth 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.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -32,7 +33,6 @@ 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.ElementTheme import io.element.android.libraries.designsystem.theme.components.Text @Composable @@ -58,9 +58,9 @@ fun PreferenceText( modifier = Modifier .weight(1f) .padding(end = preferencePaddingEnd), - style = ElementTheme.typography.bodyLarge, + style = MaterialTheme.typography.bodyLarge, text = title, - color = ElementTheme.colors.primary, + color = MaterialTheme.colorScheme.primary, ) } } 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 index 7a286f5314..f79cfed70b 100644 --- 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 @@ -18,6 +18,7 @@ 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 @@ -54,7 +55,7 @@ private fun ElementPreview( ) { ElementTheme(darkTheme = darkTheme) { if (showBackground) { - Box(modifier = Modifier.background(ElementTheme.colors.background)) { + Box(modifier = Modifier.background(MaterialTheme.colorScheme.background)) { content() } } else { 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 index 1325211232..33c4032dc9 100644 --- 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 @@ -16,14 +16,25 @@ package io.element.android.libraries.designsystem.theme +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable import io.element.android.libraries.designsystem.SystemGrey4Dark import io.element.android.libraries.designsystem.SystemGrey6Light /** * Room list */ -val ElementColors.roomListRoomName get() = primary -val ElementColors.roomListRoomMessage get() = secondary -val ElementColors.roomListRoomMessageDate get() = secondary -val ElementColors.roomListUnreadIndicator get() = primary -val ElementColors.roomListPlaceHolder get() = if (isLight) SystemGrey6Light else SystemGrey4Dark +@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 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 index 19c7c2f49a..80b6122ba5 100644 --- 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 @@ -16,6 +16,7 @@ package io.element.android.libraries.designsystem.theme +import androidx.compose.material3.darkColorScheme import androidx.compose.ui.graphics.Color import io.element.android.libraries.designsystem.Azure import io.element.android.libraries.designsystem.DarkGrey @@ -23,24 +24,13 @@ import io.element.android.libraries.designsystem.SystemGrey5Dark import io.element.android.libraries.designsystem.SystemGrey6Dark fun elementColorsDark() = ElementColors( - primary = Color.White, - onPrimary = Color.Black, - secondary = DarkGrey, - text = Color.White, - background = Color.Black, - onBackground = Color.White, - surfaceVariant = SystemGrey5Dark, - onSurfaceVariant = Color.White, messageFromMeBackground = SystemGrey5Dark, messageFromOtherBackground = SystemGrey6Dark, messageHighlightedBackground = Azure, - success = Color.Green, - error = Color.Red, isLight = false, ) -/* -private val DarkColorScheme = darkColorScheme( +val materialColorSchemeDark = darkColorScheme( primary = Color.White, secondary = DarkGrey, tertiary = Color.White, @@ -50,4 +40,3 @@ private val DarkColorScheme = darkColorScheme( surfaceVariant = SystemGrey5Dark, onSurface = Color.White, ) - */ 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 index 6d7c7f62ea..8e8e41c9d6 100644 --- 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 @@ -16,6 +16,7 @@ package io.element.android.libraries.designsystem.theme +import androidx.compose.material3.lightColorScheme import androidx.compose.ui.graphics.Color import io.element.android.libraries.designsystem.Azure import io.element.android.libraries.designsystem.LightGrey @@ -23,24 +24,13 @@ import io.element.android.libraries.designsystem.SystemGrey5Light import io.element.android.libraries.designsystem.SystemGrey6Light fun elementColorsLight() = ElementColors( - primary = Color.Black, - onPrimary = Color.White, - secondary = LightGrey, - text = Color.Black, - background = Color.White, - onBackground = Color.Black, - surfaceVariant = SystemGrey5Light, - onSurfaceVariant = Color.Black, messageFromMeBackground = SystemGrey5Light, messageFromOtherBackground = SystemGrey6Light, messageHighlightedBackground = Azure, - success = Color.Green, - error = Color.Red, isLight = true, ) -/* -private val LightColorScheme = lightColorScheme( +val materialColorSchemeLight = lightColorScheme( primary = Color.Black, secondary = LightGrey, tertiary = Color.Black, @@ -50,5 +40,4 @@ private val LightColorScheme = lightColorScheme( surfaceVariant = SystemGrey5Light, onSurface = Color.Black, onSurfaceVariant = Color.Black, - - */ +) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ContentColor.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ContentColor.kt deleted file mode 100644 index 1af916a109..0000000000 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ContentColor.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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.LocalContentColor -import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReadOnlyComposable -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.takeOrElse - -@Composable -@ReadOnlyComposable -fun elementContentColorFor(backgroundColor: Color): Color { - return ElementTheme.colors.elementContentColorFor(backgroundColor).takeOrElse { - LocalContentColor.current - } -} - -fun ElementColors.elementContentColorFor(backgroundColor: Color): Color { - return when (backgroundColor) { - primary -> onPrimary - surfaceVariant -> onSurfaceVariant - background -> onBackground - else -> Color.Unspecified - } -} 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 index 38da8e7816..b39c31d157 100644 --- 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 @@ -22,41 +22,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color class ElementColors( - primary: Color, - onPrimary: Color, - secondary: Color, - text: Color, - background: Color, - onBackground: Color, - surfaceVariant: Color, - onSurfaceVariant: Color, messageFromMeBackground: Color, messageFromOtherBackground: Color, messageHighlightedBackground: Color, - success: Color, - error: Color, isLight: Boolean, ) { - var primary by mutableStateOf(primary) - private set - var onPrimary by mutableStateOf(onPrimary) - private set - var secondary by mutableStateOf(secondary) - private set - var text by mutableStateOf(text) - private set - var success by mutableStateOf(success) - private set - var error by mutableStateOf(error) - private set - var background by mutableStateOf(background) - private set - var onBackground by mutableStateOf(onBackground) - private set - var surfaceVariant by mutableStateOf(surfaceVariant) - private set - var onSurfaceVariant by mutableStateOf(onSurfaceVariant) - private set var messageFromMeBackground by mutableStateOf(messageFromMeBackground) private set var messageFromOtherBackground by mutableStateOf(messageFromOtherBackground) @@ -68,51 +38,21 @@ class ElementColors( private set fun copy( - primary: Color = this.primary, - onPrimary: Color = this.onPrimary, - secondary: Color = this.secondary, - text: Color = this.text, - background: Color = this.background, - onBackground: Color = this.onBackground, - surfaceVariant: Color = this.surfaceVariant, - onSurfaceVariant: Color = this.onSurfaceVariant, messageFromMeBackground: Color = this.messageFromMeBackground, messageFromOtherBackground: Color = this.messageFromOtherBackground, messageHighlightedBackground: Color = this.messageHighlightedBackground, - success: Color = this.success, - error: Color = this.error, isLight: Boolean = this.isLight, ) = ElementColors( - primary = primary, - onPrimary = onPrimary, - secondary = secondary, - text = text, - background = background, - onBackground = onBackground, - surfaceVariant = surfaceVariant, - onSurfaceVariant = onSurfaceVariant, messageFromMeBackground = messageFromMeBackground, messageFromOtherBackground = messageFromOtherBackground, messageHighlightedBackground = messageHighlightedBackground, - success = success, - error = error, isLight = isLight, ) fun updateColorsFrom(other: ElementColors) { - primary = other.primary - onPrimary = other.onPrimary - secondary = other.secondary - text = other.text - success = other.success - background = other.background - onBackground = other.onBackground - surfaceVariant = other.surfaceVariant - onSurfaceVariant = other.onSurfaceVariant messageFromMeBackground = other.messageFromMeBackground messageFromOtherBackground = other.messageFromOtherBackground messageHighlightedBackground = other.messageHighlightedBackground - error = other.error isLight = other.isLight } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementSpaces.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementSpaces.kt deleted file mode 100644 index e5f2236cf0..0000000000 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementSpaces.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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.unit.Dp -import androidx.compose.ui.unit.dp - -data class ElementSpaces( - val small: Dp = 4.dp, - val medium: Dp = 8.dp, - val large: Dp = 16.dp, - val extraLarge: Dp = 40.dp, -) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementTheme.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementTheme.kt index 5281888b17..7e7d4d8caf 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementTheme.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementTheme.kt @@ -17,7 +17,8 @@ package io.element.android.libraries.designsystem.theme import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material.ProvideTextStyle +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ReadOnlyComposable @@ -35,50 +36,42 @@ object ElementTheme { @Composable @ReadOnlyComposable get() = LocalColors.current - - val typography: ElementTypography - @Composable - @ReadOnlyComposable - get() = LocalTypography.current - - val spaces: ElementSpaces - @Composable - @ReadOnlyComposable - get() = LocalSpaces.current } /* Global variables (application level) */ -val LocalSpaces = staticCompositionLocalOf { ElementSpaces() } val LocalColors = staticCompositionLocalOf { elementColorsLight() } -val LocalTypography = staticCompositionLocalOf { ElementTypography() } @Composable fun ElementTheme( darkTheme: Boolean = isSystemInDarkTheme(), lightColors: ElementColors = elementColorsLight(), darkColors: ElementColors = elementColorsDark(), - typography: ElementTypography = ElementTheme.typography, - spaces: ElementSpaces = ElementTheme.spaces, + materialLightColors: ColorScheme = materialColorSchemeLight, + materialDarkColors: ColorScheme = materialColorSchemeDark, content: @Composable () -> Unit, ) { val systemUiController = rememberSystemUiController() val useDarkIcons = !darkTheme val currentColor = remember { if (darkTheme) darkColors else lightColors } - SideEffect { - systemUiController.setStatusBarColor( - color = currentColor.background - ) - systemUiController.setSystemBarsColor( - color = Color.Transparent, - darkIcons = useDarkIcons - ) - } val rememberedColors = remember { currentColor.copy() }.apply { updateColorsFrom(currentColor) } CompositionLocalProvider( LocalColors provides rememberedColors, - LocalSpaces provides spaces, - LocalTypography provides typography, ) { - ProvideTextStyle(typography.body1, content = content) + MaterialTheme( + colorScheme = if (darkTheme) materialDarkColors else materialLightColors + ) { + val bgColor = MaterialTheme.colorScheme.background + SideEffect { + systemUiController.setStatusBarColor( + color = bgColor + ) + systemUiController.setSystemBarsColor( + color = Color.Transparent, + darkIcons = useDarkIcons + ) + } + + 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 index 57d0beaa36..b2159522c8 100644 --- 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 @@ -22,6 +22,9 @@ 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, @@ -104,15 +107,3 @@ val titleMediumDefault: TextStyle = TextStyle( letterSpacing = 0.5.sp ) -data class ElementTypography( - val h1: TextStyle = h1Default, - val body1: TextStyle = body1Default, - val bodySmall: TextStyle = bodySmallDefault, - val bodyMedium: TextStyle = bodyMediumDefault, - val bodyLarge: TextStyle = bodyLargeDefault, - val headlineSmall: TextStyle = headlineSmallDefault, - val headlineMedium: TextStyle = headlineMediumDefault, - val headlineLarge: TextStyle = headlineLargeDefault, - val titleSmall: TextStyle = titleSmallDefault, - val titleMedium: TextStyle = titleMediumDefault, -) 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 index cb49ebdcd7..d20e5f9651 100644 --- 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 @@ -16,55 +16,46 @@ 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.material.ContentAlpha -import androidx.compose.material.ProvideTextStyle +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.Color -import androidx.compose.ui.graphics.compositeOver +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 -import io.element.android.libraries.designsystem.theme.ElementTheme -import io.element.android.libraries.designsystem.theme.elementContentColorFor @Composable fun Button( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, - containerColor: Color = ElementTheme.colors.primary, + 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( - colors = ButtonDefaults.buttonColors( - containerColor = containerColor, - contentColor = elementContentColorFor(backgroundColor = containerColor), - disabledContainerColor = containerColor - .copy(alpha = 0.12f) - .compositeOver(containerColor), - disabledContentColor = elementContentColorFor(backgroundColor = containerColor) - .copy(alpha = ContentAlpha.disabled) - ), - // TODO shape = ButtonShape, - // TODO elevation = ButtonDefaults.elevation( - // defaultElevation = ElementTheme.elevation.default, - // pressedElevation = ElementTheme.elevation.pressed - // /* disabledElevation = 0.dp */ - // ), onClick = onClick, modifier = modifier, enabled = enabled, - content = { - ProvideTextStyle( - value = ElementTheme.typography.body1 - ) { - content() - } - } + shape = shape, + colors = colors, + elevation = elevation, + border = border, + contentPadding = contentPadding, + interactionSource = interactionSource, + content = content, ) } 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 index 25b1637ac8..2c8a7411fb 100644 --- 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 @@ -23,11 +23,9 @@ import androidx.compose.material3.CheckboxDefaults 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.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.ElementTheme @Composable fun Checkbox( @@ -35,16 +33,7 @@ fun Checkbox( onCheckedChange: ((Boolean) -> Unit)?, modifier: Modifier = Modifier, enabled: Boolean = true, - colors: CheckboxColors = CheckboxDefaults.colors( - checkedColor = ElementTheme.colors.primary, - uncheckedColor = Color.Gray, // TODO ElementTheme.colors. - checkmarkColor = Color.Gray, // TODO ElementTheme.colors. - disabledCheckedColor = Color.Gray // TODO ElementTheme.colors. - .copy(alpha = 0.2F), - disabledUncheckedColor = Color.Gray // TODO ElementTheme.colors. - .copy(alpha = 0.2F), - disabledIndeterminateColor = Color.Gray // TODO ElementTheme.colors. - ), + colors: CheckboxColors = CheckboxDefaults.colors(), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } ) { androidx.compose.material3.Checkbox( 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 index 38eb970828..00a0f8c0de 100644 --- 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 @@ -21,7 +21,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp -import io.element.android.libraries.designsystem.theme.ElementTheme @Composable fun CircularProgressIndicator( @@ -41,7 +40,7 @@ fun CircularProgressIndicator( @Composable fun CircularProgressIndicator( modifier: Modifier = Modifier, - color: Color = ElementTheme.colors.primary, + color: Color = ProgressIndicatorDefaults.circularColor, strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth, ) { androidx.compose.material3.CircularProgressIndicator( 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 index 832f3b89d4..f15b81129e 100644 --- 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 @@ -24,13 +24,12 @@ 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.ElementTheme @Composable fun Divider( modifier: Modifier = Modifier, thickness: Dp = DividerDefaults.Thickness, - color: Color = ElementTheme.colors.onBackground, + color: Color = DividerDefaults.color, ) { androidx.compose.material3.Divider( modifier = modifier, 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 index 579583757f..cbc215717d 100644 --- 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 @@ -24,7 +24,6 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import io.element.android.libraries.designsystem.theme.ElementTheme @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -34,13 +33,7 @@ fun MediumTopAppBar( navigationIcon: @Composable () -> Unit = {}, actions: @Composable RowScope.() -> Unit = {}, windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, - colors: TopAppBarColors = TopAppBarDefaults.mediumTopAppBarColors( - containerColor = ElementTheme.colors.background, - scrolledContainerColor = ElementTheme.colors.background, - navigationIconContentColor = ElementTheme.colors.onBackground, - titleContentColor = ElementTheme.colors.onBackground, - actionIconContentColor = ElementTheme.colors.onBackground, - ), + colors: TopAppBarColors = TopAppBarDefaults.mediumTopAppBarColors(), scrollBehavior: TopAppBarScrollBehavior? = null ) { androidx.compose.material3.MediumTopAppBar( 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 index d3ca206832..6b558e3418 100644 --- 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 @@ -22,14 +22,13 @@ 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 -import io.element.android.libraries.designsystem.theme.ElementTheme -import io.element.android.libraries.designsystem.theme.elementContentColorFor @OptIn(ExperimentalMaterialApi::class) @Composable @@ -39,9 +38,9 @@ fun ModalBottomSheetLayout( sheetState: ModalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden), sheetShape: Shape = MaterialTheme.shapes.large, sheetElevation: Dp = ModalBottomSheetDefaults.Elevation, - sheetBackgroundColor: Color = ElementTheme.colors.surfaceVariant, - sheetContentColor: Color = elementContentColorFor(sheetBackgroundColor), - scrimColor: Color = ElementTheme.colors.onSurfaceVariant.copy(alpha = 0.32f), + sheetBackgroundColor: Color = MaterialTheme.colors.surface, + sheetContentColor: Color = contentColorFor(sheetBackgroundColor), + scrimColor: Color = ModalBottomSheetDefaults.scrimColor, content: @Composable () -> Unit = {} ) { androidx.compose.material.ModalBottomSheetLayout( 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 index fe178bb28d..fae6c06328 100644 --- 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 @@ -23,6 +23,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.selection.TextSelectionColors 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 @@ -35,7 +36,6 @@ import androidx.compose.ui.tooling.preview.Preview import io.element.android.libraries.designsystem.ElementGreen import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight -import io.element.android.libraries.designsystem.theme.ElementTheme @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -59,6 +59,7 @@ fun OutlinedTextField( maxLines: Int = Int.MAX_VALUE, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = TextFieldDefaults.outlinedShape, + colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors() ) { androidx.compose.material3.OutlinedTextField( value = value, @@ -80,39 +81,7 @@ fun OutlinedTextField( maxLines = maxLines, interactionSource = interactionSource, shape = shape, - colors = TextFieldDefaults.outlinedTextFieldColors( - textColor = ElementTheme.colors.primary, - disabledTextColor = ElementTheme.colors.primary.copy(alpha = 0.38f), - containerColor = Color.Transparent, - cursorColor = ElementTheme.colors.primary, - errorCursorColor = ElementTheme.colors.error, - selectionColors = TextSelectionColors( - handleColor = ElementGreen, - backgroundColor = ElementGreen.copy(alpha = 0.4f) - ), - focusedBorderColor = ElementTheme.colors.primary, - unfocusedBorderColor = ElementTheme.colors.secondary, - disabledBorderColor = ElementTheme.colors.secondary.copy(alpha = 0.12f), - errorBorderColor = ElementTheme.colors.error, - focusedLeadingIconColor = ElementTheme.colors.primary, - unfocusedLeadingIconColor = ElementTheme.colors.secondary, - disabledLeadingIconColor = ElementTheme.colors.secondary.copy(0.12f), - errorLeadingIconColor = ElementTheme.colors.error, - focusedTrailingIconColor = ElementTheme.colors.primary, - unfocusedTrailingIconColor = ElementTheme.colors.secondary, - disabledTrailingIconColor = ElementTheme.colors.secondary.copy(alpha = 0.12f), - errorTrailingIconColor = ElementTheme.colors.error, - focusedLabelColor = ElementTheme.colors.primary, - unfocusedLabelColor = ElementTheme.colors.secondary, - disabledLabelColor = ElementTheme.colors.secondary.copy(alpha = 0.12f), - errorLabelColor = ElementTheme.colors.error, - placeholderColor = ElementTheme.colors.secondary, - disabledPlaceholderColor = ElementTheme.colors.secondary.copy(alpha = 0.12f), - focusedSupportingTextColor = ElementTheme.colors.primary, - unfocusedSupportingTextColor = ElementTheme.colors.secondary, - disabledSupportingTextColor = ElementTheme.colors.primary.copy(alpha = 0.12f), - errorSupportingTextColor = ElementTheme.colors.error, - ), + colors = colors, ) } 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 index 7a1ede6d70..99a69c0553 100644 --- 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 @@ -20,12 +20,12 @@ 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 -import io.element.android.libraries.designsystem.theme.ElementTheme -import io.element.android.libraries.designsystem.theme.elementContentColorFor @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -36,8 +36,8 @@ fun Scaffold( snackbarHost: @Composable () -> Unit = {}, floatingActionButton: @Composable () -> Unit = {}, floatingActionButtonPosition: FabPosition = FabPosition.End, - containerColor: Color = ElementTheme.colors.background, - contentColor: Color = elementContentColorFor(containerColor), + containerColor: Color = MaterialTheme.colorScheme.background, + contentColor: Color = contentColorFor(containerColor), contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets, content: @Composable (PaddingValues) -> Unit ) { 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 index 87e1757dd7..edefe1ab98 100644 --- 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 @@ -18,6 +18,7 @@ package io.element.android.libraries.designsystem.theme.components import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Column +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SliderColors import androidx.compose.material3.SliderDefaults import androidx.compose.runtime.Composable @@ -26,7 +27,6 @@ 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.ElementTheme @Composable fun Slider( @@ -38,18 +38,7 @@ fun Slider( /*@IntRange(from = 0)*/ steps: Int = 0, onValueChangeFinished: (() -> Unit)? = null, - colors: SliderColors = SliderDefaults.colors( - thumbColor = ElementTheme.colors.primary, - activeTrackColor = ElementTheme.colors.primary, - activeTickColor = ElementTheme.colors.primary, - inactiveTrackColor = ElementTheme.colors.primary, - inactiveTickColor = ElementTheme.colors.primary, - disabledThumbColor = ElementTheme.colors.primary.copy(alpha = 0.40f), - disabledActiveTrackColor = ElementTheme.colors.primary.copy(alpha = 0.40f), - disabledActiveTickColor = ElementTheme.colors.primary.copy(alpha = 0.40f), - disabledInactiveTrackColor = ElementTheme.colors.primary.copy(alpha = 0.40f), - disabledInactiveTickColor = ElementTheme.colors.primary.copy(alpha = 0.40f), - ), + colors: SliderColors = SliderDefaults.colors(), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } ) { androidx.compose.material3.Slider( 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 index 0689680972..8518d8a715 100644 --- 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 @@ -17,6 +17,8 @@ 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 @@ -24,15 +26,13 @@ import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import io.element.android.libraries.designsystem.theme.ElementTheme -import io.element.android.libraries.designsystem.theme.elementContentColorFor @Composable fun Surface( modifier: Modifier = Modifier, shape: Shape = RectangleShape, - color: Color = ElementTheme.colors.surfaceVariant, - contentColor: Color = elementContentColorFor(color), + color: Color = MaterialTheme.colorScheme.surface, + contentColor: Color = contentColorFor(color), tonalElevation: Dp = 0.dp, shadowElevation: Dp = 0.dp, border: BorderStroke? = null, 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 index 3a16b8fedb..dba39fccf1 100644 --- 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 @@ -22,6 +22,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.ContentAlpha 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 @@ -30,7 +31,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.VisualTransformation -import io.element.android.libraries.designsystem.theme.ElementTheme @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -54,6 +54,7 @@ fun TextField( maxLines: Int = Int.MAX_VALUE, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = TextFieldDefaults.filledShape, + colors: TextFieldColors = TextFieldDefaults.textFieldColors() ) { androidx.compose.material3.TextField( value = value, @@ -75,13 +76,6 @@ fun TextField( maxLines = maxLines, interactionSource = interactionSource, shape = shape, - colors = TextFieldDefaults.textFieldColors( - textColor = ElementTheme.colors.onBackground, - containerColor = Color.Transparent, - cursorColor = ElementTheme.colors.onBackground.copy(alpha = ContentAlpha.medium), - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent - ), + 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 index 3930e2e3c1..76c388cefe 100644 --- 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 @@ -24,7 +24,6 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import io.element.android.libraries.designsystem.theme.ElementTheme @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -34,13 +33,7 @@ fun TopAppBar( navigationIcon: @Composable () -> Unit = {}, actions: @Composable RowScope.() -> Unit = {}, windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, - colors: TopAppBarColors = TopAppBarDefaults.smallTopAppBarColors( - containerColor = ElementTheme.colors.background, - scrolledContainerColor = ElementTheme.colors.background, - navigationIconContentColor = ElementTheme.colors.onBackground, - titleContentColor = ElementTheme.colors.onBackground, - actionIconContentColor = ElementTheme.colors.onBackground, - ), + colors: TopAppBarColors = TopAppBarDefaults.smallTopAppBarColors(), scrollBehavior: TopAppBarScrollBehavior? = null ) { androidx.compose.material3.TopAppBar( 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 a8ce4af56f..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 @@ -23,6 +23,7 @@ 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.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -36,7 +37,6 @@ 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.ElementTheme 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 @@ -67,14 +67,14 @@ fun MatrixUserHeader( text = matrixUser.getBestName(), maxLines = 1, overflow = TextOverflow.Ellipsis, - color = ElementTheme.colors.primary, + color = MaterialTheme.colorScheme.primary, ) // Id if (matrixUser.username.isNullOrEmpty().not()) { Spacer(modifier = Modifier.height(4.dp)) Text( text = matrixUser.id.value, - color = ElementTheme.colors.secondary, + color = MaterialTheme.colorScheme.secondary, fontSize = 14.sp, maxLines = 1, overflow = TextOverflow.Ellipsis 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 5b6db5b7e6..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 @@ -23,6 +23,7 @@ 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.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -35,7 +36,6 @@ 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.ElementTheme 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 @@ -71,13 +71,13 @@ fun MatrixUserRow( text = matrixUser.getBestName(), maxLines = 1, overflow = TextOverflow.Ellipsis, - color = ElementTheme.colors.primary, + color = MaterialTheme.colorScheme.primary, ) // Id if (matrixUser.username.isNullOrEmpty().not()) { Text( text = matrixUser.id.value, - color = ElementTheme.colors.secondary, + color = MaterialTheme.colorScheme.secondary, fontSize = 14.sp, maxLines = 1, overflow = TextOverflow.Ellipsis 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 ab736f4177..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,6 +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.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -34,7 +35,6 @@ 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.ElementTheme import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.ui.strings.R as StringR @@ -127,7 +127,7 @@ private fun FakeComposer( textAlign = TextAlign.Center, text = "Composer Preview", fontSize = 20.sp, - color = ElementTheme.colors.secondary, + color = MaterialTheme.colorScheme.secondary, ) } } diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index 49cf6dd1cd..710f603cad 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -38,6 +38,7 @@ fun DependencyHandlerScope.composeDependencies() { androidTestImplementation(composeBom) implementation("androidx.compose.ui:ui") implementation("androidx.compose.material:material") + implementation("androidx.compose.material3:material3") implementation("androidx.compose.material:material-icons-extended") implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.activity:activity-compose:1.6.1") 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 bcd78869cf..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 @@ -24,6 +24,7 @@ import androidx.activity.OnBackPressedDispatcherOwner import androidx.activity.compose.LocalOnBackPressedDispatcherOwner import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration @@ -74,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( @@ -100,8 +100,8 @@ class ScreenshotTest { override fun getOnBackPressedDispatcher() = OnBackPressedDispatcher() } ) { - ElementTheme(darkTheme = (theme == "dark")) { - Box(modifier = Modifier.background(ElementTheme.colors.background)) { + ElementTheme { + Box(modifier = Modifier.background(MaterialTheme.colorScheme.background)) { componentTestPreview.Content() } } diff --git a/tests/uitests/src/test/kotlin/io/element/android/tests/uitests/TypographyTestPreview.kt b/tests/uitests/src/test/kotlin/io/element/android/tests/uitests/TypographyTestPreview.kt index 6f3b4f4bbf..80f27c25a0 100644 --- a/tests/uitests/src/test/kotlin/io/element/android/tests/uitests/TypographyTestPreview.kt +++ b/tests/uitests/src/test/kotlin/io/element/android/tests/uitests/TypographyTestPreview.kt @@ -19,11 +19,11 @@ package io.element.android.tests.uitests import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.BasicText +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.airbnb.android.showkase.models.ShowkaseBrowserTypography import com.airbnb.android.showkase.ui.padding4x -import io.element.android.libraries.designsystem.theme.ElementTheme import java.util.Locale class TypographyTestPreview( @@ -39,7 +39,7 @@ class TypographyTestPreview( .fillMaxWidth() .padding(padding4x), style = showkaseBrowserTypography.textStyle.copy( - color = ElementTheme.colors.onBackground + color = MaterialTheme.colorScheme.onBackground ) ) } From 41456ec13bb7b0378f8f72b3d3989c7a75ab071f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 3 Feb 2023 10:24:52 +0100 Subject: [PATCH 28/96] Add some TODO for design input --- .../designsystem/theme/ColorsDark.kt | 24 ++++++++++++++++++- .../designsystem/theme/ColorsLight.kt | 23 +++++++++++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) 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 index 80b6122ba5..ff02755081 100644 --- 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 @@ -30,13 +30,35 @@ fun elementColorsDark() = ElementColors( 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, - surfaceVariant = SystemGrey5Dark, 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, ) 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 index 8e8e41c9d6..43932edd10 100644 --- 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 @@ -30,14 +30,35 @@ fun elementColorsLight() = ElementColors( 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, - surfaceVariant = SystemGrey5Light, 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, ) From 2989370c3be5eafe1cdbde2dab687df7aaeed3b8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 3 Feb 2023 10:46:06 +0100 Subject: [PATCH 29/96] Fix issue. --- .../designsystem/theme/ElementTheme.kt | 41 ++++++++++++------- .../designsystem/theme/components/Slider.kt | 1 - 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementTheme.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementTheme.kt index 7e7d4d8caf..cbeb84badb 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementTheme.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementTheme.kt @@ -16,9 +16,12 @@ 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.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ReadOnlyComposable @@ -26,6 +29,7 @@ import androidx.compose.runtime.SideEffect 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 /** @@ -44,6 +48,7 @@ val LocalColors = staticCompositionLocalOf { elementColorsLight() } @Composable fun ElementTheme( darkTheme: Boolean = isSystemInDarkTheme(), + dynamicColor: Boolean = false, /* true to enable MaterialYou */ lightColors: ElementColors = elementColorsLight(), darkColors: ElementColors = elementColorsDark(), materialLightColors: ColorScheme = materialColorSchemeLight, @@ -53,25 +58,31 @@ fun ElementTheme( 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 -> materialDarkColors + else -> materialLightColors + } + SideEffect { + systemUiController.setStatusBarColor( + color = colorScheme.background + ) + systemUiController.setSystemBarsColor( + color = Color.Transparent, + darkIcons = useDarkIcons + ) + } val rememberedColors = remember { currentColor.copy() }.apply { updateColorsFrom(currentColor) } CompositionLocalProvider( LocalColors provides rememberedColors, ) { MaterialTheme( - colorScheme = if (darkTheme) materialDarkColors else materialLightColors - ) { - val bgColor = MaterialTheme.colorScheme.background - SideEffect { - systemUiController.setStatusBarColor( - color = bgColor - ) - systemUiController.setSystemBarsColor( - color = Color.Transparent, - darkIcons = useDarkIcons - ) - } - - content() - } + colorScheme = colorScheme, + // TODO typography = + 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 index edefe1ab98..a303e1ba77 100644 --- 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 @@ -18,7 +18,6 @@ package io.element.android.libraries.designsystem.theme.components import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Column -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SliderColors import androidx.compose.material3.SliderDefaults import androidx.compose.runtime.Composable From d7e62f4ae3fd0ded42ea3c1e97e771fd59bdb465 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 3 Feb 2023 13:12:11 +0100 Subject: [PATCH 30/96] Update Screenshot test reference. --- ...iew_tests[Color_Material Design_DarkGrey,NEXUS_5,1,en].png} | 0 ...ew_tests[Color_Material Design_LightGrey,NEXUS_5,1,en].png} | 0 ...st_preview_tests[Typo_Element_Body Large,NEXUS_5,1,en].png} | 0 ...shotTest_preview_tests[Typo_Element_Body1,NEXUS_5,1,en].png | 3 +++ ...Test_preview_tests[Typo_Element_BodySmall,NEXUS_5,1,en].png | 3 +++ ...eenshotTest_preview_tests[Typo_Element_H1,NEXUS_5,1,en].png | 3 +++ ...preview_tests[Typo_Element_Headline Large,NEXUS_5,1,en].png | 3 +++ ...review_tests[Typo_Element_Headline Medium,NEXUS_5,1,en].png | 3 +++ ...preview_tests[Typo_Element_Headline Small,NEXUS_5,1,en].png | 3 +++ ...est_preview_tests[Typo_Element_bodyMedium,NEXUS_5,1,en].png | 3 +++ ...st_preview_tests[Typo_Element_titleMedium,NEXUS_5,1,en].png | 3 +++ ...est_preview_tests[Typo_Element_titleSmall,NEXUS_5,1,en].png | 3 +++ ...DefaultGroup_ChangeServerViewDarkPreview_,NEXUS_5,1,en].png | 3 +++ ...faultGroup_ChangeServerViewLightPreview_,NEXUS_5,1,en].png} | 0 ..._DefaultGroup_LoginRootScreenDarkPreview_,NEXUS_5,1,en].png | 3 +++ ...efaultGroup_LoginRootScreenLightPreview_,NEXUS_5,1,en].png} | 0 ...ultGroup_LogoutPreferenceViewDarkPreview_,NEXUS_5,1,en].png | 3 +++ ...tGroup_LogoutPreferenceViewLightPreview_,NEXUS_5,1,en].png} | 0 ...ll_DefaultGroup_MatrixUserRowDarkPreview_,NEXUS_5,1,en].png | 3 +++ ...l_DefaultGroup_MatrixUserRowLightPreview_,NEXUS_5,1,en].png | 3 +++ ...efaultGroup_LoginRootScreenDarkPreview__0,NEXUS_5,1,en].png | 3 +++ ...faultGroup_LoginRootScreenDarkPreview__1,NEXUS_5,1,en].png} | 0 ...efaultGroup_LoginRootScreenDarkPreview__2,NEXUS_5,1,en].png | 3 +++ ...faultGroup_LoginRootScreenDarkPreview__3,NEXUS_5,1,en].png} | 0 ...efaultGroup_LoginRootScreenDarkPreview__4,NEXUS_5,1,en].png | 3 +++ ...faultGroup_LoginRootScreenDarkPreview__5,NEXUS_5,1,en].png} | 0 ...aultGroup_LoginRootScreenLightPreview__0,NEXUS_5,1,en].png} | 0 ...aultGroup_LoginRootScreenLightPreview__1,NEXUS_5,1,en].png} | 0 ...aultGroup_LoginRootScreenLightPreview__2,NEXUS_5,1,en].png} | 0 ...aultGroup_LoginRootScreenLightPreview__3,NEXUS_5,1,en].png} | 0 ...aultGroup_LoginRootScreenLightPreview__4,NEXUS_5,1,en].png} | 0 ...aultGroup_LoginRootScreenLightPreview__5,NEXUS_5,1,en].png} | 0 ...aultGroup_PreferencesRootViewDarkPreview_,NEXUS_5,1,en].png | 3 +++ ...ultGroup_PreferencesRootViewLightPreview_,NEXUS_5,1,en].png | 3 +++ ...ll_DefaultGroup_BugReportViewDarkPreview_,NEXUS_5,1,en].png | 3 +++ ..._DefaultGroup_BugReportViewLightPreview_,NEXUS_5,1,en].png} | 0 ...ltGroup_CrashDetectionContentDarkPreview_,NEXUS_5,1,en].png | 3 +++ ...tGroup_CrashDetectionContentLightPreview_,NEXUS_5,1,en].png | 3 +++ ...tGroup_RageshakeDialogContentDarkPreview_,NEXUS_5,1,en].png | 3 +++ ...Group_RageshakeDialogContentLightPreview_,NEXUS_5,1,en].png | 3 +++ ...roup_RageshakePreferencesViewDarkPreview_,NEXUS_5,1,en].png | 3 +++ ...up_RageshakePreferencesViewLightPreview_,NEXUS_5,1,en].png} | 0 ...kePreferencesViewNotSupportedDarkPreview_,NEXUS_5,1,en].png | 3 +++ ...PreferencesViewNotSupportedLightPreview_,NEXUS_5,1,en].png} | 0 ...ull_DefaultGroup_RoomListViewDarkPreview_,NEXUS_5,1,en].png | 3 +++ ...ll_DefaultGroup_RoomListViewLightPreview_,NEXUS_5,1,en].png | 3 +++ ...atar_null_DefaultGroup_AvatarDarkPreview_,NEXUS_5,1,en].png | 3 +++ ...ar_null_DefaultGroup_AvatarLightPreview_,NEXUS_5,1,en].png} | 0 ...faultGroup_ConfirmationDialogDarkPreview_,NEXUS_5,1,en].png | 3 +++ ...aultGroup_ConfirmationDialogLightPreview_,NEXUS_5,1,en].png | 3 +++ ...null_DefaultGroup_ErrorDialogDarkPreview_,NEXUS_5,1,en].png | 3 +++ ...ull_DefaultGroup_ErrorDialogLightPreview_,NEXUS_5,1,en].png | 3 +++ ...faultGroup_PreferenceCategoryDarkPreview_,NEXUS_5,1,en].png | 3 +++ ...ultGroup_PreferenceCategoryLightPreview_,NEXUS_5,1,en].png} | 0 ..._DefaultGroup_PreferenceSlideDarkPreview_,NEXUS_5,1,en].png | 3 +++ ...efaultGroup_PreferenceSlideLightPreview_,NEXUS_5,1,en].png} | 0 ...DefaultGroup_PreferenceSwitchDarkPreview_,NEXUS_5,1,en].png | 3 +++ ...faultGroup_PreferenceSwitchLightPreview_,NEXUS_5,1,en].png} | 0 ...l_DefaultGroup_PreferenceTextDarkPreview_,NEXUS_5,1,en].png | 3 +++ ...DefaultGroup_PreferenceTextLightPreview_,NEXUS_5,1,en].png} | 0 ..._DefaultGroup_PreferenceViewDarkPreview_,NEXUS_5,1,en].png} | 0 ...DefaultGroup_PreferenceViewLightPreview_,NEXUS_5,1,en].png} | 0 ...DefaultGroup_LabelledCheckboxDarkPreview_,NEXUS_5,1,en].png | 3 +++ ...faultGroup_LabelledCheckboxLightPreview_,NEXUS_5,1,en].png} | 0 ...l_DefaultGroup_ProgressDialogDarkPreview_,NEXUS_5,1,en].png | 3 +++ ..._DefaultGroup_ProgressDialogLightPreview_,NEXUS_5,1,en].png | 3 +++ ...nts_null_DefaultGroup_ButtonsDarkPreview_,NEXUS_5,1,en].png | 3 +++ ...ts_null_DefaultGroup_ButtonsLightPreview_,NEXUS_5,1,en].png | 3 +++ ..._null_DefaultGroup_CheckboxesDarkPreview_,NEXUS_5,1,en].png | 3 +++ ...null_DefaultGroup_CheckboxesLightPreview_,NEXUS_5,1,en].png | 3 +++ ...nts_null_DefaultGroup_DividerDarkPreview_,NEXUS_5,1,en].png | 3 +++ ...ts_null_DefaultGroup_DividerLightPreview_,NEXUS_5,1,en].png | 3 +++ ...faultGroup_OutlinedTextFieldsDarkPreview_,NEXUS_5,1,en].png | 3 +++ ...aultGroup_OutlinedTextFieldsLightPreview_,NEXUS_5,1,en].png | 3 +++ ...nts_null_DefaultGroup_SlidersDarkPreview_,NEXUS_5,1,en].png | 3 +++ ...ts_null_DefaultGroup_SlidersLightPreview_,NEXUS_5,1,en].png | 3 +++ ...faultGroup_ShowkaseButtonDarkPreview_null,NEXUS_5,1,en].png | 3 +++ ...ultGroup_ShowkaseButtonLightPreview_null,NEXUS_5,1,en].png} | 0 ...tests[Color_Material Design_DarkGrey,NEXUS_5,1,dark,en].png | 3 --- ...ests[Color_Material Design_LightGrey,NEXUS_5,1,dark,en].png | 3 --- ...review_tests[Typo_Element_Body Large,NEXUS_5,1,dark,en].png | 3 --- ...ew_tests[Typo_Element_Headline Small,NEXUS_5,1,dark,en].png | 3 --- ...w_tests[Typo_Element_Headline Small,NEXUS_5,1,light,en].png | 3 --- ..._DefaultGroup_InitialsAvatarPreview_,NEXUS_5,1,dark,en].png | 3 --- ...aultGroup_ConfirmationDialogPreview_,NEXUS_5,1,dark,en].png | 3 --- ...ultGroup_ConfirmationDialogPreview_,NEXUS_5,1,light,en].png | 3 --- ...ull_DefaultGroup_ErrorDialogPreview_,NEXUS_5,1,dark,en].png | 3 --- ...ll_DefaultGroup_ErrorDialogPreview_,NEXUS_5,1,light,en].png | 3 --- ...aultGroup_PreferenceCategoryPreview_,NEXUS_5,1,dark,en].png | 3 --- ...DefaultGroup_PreferenceSlidePreview_,NEXUS_5,1,dark,en].png | 3 --- ...efaultGroup_PreferenceSwitchPreview_,NEXUS_5,1,dark,en].png | 3 --- ..._DefaultGroup_PreferenceTextPreview_,NEXUS_5,1,dark,en].png | 3 --- ...efaultGroup_LabelledCheckboxPreview_,NEXUS_5,1,dark,en].png | 3 --- ..._DefaultGroup_ProgressDialogPreview_,NEXUS_5,1,dark,en].png | 3 --- ...DefaultGroup_ProgressDialogPreview_,NEXUS_5,1,light,en].png | 3 --- ...ultGroup_ChangeServerContentPreview_,NEXUS_5,1,dark,en].png | 3 --- ...ll_DefaultGroup_LoginContentPreview_,NEXUS_5,1,dark,en].png | 3 --- ...l_DefaultGroup_LogoutContentPreview_,NEXUS_5,1,dark,en].png | 3 --- ...DefaultGroup_TimelineItemsPreview__0,NEXUS_5,1,dark,en].png | 3 --- ...DefaultGroup_TimelineItemsPreview__2,NEXUS_5,1,dark,en].png | 3 --- ...DefaultGroup_TimelineItemsPreview__4,NEXUS_5,1,dark,en].png | 3 --- ...aultGroup_PreferencesContentPreview_,NEXUS_5,1,dark,en].png | 3 --- ...ultGroup_PreferencesContentPreview_,NEXUS_5,1,light,en].png | 3 --- ...efaultGroup_BugReportContentPreview_,NEXUS_5,1,dark,en].png | 3 --- ...tGroup_CrashDetectionContentPreview_,NEXUS_5,1,dark,en].png | 3 --- ...Group_CrashDetectionContentPreview_,NEXUS_5,1,light,en].png | 3 --- ...Group_RageshakeDialogContentPreview_,NEXUS_5,1,dark,en].png | 3 --- ...roup_RageshakeDialogContentPreview_,NEXUS_5,1,light,en].png | 3 --- ...eshakePreferenceNotSupportedPreview_,NEXUS_5,1,dark,en].png | 3 --- ...oup_RageshakePreferencesViewPreview_,NEXUS_5,1,dark,en].png | 3 --- ...ll_DefaultGroup_RoomListViewPreview_,NEXUS_5,1,dark,en].png | 3 --- ...l_DefaultGroup_RoomListViewPreview_,NEXUS_5,1,light,en].png | 3 --- ...sts_null_Buttons_Showkasebutton_null,NEXUS_5,1,dark,en].png | 3 --- 113 files changed, 153 insertions(+), 105 deletions(-) rename 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 => io.element.android.tests.uitests_ScreenshotTest_preview_tests[Color_Material Design_DarkGrey,NEXUS_5,1,en].png} (100%) rename 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 => io.element.android.tests.uitests_ScreenshotTest_preview_tests[Color_Material Design_LightGrey,NEXUS_5,1,en].png} (100%) rename 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 => io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_Body Large,NEXUS_5,1,en].png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_Body1,NEXUS_5,1,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_BodySmall,NEXUS_5,1,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_H1,NEXUS_5,1,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_Headline Large,NEXUS_5,1,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_Headline Medium,NEXUS_5,1,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_Headline Small,NEXUS_5,1,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_bodyMedium,NEXUS_5,1,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_titleMedium,NEXUS_5,1,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[Typo_Element_titleSmall,NEXUS_5,1,en].png create mode 100644 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 rename 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 => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_,NEXUS_5,1,en].png} (100%) create mode 100644 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 rename 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 => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.root_null_DefaultGroup_LoginRootScreenLightPreview_,NEXUS_5,1,en].png} (100%) create mode 100644 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 rename 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 => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.logout_null_DefaultGroup_LogoutPreferenceViewLightPreview_,NEXUS_5,1,en].png} (100%) create mode 100644 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 create mode 100644 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 create mode 100644 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 rename 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 => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenDarkPreview__1,NEXUS_5,1,en].png} (100%) create mode 100644 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 rename 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 => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenDarkPreview__3,NEXUS_5,1,en].png} (100%) create mode 100644 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 rename 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 => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenDarkPreview__5,NEXUS_5,1,en].png} (100%) rename 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 => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__0,NEXUS_5,1,en].png} (100%) rename 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 => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__1,NEXUS_5,1,en].png} (100%) rename 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 => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__2,NEXUS_5,1,en].png} (100%) rename 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 => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__3,NEXUS_5,1,en].png} (100%) rename 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 => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__4,NEXUS_5,1,en].png} (100%) rename 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 => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__5,NEXUS_5,1,en].png} (100%) create mode 100644 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 create mode 100644 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 create mode 100644 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 rename 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 => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.bugreport_null_DefaultGroup_BugReportViewLightPreview_,NEXUS_5,1,en].png} (100%) create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 rename 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 => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.preferences_null_DefaultGroup_RageshakePreferencesViewLightPreview_,NEXUS_5,1,en].png} (100%) create mode 100644 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 rename 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 => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.preferences_null_DefaultGroup_RageshakePreferencesViewNotSupportedLightPreview_,NEXUS_5,1,en].png} (100%) create mode 100644 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 create mode 100644 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 create mode 100644 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 rename 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 => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.avatar_null_DefaultGroup_AvatarLightPreview_,NEXUS_5,1,en].png} (100%) create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 rename 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 => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_DefaultGroup_PreferenceCategoryLightPreview_,NEXUS_5,1,en].png} (100%) create mode 100644 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 rename 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 => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_DefaultGroup_PreferenceSlideLightPreview_,NEXUS_5,1,en].png} (100%) create mode 100644 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 rename 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 => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_DefaultGroup_PreferenceSwitchLightPreview_,NEXUS_5,1,en].png} (100%) create mode 100644 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 rename 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 => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_DefaultGroup_PreferenceTextLightPreview_,NEXUS_5,1,en].png} (100%) rename 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 => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_DefaultGroup_PreferenceViewDarkPreview_,NEXUS_5,1,en].png} (100%) rename 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 => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.preferences_null_DefaultGroup_PreferenceViewLightPreview_,NEXUS_5,1,en].png} (100%) create mode 100644 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 rename 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 => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components_null_DefaultGroup_LabelledCheckboxLightPreview_,NEXUS_5,1,en].png} (100%) create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 rename 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 => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.tests.uitests_null_DefaultGroup_ShowkaseButtonLightPreview_null,NEXUS_5,1,en].png} (100%) delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 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..ad57707ebb --- /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:5151b9d544a6e50a3c3f2993d33e494ce3779c05802f05542706ebd10ba38373 +size 7245 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..5ea5c861b4 --- /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:6b49b04090d8da089c170c82f864de5bf0edca49a3db38367aa0b2d0f42147e8 +size 31658 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.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenDarkPreview__1,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.messages.timeline_null_DefaultGroup_TimelineItemsPreview__1,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.features.messages.timeline_null_DefaultGroup_LoginRootScreenDarkPreview__1,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_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..4fe710e166 --- /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:a0af0dec6058708a9e8ef643bfc9401d29be31808b9882fdc112b52da240103b +size 33508 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.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenDarkPreview__3,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.messages.timeline_null_DefaultGroup_TimelineItemsPreview__3,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.features.messages.timeline_null_DefaultGroup_LoginRootScreenDarkPreview__3,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_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..8566bb320b --- /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:c9607ad7cab7d64ef9eafdcaa33090b2475b69a742fd80b10327e8506248edf8 +size 29598 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.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.timeline_null_DefaultGroup_LoginRootScreenDarkPreview__5,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.messages.timeline_null_DefaultGroup_TimelineItemsPreview__5,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.features.messages.timeline_null_DefaultGroup_LoginRootScreenDarkPreview__5,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.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__0,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.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__0,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.messages.timeline_null_DefaultGroup_TimelineItemsPreview__0,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.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__0,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.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__1,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.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__1,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.messages.timeline_null_DefaultGroup_TimelineItemsPreview__1,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.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__1,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.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__2,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.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__2,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.messages.timeline_null_DefaultGroup_TimelineItemsPreview__2,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.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__2,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.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__3,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.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__3,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.messages.timeline_null_DefaultGroup_TimelineItemsPreview__3,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.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__3,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.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__4,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.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__4,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.messages.timeline_null_DefaultGroup_TimelineItemsPreview__4,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.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__4,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.features.messages.timeline_null_DefaultGroup_TimelineItemsPreview__5,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.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__5,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.messages.timeline_null_DefaultGroup_TimelineItemsPreview__5,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.messages.timeline_null_DefaultGroup_LoginRootScreenLightPreview__5,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.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..f305574fe8 --- /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:2672387b0b02aee24ecdef31db43665b08b5660209d25b80da7286bc9517a60e +size 18727 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..bd30d0b256 --- /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:1f37a8d115ca1253fb09505bc09cb14d7623d90cfba87940f53ff9f8033f724b +size 16614 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..387b8dcbeb --- /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:4c470aa6515fa5b7564805ccffecbb5c0632d690ea9311868b81f0456e7e0e19 +size 13138 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.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__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__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.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 From 426005f29869fae0acef205067658d32da0e3ed5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 3 Feb 2023 13:39:43 +0100 Subject: [PATCH 31/96] Fix detekt issue, optimize import, fix color of preference category title --- .../main/kotlin/io/element/android/x/di/AppBindings.kt | 1 - .../android/features/login/root/LoginRootScreen.kt | 1 - .../android/features/messages/timeline/TimelineView.kt | 9 ++++----- .../designsystem/{Type.kt => ElementTextStyles.kt} | 0 .../components/preferences/PreferenceCategory.kt | 3 ++- .../components/preferences/PreferenceSwitch.kt | 1 - .../android/libraries/designsystem/theme/ColorAliases.kt | 2 +- .../designsystem/theme/components/OutlinedTextField.kt | 3 --- .../libraries/designsystem/theme/components/Text.kt | 4 +++- .../libraries/designsystem/theme/components/TextField.kt | 2 -- .../libraries/matrix/room/RoomSummaryDataSource.kt | 2 -- ...up_LogoutPreferenceViewDarkPreview_,NEXUS_5,1,en].png | 4 ++-- ...Group_LoginRootScreenDarkPreview__0,NEXUS_5,1,en].png | 4 ++-- ...Group_LoginRootScreenDarkPreview__1,NEXUS_5,1,en].png | 4 ++-- ...Group_LoginRootScreenDarkPreview__2,NEXUS_5,1,en].png | 4 ++-- ...Group_LoginRootScreenDarkPreview__3,NEXUS_5,1,en].png | 4 ++-- ...Group_LoginRootScreenDarkPreview__4,NEXUS_5,1,en].png | 4 ++-- ...Group_LoginRootScreenDarkPreview__5,NEXUS_5,1,en].png | 4 ++-- ...roup_LoginRootScreenLightPreview__0,NEXUS_5,1,en].png | 4 ++-- ...roup_LoginRootScreenLightPreview__1,NEXUS_5,1,en].png | 4 ++-- ...roup_LoginRootScreenLightPreview__2,NEXUS_5,1,en].png | 4 ++-- ...roup_LoginRootScreenLightPreview__3,NEXUS_5,1,en].png | 4 ++-- ...roup_LoginRootScreenLightPreview__4,NEXUS_5,1,en].png | 4 ++-- ...roup_LoginRootScreenLightPreview__5,NEXUS_5,1,en].png | 4 ++-- ...ageshakePreferencesViewDarkPreview_,NEXUS_5,1,en].png | 4 ++-- ...erencesViewNotSupportedDarkPreview_,NEXUS_5,1,en].png | 4 ++-- ...roup_PreferenceCategoryDarkPreview_,NEXUS_5,1,en].png | 4 ++-- tools/detekt/detekt.yml | 4 ++-- 28 files changed, 44 insertions(+), 52 deletions(-) rename libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/{Type.kt => ElementTextStyles.kt} (100%) 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/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 2d5381f99c..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 @@ -52,7 +52,6 @@ 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.ElementTheme 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 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 91b9cc6023..a6d6d8a2af 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 @@ -77,7 +77,6 @@ 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.ElementTheme import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.FloatingActionButton import io.element.android.libraries.designsystem.theme.components.Icon @@ -377,7 +376,7 @@ private fun ContentToPreview(content: TimelineItemContent) { createMessageEvent( isMine = false, content = content, - groupPosition = MessagesItemGroupPosition.First + groupPosition = MessagesItemGroupPosition.Last ), createMessageEvent( isMine = false, @@ -387,13 +386,13 @@ private fun ContentToPreview(content: TimelineItemContent) { 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, @@ -403,7 +402,7 @@ private fun ContentToPreview(content: TimelineItemContent) { createMessageEvent( isMine = true, content = content, - groupPosition = MessagesItemGroupPosition.Last + groupPosition = MessagesItemGroupPosition.First ), ) TimelineView( 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 100% 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 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 eeed206e5f..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 @@ -51,7 +51,8 @@ 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() } 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 7a4177a5ae..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 @@ -33,7 +33,6 @@ 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.ElementTheme 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 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 index 33c4032dc9..8e7b029453 100644 --- 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 @@ -22,7 +22,7 @@ import io.element.android.libraries.designsystem.SystemGrey4Dark import io.element.android.libraries.designsystem.SystemGrey6Light /** - * Room list + * Room list. */ @Composable fun MaterialTheme.roomListRoomName() = colorScheme.primary 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 index fae6c06328..6e0cc157fe 100644 --- 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 @@ -20,7 +20,6 @@ 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.foundation.text.selection.TextSelectionColors import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.TextFieldColors @@ -28,12 +27,10 @@ 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.Color 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.ElementGreen import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight 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 index 6d7a255d4b..ff74db3b81 100644 --- 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 @@ -31,6 +31,8 @@ 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.unit.TextUnit +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.persistentMapOf @Composable fun Text( @@ -87,7 +89,7 @@ fun Text( overflow: TextOverflow = TextOverflow.Clip, softWrap: Boolean = true, maxLines: Int = Int.MAX_VALUE, - inlineContent: Map = mapOf(), + inlineContent: ImmutableMap = persistentMapOf(), onTextLayout: (TextLayoutResult) -> Unit = {}, style: TextStyle = LocalTextStyle.current ) { 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 index dba39fccf1..27995b46af 100644 --- 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 @@ -19,7 +19,6 @@ 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.material.ContentAlpha import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.TextFieldColors @@ -27,7 +26,6 @@ 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.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.VisualTransformation diff --git a/libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/room/RoomSummaryDataSource.kt b/libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/room/RoomSummaryDataSource.kt index 11ae71795d..8d3ead328d 100644 --- a/libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/room/RoomSummaryDataSource.kt +++ b/libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/room/RoomSummaryDataSource.kt @@ -20,7 +20,6 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.sync.roomListDiff import io.element.android.libraries.matrix.sync.state import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.cancelChildren @@ -29,7 +28,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.RoomListEntry 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 index ad57707ebb..92c0633a91 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5151b9d544a6e50a3c3f2993d33e494ce3779c05802f05542706ebd10ba38373 -size 7245 +oid sha256:155e39fcadcc8bc039267a8b0dca338a92bcb3b4b23ab55a0cd9f3ef91c729fc +size 8888 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 index 5ea5c861b4..0aa521f943 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b49b04090d8da089c170c82f864de5bf0edca49a3db38367aa0b2d0f42147e8 -size 31658 +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 index 6dfe58cbdc..8548e31a8c 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1be16bc1a73ef5bb7af740b8720c508dbc202d4748d60fb2e10fabcfdeacc75c -size 43668 +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 index 4fe710e166..556e55dcc1 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0af0dec6058708a9e8ef643bfc9401d29be31808b9882fdc112b52da240103b -size 33508 +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 index b4dc232172..ea261625d8 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ea0809dcddfc5df7aaba26f4ed74bebe404c86f5923c78541518fb7560e897e -size 55699 +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 index 8566bb320b..a23377387a 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9607ad7cab7d64ef9eafdcaa33090b2475b69a742fd80b10327e8506248edf8 -size 29598 +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 index bf5d223a28..47a852803b 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ab7cb0da1b64d2c6d87578debbc336feedbe203f6331726503a084101b9a10e6 -size 51186 +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 index 54c51dda5c..3d1094d520 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b7d3c2cf8d466f6e25404596079fb6254ad6e9fb3ba3f0f72ef1c0ba019dc60 -size 31165 +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 index 7b8a310f6f..77adf1dcdc 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02c410850ed4b8658a21c4a60b62757baa9799d4c6dee4d53d2e6b2ca76f7b61 -size 41942 +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 index 7344b3e35e..9509ba2522 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a0ea920c88ab127513918055621a75528de35d69296727a8be7368df4c848e9 -size 33040 +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 index 89d55c25a3..88aa21ce03 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc36b26cbaab9ee19c898a27bc5bc0a04c77e4004933c47fc69608df103e08d9 -size 52964 +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 index 1d930443fe..68a59311cc 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0500e499fb319057b0ca5feb9db9e429266a2832521b429d502d1f62804ee23f -size 29222 +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 index 0a01c81f08..8d8bc4078e 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5535df19747bdec380b79eec8b9d8e2d7e6ed765fe0bb4e4c1c2ffeb4de46854 -size 49115 +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.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 index f305574fe8..1d080b2c8b 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2672387b0b02aee24ecdef31db43665b08b5660209d25b80da7286bc9517a60e -size 18727 +oid sha256:5c385c5b124217368b054e85e8313a0dac30274ea6fc08f9a71f60e80f547df0 +size 22589 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 index bd30d0b256..c408edcdc4 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f37a8d115ca1253fb09505bc09cb14d7623d90cfba87940f53ff9f8033f724b -size 16614 +oid sha256:eac040a95a0e551d61fb0a14c26190304ac97b8a575a0911812f3fdade88f0e6 +size 21014 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 index 387b8dcbeb..a8a3738f0e 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c470aa6515fa5b7564805ccffecbb5c0632d690ea9311868b81f0456e7e0e19 -size 13138 +oid sha256:5f67cd11f58cba1e7de19aab0c35ca0638b4c0f0dee373d4fc7155e903285e62 +size 15843 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: From 8413b9792fd417c0301b34f8f2ff4b1e5bb10749 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 3 Feb 2023 16:21:53 +0100 Subject: [PATCH 32/96] Add preview for colors. --- .../designsystem/theme/ColorAliases.kt | 28 ++++++++ .../designsystem/theme/ColorsDark.kt | 11 +++ .../designsystem/theme/ColorsLight.kt | 11 +++ .../designsystem/theme/components/Text.kt | 63 +++++++++++++++++ .../theme/previews/ColorListPreview.kt | 47 +++++++++++++ .../theme/previews/ColorPreview.kt | 64 +++++++++++++++++ .../theme/previews/ColorsSchemePreview.kt | 68 +++++++++++++++++++ .../libraries/designsystem/utils/Colors.kt | 26 +++++++ ...ltGroup_TextDarkPreview_,NEXUS_5,1,en].png | 3 + ...tGroup_TextLightPreview_,NEXUS_5,1,en].png | 3 + ...ColorAliasesDarkPreview_,NEXUS_5,1,en].png | 3 + ...olorAliasesLightPreview_,NEXUS_5,1,en].png | 3 + ...ColorsSchemePreviewDark_,NEXUS_5,1,en].png | 3 + ...olorsSchemePreviewLight_,NEXUS_5,1,en].png | 3 + 14 files changed, 336 insertions(+) create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/previews/ColorListPreview.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/previews/ColorPreview.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/previews/ColorsSchemePreview.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Colors.kt create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 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 index 8e7b029453..5fd5c3f0af 100644 --- 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 @@ -18,8 +18,13 @@ 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 /** * Room list. @@ -38,3 +43,26 @@ 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 = mapOf( + "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 index ff02755081..774158856e 100644 --- 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 @@ -17,11 +17,14 @@ 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, @@ -62,3 +65,11 @@ val materialColorSchemeDark = darkColorScheme( // 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 index 43932edd10..dc13718360 100644 --- 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 @@ -17,11 +17,14 @@ 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, @@ -62,3 +65,11 @@ val materialColorSchemeLight = lightColorScheme( // 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/components/Text.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Text.kt index ff74db3b81..2e05f2a793 100644 --- 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 @@ -16,8 +16,19 @@ 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 @@ -30,7 +41,12 @@ 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 @@ -113,3 +129,50 @@ fun Text( 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/previews/ColorListPreview.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/previews/ColorListPreview.kt new file mode 100644 index 0000000000..3d728c0ad4 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/previews/ColorListPreview.kt @@ -0,0 +1,47 @@ +/* + * 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 + +@Composable +internal fun ColorListPreview( + backgroundColor: Color, + foregroundColor: Color, + colors: Map, + 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..79bdb0b35f --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/previews/ColorsSchemePreview.kt @@ -0,0 +1,68 @@ +/* + * 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 + +@Composable +internal fun ColorsSchemePreview( + backgroundColor: Color, + foregroundColor: Color, + colorScheme: ColorScheme, + modifier: Modifier = Modifier, +) { + val colors = mapOf( + "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/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Colors.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Colors.kt new file mode 100644 index 0000000000..42723cbb54 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Colors.kt @@ -0,0 +1,26 @@ +/* + * 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.utils + +import androidx.compose.ui.graphics.Color + +/** + * Convert color to Human Readable Format. + */ +internal fun Color.toHrf(): String { + return "0x" + value.toString(16).take(8).uppercase() +} 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 From a8f83d7e020fc87626a351f40e4b94c413bdd6b1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 3 Feb 2023 16:44:13 +0100 Subject: [PATCH 33/96] Remove unnecessary files. --- .idea/.gitignore | 3 - .idea/.name | 1 - .idea/codeStyles/Project.xml | 125 ------------------- .idea/codeStyles/codeStyleConfig.xml | 5 - .idea/compiler.xml | 8 -- .idea/inspectionProfiles/Project_Default.xml | 37 ------ .idea/jarRepositories.xml | 25 ---- .idea/misc.xml | 10 -- 8 files changed, 214 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/.name delete mode 100644 .idea/codeStyles/Project.xml delete mode 100644 .idea/codeStyles/codeStyleConfig.xml delete mode 100644 .idea/compiler.xml delete mode 100644 .idea/inspectionProfiles/Project_Default.xml delete mode 100644 .idea/jarRepositories.xml delete mode 100644 .idea/misc.xml 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/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 From 22b81072c226309cae0899fd7a4a82e76c60c0b2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 3 Feb 2023 16:47:48 +0100 Subject: [PATCH 34/96] Ignore .idea files. --- .gitignore | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) 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. From bebba63dc94ff2f7425572438f43741dd4f459a4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 3 Feb 2023 16:48:13 +0100 Subject: [PATCH 35/96] Add project icon. --- .idea/icon.png | Bin 0 -> 14783 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .idea/icon.png diff --git a/.idea/icon.png b/.idea/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6f7872211b0f603c5340b3d0d2eae6c878e89233 GIT binary patch literal 14783 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 m^)TQraD%6$?{oGQ*iN4;K3w;V0r Date: Fri, 3 Feb 2023 17:18:06 +0100 Subject: [PATCH 36/96] Add some screenshots --- README.md | 9 +++++++++ docs/images/screen1.png | Bin 0 -> 128145 bytes docs/images/screen2.png | Bin 0 -> 65911 bytes docs/images/screen3.png | Bin 0 -> 325181 bytes docs/images/screen4.png | Bin 0 -> 210218 bytes 5 files changed, 9 insertions(+) create mode 100644 docs/images/screen1.png create mode 100644 docs/images/screen2.png create mode 100644 docs/images/screen3.png create mode 100644 docs/images/screen4.png diff --git a/README.md b/README.md index 6406ffbb6b..ad46419490 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ The application is a total rewrite of [Element-Android](https://github.com/vecto +* [Screenshots](#screenshots) * [Rust SDK](#rust-sdk) * [Roadmap](#roadmap) * [Contributing](#contributing) @@ -23,6 +24,14 @@ The application is a total rewrite of [Element-Android](https://github.com/vecto +## Screenshots + +Here are some early screenshots the application: + +|-|-|-|-| +||||| +|-|-|-|-| + ## 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. diff --git a/docs/images/screen1.png b/docs/images/screen1.png new file mode 100644 index 0000000000000000000000000000000000000000..9f9d7747ff34512382cab30c1800a9bd7a26ce6b GIT binary patch literal 128145 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�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�Y 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 R2W}TIJ$?RE(Fy0@{~vpnLG%Cs literal 0 HcmV?d00001 diff --git a/docs/images/screen3.png b/docs/images/screen3.png new file mode 100644 index 0000000000000000000000000000000000000000..3edb49d08651cc8fbc1d356f0051ab67e0fc0593 GIT binary patch literal 325181 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μ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 < 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 z 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 z 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 Wxo*64yUbp2((rWkb6Mw<&;$VG1r7oL literal 0 HcmV?d00001 diff --git a/docs/images/screen4.png b/docs/images/screen4.png new file mode 100644 index 0000000000000000000000000000000000000000..53da801a1b2a5a7120a798b89bb94c984123e235 GIT binary patch literal 210218 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 QtCKXK+>=V{Nc3YXU)*$Yp&37U!)Yg=jfw_(Lh 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� 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 Date: Fri, 3 Feb 2023 17:21:39 +0100 Subject: [PATCH 37/96] Second try. --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index ad46419490..8963525631 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,8 @@ The application is a total rewrite of [Element-Android](https://github.com/vecto ## Screenshots -Here are some early screenshots the application: +Here are some early screenshots of the application: -|-|-|-|-| ||||| |-|-|-|-| From b8a237303d26574e65cbbdb2ca497b122babac7b Mon Sep 17 00:00:00 2001 From: bmarty Date: Sat, 4 Feb 2023 06:06:06 +0000 Subject: [PATCH 38/96] Import strings from Element Android --- .../src/main/res/values-cs/strings.xml | 1 + .../src/main/res/values-de/strings.xml | 1 + .../src/main/res/values-et/strings.xml | 7 +- .../src/main/res/values-fa/strings.xml | 1 + .../src/main/res/values-fr/strings.xml | 1 + .../src/main/res/values-hu/strings.xml | 1 + .../src/main/res/values-in/strings.xml | 1 + .../src/main/res/values-it/strings.xml | 4 +- .../src/main/res/values-ja/strings.xml | 466 ++++++++++++++---- .../src/main/res/values-pl/strings.xml | 2 +- .../src/main/res/values-ru/strings.xml | 2 +- .../src/main/res/values-sk/strings.xml | 1 + .../src/main/res/values-sq/strings.xml | 21 +- .../src/main/res/values-sv/strings.xml | 23 +- .../src/main/res/values-uk/strings.xml | 1 + .../src/main/res/values-zh-rTW/strings.xml | 6 +- 16 files changed, 427 insertions(+), 112 deletions(-) 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-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-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..54a8c8ee0f 100644 --- a/libraries/ui-strings/src/main/res/values-ja/strings.xml +++ b/libraries/ui-strings/src/main/res/values-ja/strings.xml @@ -1,5 +1,5 @@ - + %sの招待 %1$sが%2$sを招待しました %1$sがあなたを招待しました @@ -23,7 +23,7 @@ ルームへの招待 %1$sと%2$s 空のルーム - %1$sが今後のルーム履歴を%2$sに見えるように設定しました。 + %1$sが今後のルーム履歴を「%2$s」閲覧可能に設定しました。 ルームのメンバー全員(招待された時点から) ルームのメンバー全員(参加した時点から) ルームのメンバー全員 @@ -76,7 +76,7 @@ すぐに返信 開く 閉じる - クリップボードへコピー + クリップボードにコピーしました 警告 お気に入り メンバー @@ -93,7 +93,7 @@ ログを送信 開発者が問題を診断するために、このクライアントのログがバグレポートと一緒に送信されます。バグレポートは、ログとスクリーンショットを含めて、公開されることはありません。上記の説明文だけを送信したい場合は、以下のチェックを解除してください。 あなたは不満で端末を振っているようです。バグレポートの画面を開きますか? - 前回、アプリケーションは正常に停止しませんでした。クラッシュ報告の画面を開きますか? + 前回アプリケーションは正常に停止しませんでした。クラッシュ報告の画面を開きますか? 不具合を報告しました 不具合の報告の送信に失敗しました (%s) ルームに参加 @@ -105,7 +105,7 @@ メールアドレスの形式が正しくありません このメールアドレスは既に登録されています。 パスワードを忘れましたか? - 正しいURLを入力して下さい + 正しいURLを入力してください 原寸 大き目 中程度 @@ -115,12 +115,12 @@ いいえ 続行する 最新の未読へ移動 - ルームを退出 - このルームを退出してよろしいですか? + ルームから退出 + このルームから退出してよろしいですか? 招待 - %sさんが文字を入力しています… - %1$sさんと%2$sさんが文字を入力しています… - %1$sさん、%2$sさん他が文字を入力しています… + %sさんが入力しています… + %1$sさんと%2$sさんが入力しています… + %1$s、%2$s他が入力しています… 明るいテーマ 暗いテーマ 黒いテーマ @@ -134,7 +134,7 @@ 削除 参加 このルームで発言する権限がありません。 - 自分のアイコン画像 + プロフィール画像 表示名 メールアドレスを追加 電話番号を追加 @@ -144,7 +144,7 @@ 1対1のチャットでのメッセージ グループチャットでのメッセージ ルームへ招待されたとき - 通話の呼び出しがあったとき + 通話への招待 自動発言プログラム(Bot)が発言した時 端末起動時に開始 アプリを閉じているときの動作 @@ -247,9 +247,7 @@ 現在のパスワード 新しいパスワード パスワードの更新に失敗しました - %sの全てのメッセージを表示しますか? -\n -\nこの操作はアプリを再起動するため、時間がかかる場合があります。 + %sの全てのメッセージを表示しますか? 外観 公開端末名 ルームのエンドツーエンド暗号鍵をエクスポート @@ -278,9 +276,9 @@ リクエストの送信に失敗しました。 ウィジェットを作成できません。 ウィジェットをこのルームから削除してもよろしいですか? - 一致していない場合は、コミュニケーションのセキュリティーが破られている可能性があります。 + 一致していない場合は、コミュニケーションのセキュリティーが損なわれている可能性があります。 このセッションでは、未認証のセッションに対して暗号化されたメッセージを送信しない。 - 認証済のセッションに対してのみ暗号化 + 認証済のセッションにのみ暗号化 インポート ローカルファイルから鍵をインポート ルームの暗号鍵をインポート @@ -294,7 +292,7 @@ 通知あり(音量大) 通知あり(サイレント) 不具合の報告 - このユーザーにあなたと同じ権限を与えます。この変更は取り消せません。 + このユーザーにあなたと同じ権限レベルを与えようとしています。この変更は取り消せません。 \nよろしいですか? 信用する 信用しない @@ -314,14 +312,14 @@ olmのバージョン サードパーティーの使用に関する掲示 ホーム画面 - 逃した通知があるルームを固定 - 未読のあるルームを固定 + 逃した通知があるルームをピン止め + 未読メッセージがあるルームをピン止め 分析 復号エラー セッションキー 未認証 - 認証する - 他のセッションのユーザー設定で、以下を比較して確認してください: + 認証 + 他のセッションのユーザー設定で、以下を比較して承認してください: ルームのディレクトリを選択 サーバー名 %sサーバー上の全てのルーム @@ -399,7 +397,7 @@ 指定したIDのユーザーの管理者権限を取り消す 指定したユーザーを現在のルームに招待 指定されたアドレスのルームに参加 - ルームを退室 + ルームから退出 ルームの説明を設定 指定したIDのユーザーをこのルームから追放 表示するニックネームを変更 @@ -409,7 +407,7 @@ エラー 今すぐ確認 アカウントを停止 - この操作により、あなたのアカウントは永久に使えなくなります。あなたはログインできなくなり、誰も同じユーザーIDを再登録できなくなります。アカウントが参加している全てのルームを退出し、IDサーバーからアカウントの詳細は削除されます。 この操作は取り消せません。 + この操作により、あなたのアカウントは永久に使えなくなります。あなたはログインできなくなり、誰も同じユーザーIDを再登録できなくなります。アカウントが参加している全てのルームから退出し、IDサーバーからアカウントの詳細は削除されます。 この操作は取り消せません。 \n \nアカウントを停止しても、 デフォルトではあなたが送信したメッセージの履歴は消去されません。メッセージの履歴の消去を望む場合は、以下のボックスにチェックを入れてください。 \n @@ -420,18 +418,18 @@ このルームは置き換えられており、アクティブではありません。 こちらから継続中の会話を確認 このルームは別の会話の続きです - 以前のメッセージを見るには、ここをクリックしてください + 以前のメッセージを表示するには、ここをクリックしてください サービス管理者に連絡してください このホームサーバーはリソース制限の1つを超過しているため、 ユーザーがログインできなくなることがあります - このホームサーバーはリソース制限の1つを超過しています。 + このホームサーバーはリソースの上限に達しました。 このホームサーバーは月間アクティブユーザーの上限に達しているため、 ユーザーがログインできなくなることがあります - このホームサーバーは月間アクティブユーザーの上限に達しています。 + このホームサーバーは月間アクティブユーザー数の上限に達しました 。 この制限を上げるには、%sしてください。 - このサービスを使い続けるには、%sしてください。 + このサービスの使用を継続するには、%sしてください。 申し訳ありません、エラーが発生しました エクスポートされた鍵を暗号化するパスフレーズを作成してください。 鍵をインポートするには、同一のパスフレーズを入力する必要があります。 パスフレーズの作成 - パスフレーズが一致していません + パスフレーズが一致しません %1$s:%2$s %d+ 展開 @@ -465,7 +463,7 @@ パスワード パスワード 今ここでサインアウトすると、あなたの暗号化されたメッセージは失われてしまいます - 鍵のバックアップは現在処理中です。処理中にサインアウトすると、暗号化されたメッセージにアクセスできなくなります。 + 鍵をバックアップしています。処理中にサインアウトすると、暗号化されたメッセージにアクセスできなくなります。 暗号化されたメッセージにアクセスできなくなることを防ぐため、鍵の安全なバックアップはあなたのセッション全てで有効化してください。 暗号化されたメッセージは不要です 鍵をバックアップしています… @@ -485,13 +483,13 @@ 署名 通知に関する問題の解決 システム設定。 - アカウント設定。 + アカウントの設定。 カスタム設定。 起動時に実行 バックグラウンド制限の確認 編集 返信 - メッセージが削除されました + メッセージを削除しました 削除済のメッセージを表示 削除されたメッセージに関する通知を表示 ユーザーによって削除されたイベント @@ -535,7 +533,7 @@ %1$d人の参加者 アップロード - ルームを退出 + ルームから退出 ルームから退室しています… 管理者 モデレーター @@ -560,8 +558,8 @@ クロス署名は無効です 有効なセッション 全てのセッションを表示 - セッションの管理 - このセッションからログアウト + セッションを管理 + このセッションからサインアウト %d件のアクティブなセッション @@ -585,7 +583,7 @@ コピー 成功 通知 - 破棄 + 取り消す 再生 閉じる スキップ @@ -616,7 +614,7 @@ マークダウン書式 メッセージ送信前にマークダウン書式を適用します。これにより、アスタリスクを使用して斜体のテキストを表示するなどの高度な書式設定が利用できます。 音声とビデオ - 国際電話番号形式で入力してください(電話番号の最初に「+」が付きます) + 国際電話番号形式で入力してください(電話番号の最初に「+」を付けてください) メールアドレス あなたのアカウントに追加されたメールアドレスはありません あなたのアカウントに追加された電話番号はありません @@ -678,7 +676,7 @@ サウンドデバイスを選択 リアルタイム接続を確立できませんでした。 \nホームサーバーの管理者に、通話が正常に動作するためにTURNを設定するようご連絡ください。 - 終了 + 電話を切る 拒否 承諾 ウィジェットを削除できませんでした @@ -691,8 +689,8 @@ なし トピック ルーム名 - このルーム内のメッセージはエンドツーエンド暗号化されていません。 - ここでのメッセージはエンドツーエンド暗号化されていません。 + このルーム内のメッセージはエンドツーエンドで暗号化されていません。 + ここでのメッセージはエンドツーエンドで暗号化されていません。 設定 あなたにはこのルームの暗号化を有効にする権限がありません。 未読メッセージ @@ -700,7 +698,7 @@ タイムラインで非表示のイベントを表示 QRコードをスキャン QRコード - QRコードによる追加 + QRコードで追加 コードを共有 ${app_name}で話しましょう:%s 友達を招待 @@ -775,8 +773,8 @@ このルームは公開されていません。 招待がなければ再び参加することはできません。 連絡先へのアクセスを許可します。 QRコードをスキャンするには、カメラへのアクセスを許可する必要があります。 - 通話をかけました - %sが通話をかけました + 通話を保留しました + %sが通話を保留しました 保留 通話をやり直す ビデオ通話が行われています… @@ -858,9 +856,9 @@ %sがこのルームのサーバーアクセス制御リストを設定しました。 %sがここをアップグレードしました。 %sがこのルームをアップグレードしました。 - 今後のメッセージを%1$sに見えるように設定しました。 - 今後のルーム履歴を%1$sに見えるように設定しました。 - %1$sが今後のメッセージを%2$sに見えるように設定しました。 + 今後のメッセージを「%1$s」閲覧可能に設定しました。 + 今後のルーム履歴を「%1$s」閲覧可能に設定しました。 + %1$sが今後のメッセージを「%2$s」閲覧可能に設定しました。 %sが通話を設定するためにデータを送信しました。 通話を開始しました。 ビデオ通話を開始しました。 @@ -874,7 +872,7 @@ %1$sが招待を拒否しました。理由:%2$s 退出しました。理由:%1$s %1$sが退出しました。理由:%2$s - このルームを退出しました。理由:%1$s + このルームから退出しました。理由:%1$s 初期同期: \n退出したルームをインポートしています 初期同期: @@ -882,7 +880,7 @@ 初期同期: \n会話を読み込んでいます \n多くのルームに参加している場合、読み込みに時間がかかるかもしれません - %1$sがこのルームを退出しました。理由:%2$s + %1$sがこのルームから退出しました。理由:%2$s このルームに参加しました。理由:%1$s %1$sがこのルームに参加しました。理由:%2$s このルームに参加しました。理由:%1$s @@ -1014,7 +1012,7 @@ ファイルとして保存 共有 完了 - 成功! + 成功しました! バックアップを作成しています パスフレーズを設定 手動で鍵をエクスポート @@ -1076,7 +1074,7 @@ バックアップには認証済のセッション %s による不正な署名があります バックアップには未認証のセッション %s による有効な署名があります バックアップには認証済のセッション %s による署名があります。 - バックアップはこのセッションによる有効な署名があります。 + バックアップにはこのセッションによる有効な署名があります。 バックアップには%sというIDの不明のセッションによる署名があります。 このセッションでは鍵がバックアップされていません。 このセッションでは鍵のバックアップが無効になっています。 @@ -1088,7 +1086,7 @@ %d個のキーが含まれたバックアップを復元しました。 - バックアップが復元されました %s! + バックアップを復元しました %s! このリカバリーキーではバックアップを復号化できませんでした。正しいリカバリーキーを入力したことを確認してください。 リカバリーキーを入力してください 履歴のロックを解除 @@ -1097,11 +1095,11 @@ リカバリーキーを計算しています… バックアップを復元しています: このパスフレーズではバックアップを復号化できませんでした。正しい復旧用のパスフレーズを入力したことを確認してください。 - リカバリーキーを喪失しましたか? 設定で新しいリカバリーキーを設定できます。 + リカバリーキーを無くしましたか? 設定で新しいリカバリーキーを設定できます。 バックアップのバージョンを取得しています… 暗号化されたメッセージ履歴のロックを解除するには、復旧用のパスフレーズを使用してください 復旧用のパスフレーズが分からなければ、%sできます。 - リカバリーキーを使用して、暗号化されたメッセージの履歴のロックを解除 + リカバリーキーを使うと、暗号化されたメッセージの履歴のロックを解除できます リカバリーキーを入力 リカバリーキーを使用 ログアウトしたりこの端末を失くしたりすると、メッセージにアクセスできなくなる可能性があります。 @@ -1161,7 +1159,7 @@ \n%s ウィジェットを再読み込み これを使用すると、クッキーが設定され、データが%sと共有される可能性があります: - ウィジェットの追加者: + ウィジェットを追加した人: **送信に失敗 - ルームを開いてください 新しい招待 %1$sと%2$s @@ -1221,7 +1219,7 @@ ホームサーバーAPIのURL アクセスを取り消す 表示 - このルーム内のメッセージはエンドツーエンド暗号化されています。 + このルームのメッセージはエンドツーエンドで暗号化されています。 ダイレクトメッセージ 新しいダイレクトメッセージを送信 メールアドレス(任意) @@ -1305,7 +1303,7 @@ 現在、このルームにはアクセスできません。 \n後でもう一度やり直すか、ルームの管理者にアクセス権があるかどうかを確認するよう依頼してください。 このルームはプレビューできません - お待ち下さい… + お待ちください… ネットワークがありません。インターネット接続を確認してください。 不正な形式のイベントです。表示できません ルームの管理者によってモデレートされたイベント @@ -1314,8 +1312,8 @@ リアクションを追加 同意 リアクション - ルームがここに表示されます。右下の[+]をタップして、既存のルームを検索するか、独自のルームを開始します。 - ダイレクトメッセージの会話がここに表示されます。右下の[+]をタップして開始します。 + ルームがここに表示されます。右下の+をタップすると、既存のルームを検索するか、自分のルームを開始できます。 + ダイレクトメッセージの会話がここに表示されます。右下の+をタップして開始します。 ルーム 会話 未読メッセージはありません @@ -1336,7 +1334,7 @@ %1$s:%2$s %3$s %1$s:%2$s あなたが知らないかもしれない他のスペースやルーム - 誰がこのルームを検索し、参加できるか決める。 + 誰がこのルームを検索・参加できるか選択してください。 タップしスペースを編集 スペースを選択 アクセス可能なスペース @@ -1363,16 +1361,16 @@ メンションとキーワード 通知のデフォルト - %d個の不在着信(ビデオ) + %d件の不在着信(ビデオ) - %d個の不在着信(音声) + %d件の不在着信(音声) デフォルトで使いもう尋ねない 鍵の共有リクエストの履歴を送信 結果がありません 自分に電話をかけることはできません。参加者が招待を受け入れるまでお待ちください - ミーティングはJitsiのセキュリティーとパーミッションポリシーを使用します。会議中は、現在ルームにいる全ての人に招待状が表示されます。 + ミーティングはJitsiのセキュリティーとパーミッションポリシーを使用します。会議中は、現在ルームにいる全ての人に招待が表示されます。 権限がありません 音声メッセージを送信するには、マイクの権限を許可してください。 この操作を実行するには、システム設定からカメラの権限を許可してください。 @@ -1386,11 +1384,11 @@ %sにメールを送りました。メールを確認してリンクをクリックしてください %sにメールを送りました。メールの確認リンクをクリックしてください 発見可能な電話番号 - IDサーバーとの接続を解除すると、他のユーザーによって発見されなくなり、また、メールアドレスや電話で他のユーザーを招待することができなくなります。 + IDサーバーとの接続を解除すると、他のユーザーによって見つけられなくなり、また、メールアドレスや電話で他のユーザーを招待することもできなくなります。 電話番号を追加すると、発見可能に設定する電話番号を選択できるようになります。 メールアドレスを追加すると、発見可能に設定するメールアドレスを選択できるようになります。 - 現在、IDサーバーを使用していません。あなたの知っている連絡先を発見したり、その連絡先から発見されるようにするには、以下でIDサーバーを設定してください。 - あなたは現在%1$sを使って連絡先を見つけたり、連絡先から見つけられるようにしています。 + 現在、IDサーバーを使用していません。自分の連絡先を見つけたり、連絡先から見つけてもらったりするには、以下でIDサーバーを設定してください。 + 現在%1$sを使って自分の連絡先を見つけたり、連絡先から見つけてもらったりできるようにしています。 IDサーバーを変更 IDサーバーの設定 IDサーバーの切断 @@ -1400,7 +1398,7 @@ 利用規約 編集履歴を表示 提案 - クリップボードにコピーされたリンク + リンクをクリップボードにコピーしました メイン画面に未読通知専用のタブを追加する。 ルーム名を検索 名前もしくはID (#例えば:matrix.org) @@ -1421,7 +1419,7 @@ \n- あなたの端末が使用しているインターネット接続 \n \n設定画面からパスワードとリカバリーキーを早急に変更することを推奨します。 - 電子メール + メールアドレス アドレス 続行する ファイル @@ -1472,10 +1470,10 @@ このルームにファイルはありません このルームにメディアはありません 公開ルームをアップグレード - 非公開スペース + 非公開のスペース 公開スペース - 送信済 - 送信中 + 送信しました + 送信しています 種類 確認済 選択済 @@ -1521,7 +1519,7 @@ 音声メッセージを録音できません ルームのアップグレードには権限が必要です アップグレード - アップグレードが必要です + アップグレードが必要 非公開のルームをアップグレード 音声メッセージを一時停止 このメールアドレスをアカウントにリンク @@ -1594,11 +1592,11 @@ キーワードに「%s」を含めることはできません %sへのメール通知を有効にする ヒント:メッセージを長押しして「%s」を選択。 - スレッドを用いると、会話のテーマを保ったり、会話を追跡したりするのが容易になります。 - あなたの非公開スペース + スレッド機能を使うと、会話のテーマを維持したり、会話を簡単に追跡したりすることができます。 + あなたの非公開のスペース あなたの公開スペース 自分のみ - スレッドで議論を整理して管理 + スレッド機能を使って、会話をまとめましょう %sを待機しています… この端末でスキャン 認証を送信済 @@ -1757,7 +1755,7 @@ あなたの連絡先は非公開です。端末の連絡先からユーザーを発見するためには、連絡先の情報をIDサーバーに送信する許可が必要です。 ディスカバリー設定を開く トピックを追加 - %sはルームを作成し設定しました。 + %sがルームを作成し設定しました。 参加しました。 %sが参加しました。 このルームで使用されている暗号化はサポートされていません @@ -1819,7 +1817,7 @@ 再送信 コードを入力 電話番号 - 退席中 + 離席中 オフライン オンライン 一般 @@ -1850,11 +1848,11 @@ 紙吹雪🎉を送る 降雪❄️を送る あなたのチームのメッセージングに。 - エンドツーエンドで暗号化され、電話番号不要。広告やデータマイニング無し。 + エンドツーエンドで暗号化されており、登録に電話番号は不要です。広告もデータ収集もありません。 会話の保存先を自分で決められ、自分で管理できる独立したコミュニケーション。Matrixをもとに。 - お宅での対面会話と同じぐらいのプライバシーを提供する、セキュアで独立したコミュニケーション。 - セキュアメッセージング - 管理権を握るのは、あなたです。 + オンライン上でも対面の会話と同じレベルでプライバシーを守る、安全で独立したコミュニケーション。 + 安全なメッセージ。 + 主導権を握るのは、あなたです。 ${app_name}の使用に関するヘルプ 詳細なログは、イライラシェイクでログを送信する際に、より多くのログを提供することで、開発者にとっての助けになります。有効にした場合でも、メッセージの内容やその他のプライベートな情報は記録されません。 ルームのアップグレードは高度な作業であり、不具合や欠けている機能、セキュリティー上の脆弱性がある場合に推奨されます。 @@ -1869,7 +1867,7 @@ %sに招待 ユーザー名かメールアドレスで招待 - %sを退出してよろしいですか? + %sから退出してよろしいですか? スペースは、ルームや連絡先をグループ化する新しい方法です。 招待されています 新しいスペースを、あなたが管理するスペースに追加。 @@ -1954,7 +1952,7 @@ 認証 メールアドレスが正しくないようです 国際電話番号の形式を使用してください。 - 国際電話番号は「+」から始まる必要があります + 国際電話番号は「+」から始めてください コードを%1$sに送信しました。以下に入力して認証してください。 このメールアドレスはどのアカウントにも登録されていません パスワードを変更すると、全てのセッションでのエンドツーエンド暗号鍵がリセットされ、暗号化されたメッセージ履歴が読めなくなります。パスワードを再設定する前に、鍵のバックアップを設定するか、他のセッションからルームの鍵をエクスポートしておいてください。 @@ -1970,7 +1968,7 @@ %1$sを読み込み中にエラーが発生しました(%2$d) 利用したいサーバーのアドレスを入力してください 利用したいModular Elementまたはサーバーのアドレスを入力してください - 迷っていますか?%sしてもいいです + 迷っていますか?%s みんなと繋がる手助けをいたします。 自分のコード @@ -1988,7 +1986,7 @@ 音を出さずに通知 音を出して通知 既定の信頼レベル - いくつかのメッセージは送信されませんでした + いくつかのメッセージが送信されませんでした 保存されていない変更があります。変更を破棄しますか? このルームはまだ作成されていません。キャンセルしますか? テキストメッセージで共有 @@ -2059,11 +2057,9 @@ ただいま%1$sにメールを送信しました。 \nアカウント登録を続行するにはメール内のリンクをクリックしてください。 CAPTCHA認証を行ってください - アカウントがまだ登録されていません。 -\n -\n登録を中止しますか? + アカウントがまだ作成されていません。登録を中止しますか? %1$sにアカウント登録 - あなたはすべてのセッションからログアウトしており、これ以上プッシュ通知を受け取れません。通知を再び有効にするには、各端末でサインインしてください。 + すべてのセッションからログアウトしているため、プッシュ通知を受け取れません。通知を再び有効にするには、各端末でサインインしてください。 セキュリティーフレーズを設定 セキュリティーフレーズを使用 セキュリティーキーは、パスワードマネージャーもしくは金庫のような安全な場所で保管してください。 @@ -2133,10 +2129,10 @@ 暗号化の設定が正しくありません。 暗号化を復元 暗号化を有効な状態に取り戻すために、管理者に連絡してください。 - このユーザーとのメッセージはエンドツーエンド暗号化されており、第三者には読めません。 + このユーザーとのメッセージはエンドツーエンドで暗号化されており、第三者が解読することはできません。 このコードを相手の画面に現れているコードと比較してください。 - 絵文字を比較して、同じ順番に現れているのを確認してください。 - セキュリティーを高めるために、対面で行うか、他の信頼できる通信手段を利用しましょう。 + 絵文字を比較して、同じ順番で現れているのを確認してください。 + セキュリティーを高めるために、対面で行うか、他の通信手段を利用しましょう。 選択されたエモートを虹色にして送信します 選択されたテキストを虹色にして送信します ${app_name}がID%1$sのイベントを処理中にエラーが発生しました @@ -2173,7 +2169,7 @@ %1$sが%2$sの権限レベルを変更しました。 %1$sの権限レベルを変更しました。 誰と使いますか? - どんなスペースを作りますか? + 作成するスペースの種類を選択してください 自分と仲間の非公開のスペース ルームを整理するためのプライベートスペース ここが会話のスタート地点です。 @@ -2188,7 +2184,7 @@ 私のスペース %1$s %2$s に参加してください スキップ ルームの通知 - 現在、IDサーバーを使用していません。あなたの知っているチームメイトを発見したり、そのチームメイトから発見されるようにするには、以下でIDサーバーを設定してください。 + 現在、IDサーバーを使用していません。あなたの知っているチームメイトを招待したり、チームメイトから見つけてもらったりするには、以下でIDサーバーを設定してください。 ディスカバリーの設定を終了します。 ここの参加者はあなただけです。退出すると、今後あなたを含めて誰も参加できなくなります。 再び招待されない限り、再参加することはできません。 @@ -2221,7 +2217,7 @@ このルームが発見できません。存在することを確認してください。 指紋や顔画像など、端末に固有の生体認証を有効にする。 絵文字で認証 - テキストで認証 + テキストを使って手動で認証 復旧用の手段を全て無くしてしまいましたか?全てリセットする クロス署名に対応した他のMatrixのクライアントでも使用できます。 どのような議論を%sで行いたいですか? @@ -2253,7 +2249,7 @@ 通話の転送中にエラーが発生しました 2分後にPINコードを要求 ルーム名やメッセージの内容などの詳細を表示。 - エラーが多すぎます。ログアウトしました + 多数のエラーが発生したため、ログアウトしました 警告!もう一度誤ったコードを入力すると、ログアウトします! コードが誤っています。残りの試行回数は%d回です @@ -2278,7 +2274,7 @@ エラーのためメッセージが送信されませんでした %sにいない人を探していますか? 直接${app_name}で招待を受け取るには、設定画面から%sしてください。 - PINコードでしか${app_name}のロックを解除することはできません。 + PINコードでしか${app_name}のロックを解除できません。 ${app_name}を開く際には、毎回PINコードの入力が必要です。 あなたがブロックされているルームを開くことはできません。 PINコードの認証に失敗しました。新しいコードを入力してください。 @@ -2291,7 +2287,7 @@ 確認のため、セキュリティーフレーズを再入力してください。 ホームサーバー(%1$s)は、IDサーバーに%2$sを設定するように提案しています IDサーバー %s から切断しますか? - ダイレクトメッセージを作成できませんでした。招待したユーザーを確認し、もう一度やり直してください。 + ダイレクトメッセージを作成できませんでした。招待したいユーザーを確認し、もう一度やり直してください。 セキュリティーフレーズ 自分と仲間 メッセージの種類がありません @@ -2310,7 +2306,7 @@ 操作を実行できません。ホームサーバーは最新のバージョンではありません。 ビデオ通話が拒否されました 音声通話が拒否されました - %1$sは通話を拒否しました + %1$sは着信を拒否しました このデバイスを認証可能な他の端末が全くない場合にのみ、続行してください。 このセッションを信頼済として認証すると、暗号化されたメッセージにアクセスすることができます。このアカウントにサインインしなかった場合は、あなたのアカウントのセキュリティーが破られている可能性があります: アカウントのセキュリティーが破られている可能性があります @@ -2331,7 +2327,7 @@ %d個のエントリー 保存して続行 - 設定を保存しました。 + 設定画面からいつでもプロフィールを更新できます これは後から変更できます。 プロフィール画像を追加 これは後から変更できます @@ -2342,7 +2338,7 @@ 位置情報(ライブ)を共有 中止 表示名を選択 - あなたのアカウント %s が作成されました。 + あなたのアカウント %s が作成されました おめでとうございます! 近日中にスレッドはベータ版となります。 \n @@ -2355,7 +2351,7 @@ フィードバックを送信 ベータ版 ベータ版 - 試す + 試してみる オフラインモード 新着はありません。 - ユーザーの無視が解除されました @@ -2378,7 +2374,7 @@ %1$sと他%2$d名 %1$sと%2$s - ホームサーバーがサポートしていないため、スレッド機能は不安定かもしれません。スレッドのメッセージは安定して表示されないおそれがあります。%sスレッド機能を有効にしてよろしいですか? + ホームサーバーがサポートしていないため、スレッド機能は不安定かもしれません。スレッドのメッセージが安定して表示されないおそれがあります。%sスレッド機能を有効にしてよろしいですか? スレッド(ベータ版) スレッドを用いると、会話のテーマを保ったり、会話を追跡したりするのが容易になります。%sスレッドを有効にするとアプリケーションが再起動します。再起動には時間がかかる可能性があります。 スレッド(ベータ版) @@ -2472,7 +2468,7 @@ QRコードが不正です。 スペースは、ルームと連絡先をまとめる新しい方法です。はじめに、スペースを作成しましょう。 最近の履歴を表示 - この暗号化されたメッセージの信頼性はこの端末では保証できません。 + この暗号化されたメッセージの真正性はこの端末では保証できません。 アカウントが安全かどうか確認してください 未認証のセッションがあります 連絡先 @@ -2500,4 +2496,268 @@ リッチテキストエディターを試してみる(プレーンテキストモードは近日公開) タブを使用して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 であなたを見つけることができます + 有効: + プロフィールのタグ: + 問題が発生しました。ネットワークの接続を確認して、もう一度やり直してください。 + 引用 + チーム、友達、組織向けのオールインワンの安全なチャットアプリです。はじめに、チャットを作成するか既存のルームに参加しましょう。 \ 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..caf9913299 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,25 @@ 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 + \ 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-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 From bd807b1745579ea8ace45a34106315ba5e5e1bc2 Mon Sep 17 00:00:00 2001 From: bmarty Date: Tue, 7 Feb 2023 06:06:11 +0000 Subject: [PATCH 39/96] Import strings from Element Android --- .../src/main/res/values-fi/strings.xml | 64 ++++- .../src/main/res/values-is/strings.xml | 42 +++- .../src/main/res/values-ja/strings.xml | 226 +++++++++++++----- .../src/main/res/values-zh-rCN/strings.xml | 5 +- 4 files changed, 261 insertions(+), 76 deletions(-) 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-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-ja/strings.xml b/libraries/ui-strings/src/main/res/values-ja/strings.xml index 54a8c8ee0f..d37cc518d7 100644 --- a/libraries/ui-strings/src/main/res/values-ja/strings.xml +++ b/libraries/ui-strings/src/main/res/values-ja/strings.xml @@ -16,14 +16,14 @@ %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」閲覧可能に設定しました。 ルームのメンバー全員(招待された時点から) ルームのメンバー全員(参加した時点から) ルームのメンバー全員 @@ -34,7 +34,7 @@ %1$sが%2$sにルームへの招待を送りました %1$sが%2$sの招待を受け入れました ** 復号化できません:%s ** - 送信者の端末からこのメッセージの鍵が送信されていません。 + 送信者の端末からこのメッセージ用の鍵が送信されていません。 メッセージを送信できません Matrixエラー メールアドレス @@ -72,7 +72,7 @@ 共有 削除 招待 - 全ての発言を既読にする + 全て既読にする すぐに返信 開く 閉じる @@ -230,8 +230,8 @@ ダイレクトメッセージ ブロック ブロックを解除 - この参加者の発言を全て非表示 - このメンバーの発言を全て表示 + 無視 + 無視を解除 メンション ログアウト 無視 @@ -302,7 +302,7 @@ サーバーの管理者が、これは想定されていることであると言っているのであれば、以下のフィンガープリントが、管理者によるフィンガープリントと一致していることを確認してください。 証明書はあなたの電話により信頼されていたものから変更されています。これはきわめて異常な事態です。この新しい証明書を承認しないことを強く推奨します。 証明書は以前信頼されていたものから信頼されていないものへと変更されています。サーバーがその証明書を更新した可能性があります。サーバーの管理者に連絡して、適切なフィンガープリントを確認してください。 - サーバーの管理者が上のフィンガープリントと一致するものを発行した場合に限り、証明書を承認してください。 + サーバーの管理者が上記のものと一致するフィンガープリントを発行した場合にのみ、証明書を承認してください。 検索 このアプリの情報をシステム設定で表示。 アプリの情報 @@ -352,13 +352,13 @@ メンバー - %d名のメンバー + %d人のメンバー %d件の新しいメッセージ アバター - スタンプを送る + ステッカーを送る ダウンロード システムアラート 可能であれば、英語で説明文を記述してください。 @@ -403,7 +403,7 @@ 表示するニックネームを変更 Markdown書式の入/切 Matrixアプリの管理を修正するには - %1$sのホームサーバーの使用を継続するには、利用規約を確認し、同意する必要があります。 + %1$sのホームサーバーを引き続き使用するには、利用規約を確認して同意する必要があります。 エラー 今すぐ確認 アカウントを停止 @@ -641,7 +641,7 @@ 拡張設定 現在の言語 他の利用可能な言語 - メッセージエディタ + メッセージエディター 環境設定 この端末で設定 セキュアバックアップを再設定 @@ -761,7 +761,7 @@ このユーザーの招待をキャンセルしてよろしいですか? 招待をキャンセル このユーザーを解除すると、そのユーザーからの全てのメッセージが再び表示されます。 - ユーザーを無視しない + ユーザーの無視を解除 このユーザーを無視すると、あなたが共有しているルームからそのユーザーのメッセージが削除されます。 \n \nこの操作は、設定からいつでも元に戻すことができます。 @@ -787,11 +787,11 @@ 非公開 切り替える 追加 - %1$sがエンドツーエンド暗号化(認識されていないアルゴリズム %2$s)をオンにしました。 - エンドツーエンド暗号化(認識されていないアルゴリズム %1$s)をオンにしました。 + %1$sがエンドツーエンド暗号化(認識されていないアルゴリズム %2$s)を有効にしました。 + エンドツーエンド暗号化(認識されていないアルゴリズム %1$s)を有効にしました。 会話を始める - %1$sがエンドツーエンド暗号化をオンにしました。 - エンドツーエンド暗号化をオンにしました。 + %1$sがエンドツーエンド暗号化を有効にしました。 + エンドツーエンド暗号化を有効にしました。 ゲストがルームに参加するのを拒否しました。 %1$sはゲストがルームに参加するのを拒否しました。 ゲストがルームに参加するのを拒否しました。 @@ -849,19 +849,19 @@ %1$sがこのルームのアドレスに%2$sを追加しました。 %sがこのルームのサーバーのアクセス制御リストを変更しました。 - IPリテラルに一致するサーバーは禁止されています。 + ・IPリテラルに一致するサーバーはブロックされています。 ・IPリテラルに一致するサーバーを許可します。 ・%sに一致するサーバーは許可されています。 - ・%sに一致するサーバーは禁止されています。 + ・%sに一致するサーバーはブロックされています。 %sがこのルームのサーバーアクセス制御リストを設定しました。 %sがここをアップグレードしました。 %sがこのルームをアップグレードしました。 今後のメッセージを「%1$s」閲覧可能に設定しました。 - 今後のルーム履歴を「%1$s」閲覧可能に設定しました。 + 今後のルームの履歴を「%1$s」閲覧可能に設定しました。 %1$sが今後のメッセージを「%2$s」閲覧可能に設定しました。 %sが通話を設定するためにデータを送信しました。 - 通話を開始しました。 - ビデオ通話を開始しました。 + 音声通話を発信しました。 + ビデオ通話を発信しました。 %1$sをブロックしました。理由:%2$s %1$sが%2$sをブロックしました。理由:%3$s %1$sのブロックを解除しました。理由:%2$s @@ -913,7 +913,7 @@ %1$sの権限レベルを%2$sから%3$sへ変更しました。 カスタム カスタム (%1$d) - デフォルト + 既定 モデレーター 管理者 %1$sウィジェットを変更しました @@ -1008,7 +1008,7 @@ %1$s、%2$s、他%3$d人のユーザーが読みました %1$s、%2$s、%3$sが読みました - メッセージをマークダウンとして解釈せずにプレーンテキストとして送信 + メッセージをマークダウンとして解釈せず、プレーンテキストとして送信 ファイルとして保存 共有 完了 @@ -1060,7 +1060,7 @@ ルームを追加 %sはあなたを招待しています このルームでグループ通話を開始する権限がありません - オーディオミーティングを開始 + 音声通話を開始 安全バックアップを設定 鍵のバックアップで管理 鍵のバックアップを使用 @@ -1115,7 +1115,7 @@ ホームサーバーにバックアップが存在しています リカバリーキーが保存されました。 リカバリーキーを保存 - コピーをしました + コピーしました リカバリーキーはパスワードマネージャー(もしくは金庫)のような、非常に安全な場所で保管してください リカバリーキーはセーフティーネットとなります。パスフレーズを忘れた場合でも、リカバリーキーを使えば、暗号化されたメッセージにアクセスすることができます。 \nリカバリーキーは、パスワードマネージャー(もしくは金庫)のような、非常に安全な場所で保管してください。 @@ -1175,7 +1175,7 @@ 既に一覧に載っているサーバーです サーバーまたはそのルーム一覧が見つかりません - 探索したい新しいサーバーの名前を入力してください。 + 探したい新しいサーバーの名前を入力してください。 新しいサーバーを追加 あなたのサーバー 暗号化されたメッセージの復元 @@ -1187,7 +1187,7 @@ スペースのメンバーのみ 誰でもルームを発見し参加できます 公開 - 招待された人だけが発見し参加できます + 招待した人のみが検索・参加できます 非公開 不明のアクセス設定(%s) 誰でもルームにノックができ、メンバーがその参加を承認または拒否できます @@ -1236,7 +1236,7 @@ 次に 次に ユーザー名を選択してください。 - ユーザー名やパスワードが正しくありません。 入力したパスワードは、スペースで開始または終了していますので、ご確認ください。 + ユーザー名かパスワードが正しくありません。入力されたパスワードがスペースによって開始しているか終了しているので、確認してください。 そのユーザー名は既に使用されています ユーザー名 ユーザー名またはメールアドレス @@ -1354,8 +1354,8 @@ ダイレクトメッセージ 自分のユーザー名 自分の表示名 - グループチャットでのメッセージの暗号化 - 個別チャットでのメッセージの暗号化 + グループチャットで暗号化されたメッセージ + 1対1のチャットで暗号化されたメッセージ 以下がメッセージに含まれる場合に通知 その他 メンションとキーワード @@ -1366,11 +1366,11 @@ %d件の不在着信(音声) - デフォルトで使いもう尋ねない + 既定に設定して次回から確認しない 鍵の共有リクエストの履歴を送信 結果がありません 自分に電話をかけることはできません。参加者が招待を受け入れるまでお待ちください - ミーティングはJitsiのセキュリティーとパーミッションポリシーを使用します。会議中は、現在ルームにいる全ての人に招待が表示されます。 + ミーティングはJitsiのセキュリティーとパーミッションポリシーを使用します。ミーティング中は、現在ルームにいる全ての人に招待が表示されます。 権限がありません 音声メッセージを送信するには、マイクの権限を許可してください。 この操作を実行するには、システム設定からカメラの権限を許可してください。 @@ -1490,7 +1490,7 @@ 暗号化されていません 終了 再読み込み - セッション一覧 + セッション 警告 無視を解除 動画。 @@ -1543,7 +1543,7 @@ スペースに関する変更を行うために必要な役割を更新する権限がありません スペースに関する変更を行うために必要な役割を選択 スペースに関する変更を行うために必要な役割を表示し更新します。 - フィルター + 絞り込む スレッド スレッド スペースをアップグレード @@ -1595,17 +1595,17 @@ スレッド機能を使うと、会話のテーマを維持したり、会話を簡単に追跡したりすることができます。 あなたの非公開のスペース あなたの公開スペース - 自分のみ + 自分専用 スレッド機能を使って、会話をまとめましょう %sを待機しています… この端末でスキャン - 認証を送信済 + 認証を送信しました このセッションを認証 音声 ルームのアドレスを入力してください このアドレスは既に使用されています スペースのアドレス - 一致しません + 一致していません 一致しています サインイン サインアウトしました @@ -1748,7 +1748,7 @@ %1$d個の投票 アンケートを締め切り、最終結果を表示します。 - 招待者のみ参加可能。個人やチームに最適 + 招待者のみ参加可能。個人やチーム向け スペースを作成 連絡先をスペースに招待 IDサーバーには利用規約がありません @@ -1777,7 +1777,7 @@ このアンケートを削除してよろしいですか?一度削除すると復元することはできません。 共有データの取り扱いに失敗しました 回転とクロップ - ルームを探索 + ルームを探す 既存のルームとスペースを追加 スレッドのメッセージを有効にする おすすめに追加 @@ -1826,7 +1826,7 @@ 認証済 未送信のメッセージを削除 カスタムイベントを送信 - ルームの状態を探索 + ルームの状態を調査 開封確認メッセージを表示 通知しない ファイルから鍵をインポート @@ -1840,7 +1840,7 @@ 初めに設定画面でIDサーバーの利用規約を承認してください。 初めにIDサーバーを設定してください。 - %1$d個の投票があります。結果を見るには投票してください + 合計%1$d票。投票すると結果を確認できます 未認証の端末で暗号化 メッセージを紙吹雪と共に送信 @@ -1860,15 +1860,15 @@ 一度有効にしたルームの暗号化は無効にすることはできません。暗号化されたルームで送信されたメッセージは、サーバーからは見ることができず、そのルームのメンバーだけが見ることができます。暗号化を有効にすると、多くのボットやブリッジが正常に動作しなくなる場合があります。 %sして、このルームを皆に紹介しましょう。 このコードを共有し、スキャンして追加してもらい、会話を始めましょう。 - 正当な参加者が%sにアクセスできることを確認してください。 - 参加者を追加 + 正しい参加者が%sにアクセスできるようにしましょう。 + 連絡先を追加 %d人の知り合いがすでに参加しています %sに招待 ユーザー名かメールアドレスで招待 %sから退出してよろしいですか? - スペースは、ルームや連絡先をグループ化する新しい方法です。 + スペースは、ルームや連絡先をまとめる新しい方法です。 招待されています 新しいスペースを、あなたが管理するスペースに追加。 注意:アプリケーションが再起動します @@ -1882,7 +1882,7 @@ %1$d個の投票に基づく - %1$d個の投票に基づく最終結果 + 合計%1$d票の投票に基づく最終結果 新しいセッションが認証されました。セッションは暗号化されたメッセージにアクセスでき、他のユーザーには信頼済として表示されます。 このルームを同じホームサーバー上で組織内のチームとのコラボレーションにのみ使用するなら、このオプションを有効にするといいかもしれません。これは後から変更できません。 @@ -1992,7 +1992,7 @@ テキストメッセージで共有 保護を設定 このルームのみ - 誰でも参加可能。コミュニティーに最適 + 誰でも参加可能。コミュニティー向け 既存のスペースに参加するには、招待が必要です。 これは後から変更できます 変更を破棄 @@ -2131,7 +2131,7 @@ 暗号化を有効な状態に取り戻すために、管理者に連絡してください。 このユーザーとのメッセージはエンドツーエンドで暗号化されており、第三者が解読することはできません。 このコードを相手の画面に現れているコードと比較してください。 - 絵文字を比較して、同じ順番で現れているのを確認してください。 + 絵文字を比較して、同じ順番で現れていることを確認してください。 セキュリティーを高めるために、対面で行うか、他の通信手段を利用しましょう。 選択されたエモートを虹色にして送信します 選択されたテキストを虹色にして送信します @@ -2170,17 +2170,17 @@ %1$sの権限レベルを変更しました。 誰と使いますか? 作成するスペースの種類を選択してください - 自分と仲間の非公開のスペース - ルームを整理するためのプライベートスペース + 自分とチームメイトの非公開のスペース + ルームを整理するための非公開のスペース ここが会話のスタート地点です。 - ここが%sのスタート地点です。 - あと少しです!確認を待機しています… - あと少しです!もう一方のデバイスは同じマークを表示していますか? + ここが%sの始まりです。 + もう少しです!確認を待機しています… + あと少しです!もう一方の端末は同じマークを表示していますか? %sを待機しています… このユーザーがこのセッションを認証するまで、送受信されるメッセージには警告マークが付きます。手動で認証することも可能です。 セッションの取得に失敗しました - 誰がチームの仲間ですか? - %sを探索できるようになります + チームの仲間を招待しましょう + %sを探せるようになります 私のスペース %1$s %2$s に参加してください スキップ ルームの通知 @@ -2227,7 +2227,7 @@ 最新の${app_name}を他の端末で、${app_name} ウェブ版、${app_name} デスクトップ版、${app_name} iOS、${app_name} Android、あるいはクロス署名に対応した他のMatrixのクライアントでご使用ください スライドして通話を終了 電話番号を検索する際にエラーが発生しました - 着信を拒否しました + 通話を拒否しました それぞれにルームを作りましょう。後から追加することもできます(既にあるルームも追加できます)。 このスペースを特定できるような特徴を記入してください。これはいつでも変更できます。 目立つように特徴を記入してください。これはいつでも変更できます。 @@ -2240,7 +2240,7 @@ ステートイベント カスタムのステートイベントを送信 ステートイベントを送信しました! - 続行するには名前を付けてください。 + 続行するには名前を設定してください。 どんな作業に取り組みますか? あと%1$d件 @@ -2289,7 +2289,7 @@ IDサーバー %s から切断しますか? ダイレクトメッセージを作成できませんでした。招待したいユーザーを確認し、もう一度やり直してください。 セキュリティーフレーズ - 自分と仲間 + 自分とチームメイト メッセージの種類がありません 絵文字の一覧を閉じる 絵文字の一覧を開く @@ -2306,7 +2306,7 @@ 操作を実行できません。ホームサーバーは最新のバージョンではありません。 ビデオ通話が拒否されました 音声通話が拒否されました - %1$sは着信を拒否しました + %1$sは通話を拒否しました このデバイスを認証可能な他の端末が全くない場合にのみ、続行してください。 このセッションを信頼済として認証すると、暗号化されたメッセージにアクセスすることができます。このアカウントにサインインしなかった場合は、あなたのアカウントのセキュリティーが破られている可能性があります: アカウントのセキュリティーが破られている可能性があります @@ -2401,12 +2401,12 @@ 進みましょう ユーザー名 / メールアドレス / 電話番号 あなたは人間ですか? - %sに送信された手順に従ってください + %sに送信された手順に従ってください。 パスワードを再設定 パスワードを忘れた場合 電子メールを再送信 電子メールが届いていませんか? - %sに送信された手順に従ってください + %sに送信された手順に従ってください。 メールアドレスを認証 コードを再送信 コードが%sに送信されました @@ -2447,7 +2447,7 @@ 初期同期のリクエスト %sの子スペースを折りたたむ %sの子スペースを展開 - ルームを探索 + ルームを探す スペースを変更 ルームを作成 チャットを開始 @@ -2504,7 +2504,7 @@ 地図を読み込めません \nこのホームサーバーは地図が読み込むよう設定されていないおそれがあります。 - スペースは、ルームや連絡先をグループ化する新しい方法です。右下のボタンを使うと、既存のルームを追加したり新たに作成したりできます。 + スペースは、ルームや連絡先をまとめる新しい方法です。右下のボタンを使うと、既存のルームを追加したり新たに作成したりできます。 セキュリティーに関する勧告 その他のセッション セキュリティーを最大限に高めるには、不明なセッションや利用していないセッションからサインアウトしてください。 @@ -2530,7 +2530,7 @@ セッション名を設定すると、端末をより簡単に認識できるようになります。 このセッションでプッシュ通知を受信。 絞り込みを解除 - 絞り込み + 絞り込む アプリケーション、端末、アクティビティーに関する情報。 直近のアクティビティー セッション名 @@ -2706,8 +2706,8 @@ 問い合わせる 自分でサーバーを運営したいですか? サーバーのURL - あなたのサーバーのアドレスを入力してください - あなたのサーバーのアドレスを指定してください。サーバーにはあなたの全てのデータが保管されます + ホームサーバーのアドレスを入力してください + あなたのホームサーバーのアドレスを入力してください。ここにあなたの全てのデータがホストされます サーバーを選択 編集 8文字以上にしてください @@ -2760,4 +2760,98 @@ 問題が発生しました。ネットワークの接続を確認して、もう一度やり直してください。 引用 チーム、友達、組織向けのオールインワンの安全なチャットアプリです。はじめに、チャットを作成するか既存のルームに参加しましょう。 + 一致していませんか? + 接続に失敗しました + サインイン済の端末を確認してください。以下のコードが表示されているはずです。以下のコードがサインイン済の端末と一致していることを確認してください: + サインイン済の端末で以下の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-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 From ff5a3cd86c0b604a383d2aea30628d5b910a60ad Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 7 Feb 2023 10:34:45 +0100 Subject: [PATCH 40/96] Fix detekt issue --- .../android/libraries/designsystem/theme/ColorAliases.kt | 3 ++- .../libraries/designsystem/theme/previews/ColorListPreview.kt | 3 ++- .../designsystem/theme/previews/ColorsSchemePreview.kt | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) 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 index 5fd5c3f0af..8dcf4a9dfe 100644 --- 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 @@ -25,6 +25,7 @@ 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. @@ -57,7 +58,7 @@ private fun ContentToPreview() { ColorListPreview( backgroundColor = Color.Black, foregroundColor = Color.White, - colors = mapOf( + colors = persistentMapOf( "roomListRoomName" to MaterialTheme.roomListRoomName(), "roomListRoomMessage" to MaterialTheme.roomListRoomMessage(), "roomListRoomMessageDate" to MaterialTheme.roomListRoomMessageDate(), 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 index 3d728c0ad4..f511d619d0 100644 --- 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 @@ -25,12 +25,13 @@ 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: Map, + colors: ImmutableMap, modifier: Modifier = Modifier, ) { Column( 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 index 79bdb0b35f..6a764e9907 100644 --- 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 @@ -20,6 +20,7 @@ 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( @@ -28,7 +29,7 @@ internal fun ColorsSchemePreview( colorScheme: ColorScheme, modifier: Modifier = Modifier, ) { - val colors = mapOf( + val colors = persistentMapOf( "primary" to colorScheme.primary, "onPrimary" to colorScheme.onPrimary, "primaryContainer" to colorScheme.primaryContainer, From b4f7030ec9a7d6c37df57c92284d070d21dd3519 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 6 Feb 2023 12:39:36 +0100 Subject: [PATCH 41/96] Cleanup --- .../io/element/android/features/roomlist/RoomListPresenter.kt | 2 -- .../element/android/features/roomlist/model/RoomListState.kt | 1 - .../android/features/roomlist/RoomListPresenterTests.kt | 4 +++- 3 files changed, 3 insertions(+), 4 deletions(-) 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..c684b0dd45 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 @@ -57,7 +57,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 +85,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/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/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..1f2d265cc8 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,6 +14,8 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package io.element.android.features.roomlist import app.cash.molecule.RecompositionClock @@ -22,6 +24,7 @@ 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.FakeMatrixClient +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Test @@ -29,7 +32,6 @@ class RoomListPresenterTests { @Test fun `present - should start with no user and then load user with success`() = runTest { - val presenter = RoomListPresenter( FakeMatrixClient( SessionId("sessionId") From d722495dab29eda54a0b5f600c8f06e62e471360 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 6 Feb 2023 12:42:49 +0100 Subject: [PATCH 42/96] Fix first test. --- .../element/android/features/roomlist/RoomListPresenterTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1f2d265cc8..6a238f501e 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 @@ -43,7 +43,7 @@ class RoomListPresenterTests { val initialState = awaitItem() assertThat(initialState.matrixUser).isNull() val withUserState = awaitItem() - assertThat(withUserState).isNotNull() + assertThat(withUserState.matrixUser).isNotNull() } } } From 6cd9fe18f9aeeb76546e6f98a405b88dfa8038da Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 6 Feb 2023 12:59:14 +0100 Subject: [PATCH 43/96] Test filter effect. --- .../roomlist/RoomListPresenterTests.kt | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) 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 6a238f501e..70fc4f22e4 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 @@ -22,6 +22,7 @@ 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.libraries.matrix.core.SessionId import io.element.android.libraries.matrixtest.FakeMatrixClient import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -35,7 +36,8 @@ class RoomListPresenterTests { val presenter = RoomListPresenter( FakeMatrixClient( SessionId("sessionId") - ), LastMessageFormatter() + ), + LastMessageFormatter() ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -46,4 +48,24 @@ class RoomListPresenterTests { assertThat(withUserState.matrixUser).isNotNull() } } + + @Test + fun `present - should filter room with success`() = runTest { + val presenter = RoomListPresenter( + FakeMatrixClient( + SessionId("sessionId") + ), + LastMessageFormatter() + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + var initialState = awaitItem() + val withUserState = awaitItem() + assertThat(withUserState.filter).isEqualTo("") + withUserState.eventSink.invoke(RoomListEvents.UpdateFilter("t")) + val withFilterState = awaitItem() + assertThat(withFilterState.filter).isEqualTo("t") + } + } } From cb158acf00e830d57cdb4ddbd2a39116bc4e9374 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 6 Feb 2023 15:59:13 +0100 Subject: [PATCH 44/96] Fix bug. n+1 items were created. --- .../roomlist/model/RoomListRoomSummaryPlaceholders.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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")) } } } From feac348c8986c46f7454eeaaa72ea7548a2eca48 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 6 Feb 2023 16:27:19 +0100 Subject: [PATCH 45/96] Enable `testFixtures` - but not supported by AGP yet, so put files in module `matrixtest` --- features/roomlist/build.gradle.kts | 2 + .../roomlist/RoomListPresenterTests.kt | 51 ++++++++++++++- .../template/TemplatePresenterTests.kt | 3 + gradle.properties | 3 + .../libraries/matrixtest/FakeMatrixClient.kt | 7 ++- .../matrixtest/core/RoomIdFixture.kt | 22 +++++++ .../room/InMemoryRoomSummaryDataSource.kt | 8 ++- .../matrixtest/room/RoomSummaryFixture.kt | 63 +++++++++++++++++++ .../main/kotlin/extension/CommonExtension.kt | 2 + 9 files changed, 157 insertions(+), 4 deletions(-) create mode 100644 libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/core/RoomIdFixture.kt create mode 100644 libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/room/RoomSummaryFixture.kt diff --git a/features/roomlist/build.gradle.kts b/features/roomlist/build.gradle.kts index fc0402eafc..de67bb010f 100644 --- a/features/roomlist/build.gradle.kts +++ b/features/roomlist/build.gradle.kts @@ -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/test/kotlin/io/element/android/features/roomlist/RoomListPresenterTests.kt b/features/roomlist/src/test/kotlin/io/element/android/features/roomlist/RoomListPresenterTests.kt index 70fc4f22e4..8bd6efa167 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 @@ -23,11 +23,20 @@ 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.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.core.SessionId import io.element.android.libraries.matrixtest.FakeMatrixClient +import io.element.android.libraries.matrixtest.core.A_ROOM_ID +import io.element.android.libraries.matrixtest.core.A_ROOM_ID_VALUE +import io.element.android.libraries.matrixtest.room.A_LAST_MESSAGE +import io.element.android.libraries.matrixtest.room.A_ROOM_NAME +import io.element.android.libraries.matrixtest.room.InMemoryRoomSummaryDataSource +import io.element.android.libraries.matrixtest.room.aRoomSummaryFilled import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Test +import timber.log.Timber class RoomListPresenterTests { @@ -60,7 +69,7 @@ class RoomListPresenterTests { moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { - var initialState = awaitItem() + skipItems(1) val withUserState = awaitItem() assertThat(withUserState.filter).isEqualTo("") withUserState.eventSink.invoke(RoomListEvents.UpdateFilter("t")) @@ -68,4 +77,44 @@ class RoomListPresenterTests { assertThat(withFilterState.filter).isEqualTo("t") } } + + @Test + fun `present - load 1 room with success`() = runTest { + val roomSummaryDataSource = InMemoryRoomSummaryDataSource() + val presenter = RoomListPresenter( + FakeMatrixClient( + sessionId = SessionId("sessionId"), + roomSummaryDataSource = roomSummaryDataSource + ), + LastMessageFormatter() + ) + 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() + val roomSummary = aRoomSummaryFilled() + roomSummaryDataSource.postRoomSummary( + listOf(roomSummary) + ) + skipItems(1) + val withRoomState = awaitItem() + assertThat(withRoomState.roomList.size).isEqualTo(1) + assertThat(withRoomState.roomList.first()).isEqualTo( + RoomListRoomSummary( + id = A_ROOM_ID_VALUE, + roomId = A_ROOM_ID, + name = A_ROOM_NAME, + hasUnread = true, + timestamp = "", + lastMessage = A_LAST_MESSAGE, + avatarData = AvatarData(name = A_ROOM_NAME), + isPlaceholder = false, + ) + ) + } + } } 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..34cc73ba53 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,12 +14,15 @@ * 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 kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Test 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/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..dd0181f503 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 @@ -28,7 +28,10 @@ import io.element.android.libraries.matrixtest.room.FakeMatrixRoom import io.element.android.libraries.matrixtest.room.InMemoryRoomSummaryDataSource import org.matrix.rustcomponents.sdk.MediaSource -class FakeMatrixClient(override val sessionId: SessionId) : MatrixClient { +class FakeMatrixClient( + override val sessionId: SessionId, + val roomSummaryDataSource: RoomSummaryDataSource = InMemoryRoomSummaryDataSource() +) : MatrixClient { override fun getRoom(roomId: RoomId): MatrixRoom? { return FakeMatrixRoom(roomId) @@ -39,7 +42,7 @@ class FakeMatrixClient(override val sessionId: SessionId) : MatrixClient { override fun stopSync() = Unit override fun roomSummaryDataSource(): RoomSummaryDataSource { - return InMemoryRoomSummaryDataSource() + return roomSummaryDataSource } override fun mediaResolver(): MediaResolver { diff --git a/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/core/RoomIdFixture.kt b/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/core/RoomIdFixture.kt new file mode 100644 index 0000000000..5b62ad383a --- /dev/null +++ b/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/core/RoomIdFixture.kt @@ -0,0 +1,22 @@ +/* + * 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.core + +import io.element.android.libraries.matrix.core.RoomId + +const val A_ROOM_ID_VALUE = "!aRoomId" +val A_ROOM_ID = RoomId(A_ROOM_ID_VALUE) 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/InMemoryRoomSummaryDataSource.kt index 5179e911ab..666f64acb4 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/InMemoryRoomSummaryDataSource.kt @@ -23,8 +23,14 @@ import kotlinx.coroutines.flow.StateFlow class InMemoryRoomSummaryDataSource : RoomSummaryDataSource { + private val roomSummariesFlow = MutableStateFlow>(emptyList()) + + suspend fun postRoomSummary(roomSummaries: List) { + roomSummariesFlow.emit(roomSummaries) + } + override fun roomSummaries(): StateFlow> { - return MutableStateFlow(emptyList()) + return roomSummariesFlow } override fun setSlidingSyncRange(range: IntRange) = Unit 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..0bed866300 --- /dev/null +++ b/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/room/RoomSummaryFixture.kt @@ -0,0 +1,63 @@ +/* + * 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.core.A_ROOM_ID + +const val A_ROOM_NAME = "aRoomName" +const val A_LAST_MESSAGE = "Last message" + +fun aRoomSummaryFilled( + roomId: RoomId = A_ROOM_ID, + name: String = A_ROOM_NAME, + isDirect: Boolean = false, + avatarURLString: String? = null, + lastMessage: CharSequence? = A_LAST_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_LAST_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/plugins/src/main/kotlin/extension/CommonExtension.kt b/plugins/src/main/kotlin/extension/CommonExtension.kt index f3ba843ab7..5fdb80ba1a 100644 --- a/plugins/src/main/kotlin/extension/CommonExtension.kt +++ b/plugins/src/main/kotlin/extension/CommonExtension.kt @@ -40,6 +40,7 @@ fun CommonExtension<*, *, *, *>.androidConfig(project: Project) { lintConfig = File("${project.rootDir}/tools/lint/lint.xml") checkDependencies = true abortOnError = true + ignoreTestFixturesSources = true } } @@ -64,6 +65,7 @@ fun CommonExtension<*, *, *, *>.composeConfig() { // Disabled until lint stops inspecting generated ksp files... // error.add("ComposableLambdaParameterNaming") error.add("ComposableLambdaParameterPosition") + ignoreTestFixturesSources = true } } From e4ca3541c31644d3e813b07ac9b23628738e9ff6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 6 Feb 2023 17:41:35 +0100 Subject: [PATCH 46/96] Test room filtering. --- .../roomlist/RoomListPresenterTests.kt | 61 +++++++++++++------ 1 file changed, 44 insertions(+), 17 deletions(-) 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 8bd6efa167..19b9a2755a 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 @@ -36,7 +36,6 @@ import io.element.android.libraries.matrixtest.room.aRoomSummaryFilled import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Test -import timber.log.Timber class RoomListPresenterTests { @@ -96,25 +95,53 @@ class RoomListPresenterTests { // Room list is loaded with 16 placeholders assertThat(withUserState.roomList.size).isEqualTo(16) assertThat(withUserState.roomList.all { it.isPlaceholder }).isTrue() - val roomSummary = aRoomSummaryFilled() - roomSummaryDataSource.postRoomSummary( - listOf(roomSummary) - ) + roomSummaryDataSource.postRoomSummary(listOf(aRoomSummaryFilled())) skipItems(1) val withRoomState = awaitItem() assertThat(withRoomState.roomList.size).isEqualTo(1) - assertThat(withRoomState.roomList.first()).isEqualTo( - RoomListRoomSummary( - id = A_ROOM_ID_VALUE, - roomId = A_ROOM_ID, - name = A_ROOM_NAME, - hasUnread = true, - timestamp = "", - lastMessage = A_LAST_MESSAGE, - avatarData = AvatarData(name = A_ROOM_NAME), - isPlaceholder = false, - ) - ) + assertThat(withRoomState.roomList.first()).isEqualTo(aRoomListRoomSummary) + } + } + + @Test + fun `present - load 1 room with success and filter rooms`() = runTest { + val roomSummaryDataSource = InMemoryRoomSummaryDataSource() + val presenter = RoomListPresenter( + FakeMatrixClient( + sessionId = SessionId("sessionId"), + roomSummaryDataSource = roomSummaryDataSource + ), + LastMessageFormatter() + ) + 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.size).isEqualTo(0) } } } + +private val aRoomListRoomSummary = RoomListRoomSummary( + id = A_ROOM_ID_VALUE, + roomId = A_ROOM_ID, + name = A_ROOM_NAME, + hasUnread = true, + timestamp = "", + lastMessage = A_LAST_MESSAGE, + avatarData = AvatarData(name = A_ROOM_NAME), + isPlaceholder = false, +) From c3a7ab1de45eb6a481cd52769994d696b9777174 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 6 Feb 2023 17:52:29 +0100 Subject: [PATCH 47/96] Add test about visible range --- .../roomlist/RoomListPresenterTests.kt | 37 +++++++++++++++++++ .../room/InMemoryRoomSummaryDataSource.kt | 6 ++- 2 files changed, 42 insertions(+), 1 deletion(-) 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 19b9a2755a..7af95b0196 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 @@ -133,6 +133,43 @@ class RoomListPresenterTests { assertThat(withFilteredRoomState.roomList.size).isEqualTo(0) } } + + @Test + fun `present - update visible range`() = runTest { + val roomSummaryDataSource = InMemoryRoomSummaryDataSource() + val presenter = RoomListPresenter( + FakeMatrixClient( + sessionId = SessionId("sessionId"), + roomSummaryDataSource = roomSummaryDataSource + ), + LastMessageFormatter() + ) + 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 val aRoomListRoomSummary = RoomListRoomSummary( 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/InMemoryRoomSummaryDataSource.kt index 666f64acb4..17d7d108a2 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/InMemoryRoomSummaryDataSource.kt @@ -33,5 +33,9 @@ class InMemoryRoomSummaryDataSource : RoomSummaryDataSource { return roomSummariesFlow } - override fun setSlidingSyncRange(range: IntRange) = Unit + var latestSlidingSyncRange: IntRange? = null + + override fun setSlidingSyncRange(range: IntRange) { + latestSlidingSyncRange = range + } } From ae4237abb5022c590765ed9cd7154115b2b035b2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 6 Feb 2023 18:09:18 +0100 Subject: [PATCH 48/96] Create module `dateformatter` --- features/roomlist/build.gradle.kts | 2 +- .../features/roomlist/RoomListPresenter.kt | 1 + .../roomlist/RoomListPresenterTests.kt | 11 ++--- libraries/dateformatter/.gitignore | 1 + libraries/dateformatter/build.gradle.kts | 40 +++++++++++++++++++ libraries/dateformatter/consumer-rules.pro | 0 libraries/dateformatter/proguard-rules.pro | 21 ++++++++++ .../src/main/AndroidManifest.xml | 16 ++++++++ .../dateformatter/LastMessageFormatter.kt | 21 ++++++++++ .../impl/DefaultLastMessageFormatter.kt | 14 +++++-- .../kotlin/extension/DependencyHandleScope.kt | 1 + settings.gradle.kts | 1 + 12 files changed, 119 insertions(+), 10 deletions(-) create mode 100644 libraries/dateformatter/.gitignore create mode 100644 libraries/dateformatter/build.gradle.kts create mode 100644 libraries/dateformatter/consumer-rules.pro create mode 100644 libraries/dateformatter/proguard-rules.pro create mode 100644 libraries/dateformatter/src/main/AndroidManifest.xml create mode 100644 libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/LastMessageFormatter.kt rename features/roomlist/src/main/kotlin/io/element/android/features/roomlist/LastMessageFormatter.kt => libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageFormatter.kt (87%) diff --git a/features/roomlist/build.gradle.kts b/features/roomlist/build.gradle.kts index de67bb010f..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) 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 c684b0dd45..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 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 7af95b0196..c1c2e8b141 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 @@ -24,6 +24,7 @@ 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.impl.DefaultLastMessageFormatter import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.core.SessionId import io.element.android.libraries.matrixtest.FakeMatrixClient @@ -45,7 +46,7 @@ class RoomListPresenterTests { FakeMatrixClient( SessionId("sessionId") ), - LastMessageFormatter() + DefaultLastMessageFormatter() ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -63,7 +64,7 @@ class RoomListPresenterTests { FakeMatrixClient( SessionId("sessionId") ), - LastMessageFormatter() + DefaultLastMessageFormatter() ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -85,7 +86,7 @@ class RoomListPresenterTests { sessionId = SessionId("sessionId"), roomSummaryDataSource = roomSummaryDataSource ), - LastMessageFormatter() + DefaultLastMessageFormatter() ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -111,7 +112,7 @@ class RoomListPresenterTests { sessionId = SessionId("sessionId"), roomSummaryDataSource = roomSummaryDataSource ), - LastMessageFormatter() + DefaultLastMessageFormatter() ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -142,7 +143,7 @@ class RoomListPresenterTests { sessionId = SessionId("sessionId"), roomSummaryDataSource = roomSummaryDataSource ), - LastMessageFormatter() + DefaultLastMessageFormatter() ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() 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..ef5b1fc980 --- /dev/null +++ b/libraries/dateformatter/build.gradle.kts @@ -0,0 +1,40 @@ +/* + * 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) + implementation(libs.datetime) + ksp(libs.showkase.processor) + } +} 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/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 87% 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..13ffb9c7e0 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,9 +35,12 @@ import java.util.Locale import javax.inject.Inject import kotlin.math.absoluteValue -class LastMessageFormatter @Inject constructor() { +@ContributesBinding(AppScope::class) +class DefaultLastMessageFormatter @Inject constructor() : LastMessageFormatter { + // TODO Inject in constructor private val clock: Clock = Clock.System + // TODO Inject in constructor private val locale: Locale = Locale.getDefault() private val onlyTimeFormatter: DateTimeFormatter by lazy { @@ -52,7 +58,7 @@ class LastMessageFormatter @Inject constructor() { 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) 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 b85f5e6717..0c2cf97f2f 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") From 6d8e4d9afc05bfbd82f80e91107e61e8f57588b9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 6 Feb 2023 21:05:06 +0100 Subject: [PATCH 49/96] Create FakeLastMessageFormatter --- app/build.gradle.kts | 1 + .../roomlist/FakeLastMessageFormatter.kt | 30 ++++++++++++++++ .../roomlist/RoomListPresenterTests.kt | 22 ++++++++---- .../dateformatter/di/DateFormatterModule.kt | 34 +++++++++++++++++++ .../impl/DefaultLastMessageFormatter.kt | 11 +++--- 5 files changed, 84 insertions(+), 14 deletions(-) create mode 100644 features/roomlist/src/test/kotlin/io/element/android/features/roomlist/FakeLastMessageFormatter.kt create mode 100644 libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/di/DateFormatterModule.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f5a0799676..4aa95c2f7c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -175,6 +175,7 @@ dependencies { implementation(libs.androidx.activity.compose) implementation(libs.androidx.startup) implementation(libs.coil) + implementation(libs.datetime) implementation(libs.dagger) kapt(libs.dagger.compiler) diff --git a/features/roomlist/src/test/kotlin/io/element/android/features/roomlist/FakeLastMessageFormatter.kt b/features/roomlist/src/test/kotlin/io/element/android/features/roomlist/FakeLastMessageFormatter.kt new file mode 100644 index 0000000000..997846056a --- /dev/null +++ b/features/roomlist/src/test/kotlin/io/element/android/features/roomlist/FakeLastMessageFormatter.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.roomlist + +import io.element.android.libraries.dateformatter.LastMessageFormatter + +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 c1c2e8b141..5fd2da920b 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 @@ -24,7 +24,7 @@ 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.impl.DefaultLastMessageFormatter +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.FakeMatrixClient @@ -46,7 +46,7 @@ class RoomListPresenterTests { FakeMatrixClient( SessionId("sessionId") ), - DefaultLastMessageFormatter() + createDateFormatter() ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -64,7 +64,7 @@ class RoomListPresenterTests { FakeMatrixClient( SessionId("sessionId") ), - DefaultLastMessageFormatter() + createDateFormatter() ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -86,7 +86,7 @@ class RoomListPresenterTests { sessionId = SessionId("sessionId"), roomSummaryDataSource = roomSummaryDataSource ), - DefaultLastMessageFormatter() + createDateFormatter() ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -112,7 +112,7 @@ class RoomListPresenterTests { sessionId = SessionId("sessionId"), roomSummaryDataSource = roomSummaryDataSource ), - DefaultLastMessageFormatter() + createDateFormatter() ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -143,7 +143,7 @@ class RoomListPresenterTests { sessionId = SessionId("sessionId"), roomSummaryDataSource = roomSummaryDataSource ), - DefaultLastMessageFormatter() + createDateFormatter() ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -171,14 +171,22 @@ class RoomListPresenterTests { 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 = "", + timestamp = A_FORMATTED_DATE, lastMessage = A_LAST_MESSAGE, avatarData = AvatarData(name = A_ROOM_NAME), isPlaceholder = false, 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..85deac5274 --- /dev/null +++ b/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/di/DateFormatterModule.kt @@ -0,0 +1,34 @@ +/* + * 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 java.util.* + +@Module +@ContributesTo(AppScope::class) +object DateFormatterModule { + @Provides + fun providesClock(): Clock = Clock.System + + @Provides + fun providesLocale(): Locale = Locale.getDefault() +} diff --git a/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageFormatter.kt b/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageFormatter.kt index 13ffb9c7e0..28d76551ae 100644 --- a/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageFormatter.kt +++ b/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageFormatter.kt @@ -36,13 +36,10 @@ import javax.inject.Inject import kotlin.math.absoluteValue @ContributesBinding(AppScope::class) -class DefaultLastMessageFormatter @Inject constructor() : LastMessageFormatter { - - // TODO Inject in constructor - private val clock: Clock = Clock.System - // TODO Inject in constructor - private val locale: Locale = Locale.getDefault() - +class DefaultLastMessageFormatter @Inject constructor( + private val clock: Clock, + private val locale: Locale, +) : LastMessageFormatter { private val onlyTimeFormatter: DateTimeFormatter by lazy { val pattern = DateFormat.getBestDateTimePattern(locale, "HH:mm") DateTimeFormatter.ofPattern(pattern) From 6099fe5df76dd872a2c62c1f2a84869a7c93ddcf Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 6 Feb 2023 21:52:28 +0100 Subject: [PATCH 50/96] Add test for `DefaultLastMessageFormatter` --- libraries/dateformatter/build.gradle.kts | 3 + .../impl/DefaultLastMessageFormatter.kt | 8 +- .../impl/DefaultLastMessageFormatterTest.kt | 105 ++++++++++++++++++ .../libraries/dateformatter/impl/FakeClock.kt | 30 +++++ 4 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 libraries/dateformatter/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageFormatterTest.kt create mode 100644 libraries/dateformatter/src/test/kotlin/io/element/android/libraries/dateformatter/impl/FakeClock.kt diff --git a/libraries/dateformatter/build.gradle.kts b/libraries/dateformatter/build.gradle.kts index ef5b1fc980..817435f8ac 100644 --- a/libraries/dateformatter/build.gradle.kts +++ b/libraries/dateformatter/build.gradle.kts @@ -36,5 +36,8 @@ android { implementation(projects.anvilannotations) implementation(libs.datetime) ksp(libs.showkase.processor) + + testImplementation(libs.test.junit) + testImplementation(libs.test.truth) } } diff --git a/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageFormatter.kt b/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageFormatter.kt index 28d76551ae..f3b9d03bef 100644 --- a/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageFormatter.kt +++ b/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageFormatter.kt @@ -41,17 +41,17 @@ class DefaultLastMessageFormatter @Inject constructor( private val locale: Locale, ) : 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) } @@ -100,6 +100,6 @@ class DefaultLastMessageFormatter @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..fbf038a562 --- /dev/null +++ b/libraries/dateformatter/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageFormatterTest.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.dateformatter.impl + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.dateformatter.LastMessageFormatter +import kotlinx.datetime.Instant +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("20: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("20: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("20: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("19: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) + } +} diff --git a/libraries/dateformatter/src/test/kotlin/io/element/android/libraries/dateformatter/impl/FakeClock.kt b/libraries/dateformatter/src/test/kotlin/io/element/android/libraries/dateformatter/impl/FakeClock.kt new file mode 100644 index 0000000000..58a5495218 --- /dev/null +++ b/libraries/dateformatter/src/test/kotlin/io/element/android/libraries/dateformatter/impl/FakeClock.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.libraries.dateformatter.impl + +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant + +class FakeClock : Clock { + private var instant: Instant = Instant.fromEpochMilliseconds(0) + + fun givenInstant(instant: Instant) { + this.instant = instant + } + + override fun now(): Instant = instant +} From 058b789930a33d43d46d73298d6bdd5aeb3ac539 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 7 Feb 2023 09:42:21 +0100 Subject: [PATCH 51/96] Running kover run the tests, no need to do it twice. --- .github/workflows/tests.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f4a12c9ec2..0ad1584754 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 From 34bcba17123922155b174afccd0a34c3114e4a70 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 7 Feb 2023 09:47:56 +0100 Subject: [PATCH 52/96] Rename artifact --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0ad1584754..19b3192e68 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -36,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/ From 190d20acbe9cc5ae231448810a1568177d874b3d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 7 Feb 2023 09:53:26 +0100 Subject: [PATCH 53/96] Fix test: also inject timezone to avoid relying on the system timezone. --- .../libraries/dateformatter/di/DateFormatterModule.kt | 4 ++++ .../dateformatter/impl/DefaultLastMessageFormatter.kt | 7 ++++--- .../impl/DefaultLastMessageFormatterTest.kt | 11 ++++++----- 3 files changed, 14 insertions(+), 8 deletions(-) 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 index 85deac5274..feab851a8b 100644 --- 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 @@ -21,6 +21,7 @@ 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 @@ -31,4 +32,7 @@ object DateFormatterModule { @Provides fun providesLocale(): Locale = Locale.getDefault() + + @Provides + fun providesTimezone(): TimeZone = TimeZone.currentSystemDefault() } diff --git a/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageFormatter.kt b/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageFormatter.kt index f3b9d03bef..a466491766 100644 --- a/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageFormatter.kt +++ b/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageFormatter.kt @@ -39,6 +39,7 @@ import kotlin.math.absoluteValue 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") ?: "HH:mm" @@ -59,8 +60,8 @@ class DefaultLastMessageFormatter @Inject constructor( 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 -> { @@ -80,7 +81,7 @@ class DefaultLastMessageFormatter @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) } 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 index fbf038a562..c21dcf4230 100644 --- 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 @@ -19,6 +19,7 @@ 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 @@ -43,7 +44,7 @@ class DefaultLastMessageFormatterTest { 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("20:35") + assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("18:35") } @Test @@ -51,7 +52,7 @@ class DefaultLastMessageFormatterTest { 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("20:35") + assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("18:35") } @Test @@ -59,7 +60,7 @@ class DefaultLastMessageFormatterTest { 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("20:34") + assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("18:34") } @Test @@ -67,7 +68,7 @@ class DefaultLastMessageFormatterTest { 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("19:35") + assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("17:35") } @Test @@ -100,6 +101,6 @@ class DefaultLastMessageFormatterTest { */ private fun createFormatter(@Suppress("SameParameterValue") currentDate: String): LastMessageFormatter { val clock = FakeClock().also { it.givenInstant(Instant.parse(currentDate)) } - return DefaultLastMessageFormatter(clock, Locale.US) + return DefaultLastMessageFormatter(clock, Locale.US, TimeZone.UTC) } } From f02e5d606bb85617a4459171898d2fdffb27443c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 7 Feb 2023 10:30:05 +0100 Subject: [PATCH 54/96] Rename class. --- .../android/features/roomlist/RoomListPresenterTests.kt | 8 ++++---- .../android/libraries/matrixtest/FakeMatrixClient.kt | 4 ++-- ...mSummaryDataSource.kt => FakeRoomSummaryDataSource.kt} | 3 ++- 3 files changed, 8 insertions(+), 7 deletions(-) rename libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/room/{InMemoryRoomSummaryDataSource.kt => FakeRoomSummaryDataSource.kt} (94%) 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 5fd2da920b..baa547b4bb 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 @@ -32,7 +32,7 @@ import io.element.android.libraries.matrixtest.core.A_ROOM_ID import io.element.android.libraries.matrixtest.core.A_ROOM_ID_VALUE import io.element.android.libraries.matrixtest.room.A_LAST_MESSAGE import io.element.android.libraries.matrixtest.room.A_ROOM_NAME -import io.element.android.libraries.matrixtest.room.InMemoryRoomSummaryDataSource +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 @@ -80,7 +80,7 @@ class RoomListPresenterTests { @Test fun `present - load 1 room with success`() = runTest { - val roomSummaryDataSource = InMemoryRoomSummaryDataSource() + val roomSummaryDataSource = FakeRoomSummaryDataSource() val presenter = RoomListPresenter( FakeMatrixClient( sessionId = SessionId("sessionId"), @@ -106,7 +106,7 @@ class RoomListPresenterTests { @Test fun `present - load 1 room with success and filter rooms`() = runTest { - val roomSummaryDataSource = InMemoryRoomSummaryDataSource() + val roomSummaryDataSource = FakeRoomSummaryDataSource() val presenter = RoomListPresenter( FakeMatrixClient( sessionId = SessionId("sessionId"), @@ -137,7 +137,7 @@ class RoomListPresenterTests { @Test fun `present - update visible range`() = runTest { - val roomSummaryDataSource = InMemoryRoomSummaryDataSource() + val roomSummaryDataSource = FakeRoomSummaryDataSource() val presenter = RoomListPresenter( FakeMatrixClient( sessionId = SessionId("sessionId"), 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 dd0181f503..fb0490cd64 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,12 +25,12 @@ 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 org.matrix.rustcomponents.sdk.MediaSource class FakeMatrixClient( override val sessionId: SessionId, - val roomSummaryDataSource: RoomSummaryDataSource = InMemoryRoomSummaryDataSource() + val roomSummaryDataSource: RoomSummaryDataSource = FakeRoomSummaryDataSource() ) : MatrixClient { override fun getRoom(roomId: RoomId): MatrixRoom? { 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 94% 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 17d7d108a2..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,7 +21,7 @@ import io.element.android.libraries.matrix.room.RoomSummaryDataSource import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -class InMemoryRoomSummaryDataSource : RoomSummaryDataSource { +class FakeRoomSummaryDataSource : RoomSummaryDataSource { private val roomSummariesFlow = MutableStateFlow>(emptyList()) @@ -34,6 +34,7 @@ class InMemoryRoomSummaryDataSource : RoomSummaryDataSource { } var latestSlidingSyncRange: IntRange? = null + private set override fun setSlidingSyncRange(range: IntRange) { latestSlidingSyncRange = range From d02744bc17861a985bee95c1d9b22a5db4cf182c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 7 Feb 2023 15:57:18 +0100 Subject: [PATCH 55/96] Kover: add verify rules: global and for Presenters --- .github/workflows/quality.yml | 9 +++++++++ build.gradle.kts | 31 +++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 78c407cde1..31630acf97 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: | + */build/reports/kover/merged/verification/errors.txt - name: Prepare Danger if: always() run: | diff --git a/build.gradle.kts b/build.gradle.kts index f2f16714ea..97e26f956b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -174,4 +174,35 @@ koverMerged { ) } } + + // 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 = 35 + // Setting a max value, so that if coverage is bigger, it means that we have to change minValue. + maxValue = 40 + 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" + } + bound { + minValue = 80 + counter = kotlinx.kover.api.CounterType.LINE + valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE + } + } + } } From 41a5d599faac96ed6036aa5b1b95fe20540fed1e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 7 Feb 2023 16:32:24 +0100 Subject: [PATCH 56/96] Add link to the plugin documentation. --- build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle.kts b/build.gradle.kts index 97e26f956b..6ee2173c49 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 { From 5ca2b475ccfa7f42d407a60fdf9a62c1fe2f1e4b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 7 Feb 2023 16:57:33 +0100 Subject: [PATCH 57/96] Add test for `ChangeServerPresenter` --- features/login/build.gradle.kts | 7 ++ .../changeserver/ChangeServerPresenterTest.kt | 81 +++++++++++++++++++ .../android/libraries/architecture/Async.kt | 3 +- .../auth/FakeAuthenticationService.kt | 56 +++++++++++++ 4 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 features/login/src/test/kotlin/io/element/android/features/login/changeserver/ChangeServerPresenterTest.kt create mode 100644 libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/auth/FakeAuthenticationService.kt 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/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..c8ff3de1ae --- /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.auth.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/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/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..e637ebb722 --- /dev/null +++ b/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/auth/FakeAuthenticationService.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.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 kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +const val A_HOMESERVER = "matrix.org" + +class FakeAuthenticationService : MatrixAuthenticationService { + 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 + } + + override fun getHomeserverOrDefault(): String { + return A_HOMESERVER + } + + override suspend fun setHomeserver(homeserver: String) { + delay(100) + } + + override suspend fun login(username: String, password: String): SessionId { + return SessionId("test") + } +} From 8fb1f2655ea57a3725d79bca70c082b3b341a894 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 7 Feb 2023 17:34:22 +0100 Subject: [PATCH 58/96] Add test for `LoginRootPresenter` --- .../login/root/LoginRootPresenterTest.kt | 135 ++++++++++++++++++ .../auth/FakeAuthenticationService.kt | 22 ++- 2 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 features/login/src/test/kotlin/io/element/android/features/login/root/LoginRootPresenterTest.kt 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..ba3b5e644a --- /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.auth.A_FAILURE +import io.element.android.libraries.matrixtest.auth.A_HOMESERVER +import io.element.android.libraries.matrixtest.auth.A_HOMESERVER_2 +import io.element.android.libraries.matrixtest.auth.A_LOGIN +import io.element.android.libraries.matrixtest.auth.A_PASSWORD +import io.element.android.libraries.matrixtest.auth.A_SESSION_ID +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_LOGIN)) + val loginState = awaitItem() + assertThat(loginState.formState).isEqualTo(LoginFormState(login = A_LOGIN, password = "")) + assertThat(loginState.submitEnabled).isFalse() + initialState.eventSink.invoke(LoginRootEvents.SetPassword(A_PASSWORD)) + val loginAndPasswordState = awaitItem() + assertThat(loginAndPasswordState.formState).isEqualTo(LoginFormState(login = A_LOGIN, 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_LOGIN)) + 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_LOGIN)) + initialState.eventSink.invoke(LoginRootEvents.SetPassword(A_PASSWORD)) + skipItems(1) + val loginAndPasswordState = awaitItem() + authenticationService.givenLoginError(A_FAILURE) + loginAndPasswordState.eventSink.invoke(LoginRootEvents.Submit) + val submitState = awaitItem() + assertThat(submitState.loggedInState).isEqualTo(LoggedInState.LoggingIn) + val loggedInState = awaitItem() + assertThat(loggedInState.loggedInState).isEqualTo(LoggedInState.ErrorLoggingIn(A_FAILURE)) + } + } + + @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/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 index e637ebb722..936bd01545 100644 --- 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 @@ -24,8 +24,16 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf const val A_HOMESERVER = "matrix.org" +const val A_HOMESERVER_2 = "matrix-client.org" +const val A_SESSION_ID = "sessionId" +const val A_LOGIN = "login" +const val A_PASSWORD = "password" +val A_FAILURE = Throwable("error") class FakeAuthenticationService : MatrixAuthenticationService { + private var homeserver: String = A_HOMESERVER + private var loginError: Throwable? = null + override fun isLoggedIn(): Flow { return flowOf(false) } @@ -42,8 +50,12 @@ class FakeAuthenticationService : MatrixAuthenticationService { return null } + fun givenHomeserver(homeserver: String) { + this.homeserver = homeserver + } + override fun getHomeserverOrDefault(): String { - return A_HOMESERVER + return homeserver } override suspend fun setHomeserver(homeserver: String) { @@ -51,6 +63,12 @@ class FakeAuthenticationService : MatrixAuthenticationService { } override suspend fun login(username: String, password: String): SessionId { - return SessionId("test") + delay(100) + loginError?.let { throw it } + return SessionId(A_SESSION_ID) + } + + fun givenLoginError(throwable: Throwable?) { + loginError = throwable } } From 58c59efebf015630058a8da97cfa3ae423a62013 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 7 Feb 2023 17:47:38 +0100 Subject: [PATCH 59/96] Add test for `LogoutPreferencePresenter` --- features/logout/build.gradle.kts | 7 ++ .../logout/LogoutPreferencePresenterTest.kt | 83 +++++++++++++++++++ .../libraries/matrixtest/FakeMatrixClient.kt | 12 ++- 3 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 features/logout/src/test/kotlin/io/element/android/features/logout/LogoutPreferencePresenterTest.kt 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/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..1f1643d981 --- /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.FakeMatrixClient +import io.element.android.libraries.matrixtest.auth.A_FAILURE +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_FAILURE) + 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_FAILURE)) + } + } +} + 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 fb0490cd64..8cf2056c38 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 @@ -26,6 +26,7 @@ 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.FakeRoomSummaryDataSource +import kotlinx.coroutines.delay import org.matrix.rustcomponents.sdk.MediaSource class FakeMatrixClient( @@ -33,6 +34,8 @@ class FakeMatrixClient( val roomSummaryDataSource: RoomSummaryDataSource = FakeRoomSummaryDataSource() ) : MatrixClient { + private var logoutFailure: Throwable? = null + override fun getRoom(roomId: RoomId): MatrixRoom? { return FakeMatrixRoom(roomId) } @@ -49,7 +52,14 @@ class FakeMatrixClient( return FakeMediaResolver() } - override suspend fun logout() = Unit + fun givenLogoutError(failure: Throwable) { + logoutFailure = failure + } + + override suspend fun logout() { + delay(100) + logoutFailure?.let { throw it } + } override fun userId(): UserId = UserId("") From 333f9e9709a8a2a51982ccd8725854d9db0ea85a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 7 Feb 2023 18:32:00 +0100 Subject: [PATCH 60/96] Add test for `TimelinePresenter` --- features/messages/build.gradle.kts | 7 ++ ...pleUnitTest.kt => MessagePresenterTest.kt} | 27 ++++-- .../features/messages/timeline/Test.kt | 31 +++++++ .../timeline/TimelinePresenterTest.kt | 93 +++++++++++++++++++ .../libraries/matrixtest/FakeMatrixClient.kt | 3 +- .../matrixtest/room/FakeMatrixRoom.kt | 5 +- .../matrixtest/timeline/FakeMatrixTimeline.kt | 13 ++- 7 files changed, 166 insertions(+), 13 deletions(-) rename features/messages/src/test/kotlin/io/element/android/features/messages/{ExampleUnitTest.kt => MessagePresenterTest.kt} (53%) create mode 100644 features/messages/src/test/kotlin/io/element/android/features/messages/timeline/Test.kt create mode 100644 features/messages/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt 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/test/kotlin/io/element/android/features/messages/ExampleUnitTest.kt b/features/messages/src/test/kotlin/io/element/android/features/messages/MessagePresenterTest.kt similarity index 53% rename from features/messages/src/test/kotlin/io/element/android/features/messages/ExampleUnitTest.kt rename to features/messages/src/test/kotlin/io/element/android/features/messages/MessagePresenterTest.kt index 83296930a7..c50932f6b8 100644 --- a/features/messages/src/test/kotlin/io/element/android/features/messages/ExampleUnitTest.kt +++ b/features/messages/src/test/kotlin/io/element/android/features/messages/MessagePresenterTest.kt @@ -14,19 +14,28 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package io.element.android.features.messages -import org.junit.Assert.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest import org.junit.Test -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { +class MessagePresenterTest { @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) + fun `present - initial state`() = runTest { + /* + TO BE COMPLETED + val presenter = MessagesPresenter( + FakeMatrixClient(SessionId("sessionId")), + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized) + } + */ } } 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..a1106f738e --- /dev/null +++ b/features/messages/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt @@ -0,0 +1,93 @@ +/* + * 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.libraries.matrixtest.FakeMatrixClient +import io.element.android.libraries.matrixtest.core.A_ROOM_ID +import io.element.android.libraries.matrixtest.room.FakeMatrixRoom +import io.element.android.libraries.matrixtest.timeline.AN_EVENT_ID +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(A_ROOM_ID) + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.timelineItems.size).isEqualTo(0) + } + } + + @Test + fun `present - load more`() = runTest { + val matrixTimeline = FakeMatrixTimeline() + val matrixRoom = FakeMatrixRoom(A_ROOM_ID, 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(A_ROOM_ID, 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() + } + } +} 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 8cf2056c38..c7aab0952a 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 @@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.core.UserId import io.element.android.libraries.matrix.media.MediaResolver import io.element.android.libraries.matrix.room.MatrixRoom import io.element.android.libraries.matrix.room.RoomSummaryDataSource +import io.element.android.libraries.matrixtest.auth.A_SESSION_ID import io.element.android.libraries.matrixtest.media.FakeMediaResolver import io.element.android.libraries.matrixtest.room.FakeMatrixRoom import io.element.android.libraries.matrixtest.room.FakeRoomSummaryDataSource @@ -30,7 +31,7 @@ import kotlinx.coroutines.delay import org.matrix.rustcomponents.sdk.MediaSource class FakeMatrixClient( - override val sessionId: SessionId, + override val sessionId: SessionId = SessionId(A_SESSION_ID), val roomSummaryDataSource: RoomSummaryDataSource = FakeRoomSummaryDataSource() ) : MatrixClient { 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 76da14418d..7e46219126 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 @@ -30,7 +30,8 @@ class FakeMatrixRoom( 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 +39,7 @@ class FakeMatrixRoom( } override fun timeline(): MatrixTimeline { - return FakeMatrixTimeline() + return matrixTimeline } override suspend fun userDisplayName(userId: String): Result { 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 60fa211b1d..665a2eccfb 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,24 +19,35 @@ 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 +const val AN_EVENT_ID_VALUE = "!anEventId" +val AN_EVENT_ID = EventId(AN_EVENT_ID_VALUE) + class FakeMatrixTimeline : MatrixTimeline { override var callback: MatrixTimeline.Callback? get() = null set(value) {} + private var hasMoreToLoadValue: Boolean = true + + fun givenHasMoreToLoad(hasMoreToLoad: Boolean) { + this.hasMoreToLoadValue = hasMoreToLoad + } + override val hasMoreToLoad: Boolean - get() = true + get() = hasMoreToLoadValue override fun timelineItems(): Flow> { return emptyFlow() } override suspend fun paginateBackwards(count: Int): Result { + delay(100) return Result.success(Unit) } From 21084c26b29c3c3cc14a6250205edef976cc04ff Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 7 Feb 2023 22:13:39 +0100 Subject: [PATCH 61/96] fix path --- .github/workflows/quality.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 31630acf97..93a2b8c3f4 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -37,7 +37,7 @@ jobs: with: name: kover-report path: | - */build/reports/kover/merged/verification/errors.txt + **/kover/merged/verification/errors.txt - name: Prepare Danger if: always() run: | From 46963646050b326f38d317fba9eca9f935e03edc Mon Sep 17 00:00:00 2001 From: bmarty Date: Wed, 8 Feb 2023 06:06:17 +0000 Subject: [PATCH 62/96] Import strings from Element Android --- .../src/main/res/values-ja/strings.xml | 768 +++++++++--------- .../src/main/res/values-sv/strings.xml | 1 + .../src/main/res/values/strings.xml | 1 + 3 files changed, 386 insertions(+), 384 deletions(-) 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 d37cc518d7..f51f2ba81a 100644 --- a/libraries/ui-strings/src/main/res/values-ja/strings.xml +++ b/libraries/ui-strings/src/main/res/values-ja/strings.xml @@ -3,8 +3,8 @@ %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,7 +14,7 @@ %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が音声通話を発信しました。 @@ -30,7 +30,7 @@ 全員 (アバターも変更されました) %1$sがルーム名を削除しました - %1$sがルームの説明を削除しました + %1$sがルームのトピックを削除しました %1$sが%2$sにルームへの招待を送りました %1$sが%2$sの招待を受け入れました ** 復号化できません:%s ** @@ -58,8 +58,8 @@ %1$sが参加しました ルームに参加しました %1$sを招待しました - ディスカッションを作成しました - %1$sがディスカッションを作成しました + 会話を作成しました + %1$sが会話を作成しました ルームを作成しました %1$sがルームを作成しました 招待 @@ -87,33 +87,33 @@ 会話 不具合を報告 不具合の内容と状況の説明をお願いします。何をしましたか?何が起こるべきでしたか?実際に起こった事象は何でしょうか? - ここに不具合の内容を記述 + ここに不具合の内容を記述してください スクリーンショットの画像を送信 クラッシュ時のログを送信 ログを送信 開発者が問題を診断するために、このクライアントのログがバグレポートと一緒に送信されます。バグレポートは、ログとスクリーンショットを含めて、公開されることはありません。上記の説明文だけを送信したい場合は、以下のチェックを解除してください。 あなたは不満で端末を振っているようです。バグレポートの画面を開きますか? 前回アプリケーションは正常に停止しませんでした。クラッシュ報告の画面を開きますか? - 不具合を報告しました - 不具合の報告の送信に失敗しました (%s) + バグレポートを送信しました + バグレポートの送信に失敗しました(%s) ルームに参加 音声通話 ビデオ通話 検索 ファイルを送信 - ユーザー名かパスワードが正しくありません + ユーザー名とパスワードの一方あるいは両方が正しくありません メールアドレスの形式が正しくありません このメールアドレスは既に登録されています。 パスワードを忘れましたか? 正しいURLを入力してください 原寸 - 大き目 - 中程度 - 小さ目 - 通話終了 + + + + 通話が終了しました はい いいえ - 続行する + 続行 最新の未読へ移動 ルームから退出 このルームから退出してよろしいですか? @@ -130,7 +130,7 @@ ソースコードを表示 または 確認 - 送信中 (%s%%) + 送信中(%s%%) 削除 参加 このルームで発言する権限がありません。 @@ -145,13 +145,13 @@ グループチャットでのメッセージ ルームへ招待されたとき 通話への招待 - 自動発言プログラム(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を含んでいませんでした ログイン要求が多すぎます @@ -239,7 +239,7 @@ 結果がありません 利用規約 著作権 - 個人情報保護方針 + プライバシーポリシー ホームサーバー IDサーバー この電話番号は既に使用されています。 @@ -248,12 +248,12 @@ 新しいパスワード パスワードの更新に失敗しました %sの全てのメッセージを表示しますか? - 外観 + テーマ 公開端末名 ルームのエンドツーエンド暗号鍵をエクスポート 認証済 このルームに参加していません。 - このルームで権限がありません。 + このルームでそれを行う権限がありません。 ルーム %s は閲覧できません。 ユーザー名 ホームサーバーのURL @@ -261,13 +261,13 @@ Matrixアプリを追加 権限の数値は正の整数で入力してください。 Matrixの連絡先のみ - 通信先が通話の受取に失敗しました。 + 相手が電話に出られませんでした。 情報 - ${app_name}は、音声通話を実行するためにマイクへアクセスするための許可を必要としています。 - ${app_name}はビデオ通話を行うためにカメラとマイクにアクセスする許可を必要としています。 + ${app_name}は、音声通話を実行するためにマイクにアクセスする権限を必要としています。 + ${app_name}は、ビデオ通話を行うためにカメラとマイクにアクセスする権限を必要としています。 \n \n通話をするためには、次のポップアップでアクセスを許可してください。 - 発言を通報 + コンテンツを報告 写真を撮影 動画を撮影 認証を開始 @@ -275,7 +275,7 @@ リクエストにroom_idがありません。 リクエストの送信に失敗しました。 ウィジェットを作成できません。 - ウィジェットをこのルームから削除してもよろしいですか? + ウィジェットをこのルームから削除してよろしいですか? 一致していない場合は、コミュニケーションのセキュリティーが損なわれている可能性があります。 このセッションでは、未認証のセッションに対して暗号化されたメッセージを送信しない。 認証済のセッションにのみ暗号化 @@ -289,28 +289,28 @@ 鍵をローカルファイルにエクスポート ルームの暗号鍵をエクスポート 通話 - 通知あり(音量大) - 通知あり(サイレント) - 不具合の報告 + 通知(音量大) + 通知(サイレント) + バグレポート このユーザーにあなたと同じ権限レベルを与えようとしています。この変更は取り消せません。 \nよろしいですか? - 信用する - 信用しない + 信頼する + 信頼しない フィンガープリント(%s): リモートサーバーのIDを認証できませんでした。 - 誰かが不当にあなたの通信を傍受しているか、あなたの電話がリモートサーバーの証明書を信用していない可能性があります。 - サーバーの管理者が、これは想定されていることであると言っているのであれば、以下のフィンガープリントが、管理者によるフィンガープリントと一致していることを確認してください。 + これは、誰かがあなたのトラフィックを傍受しているか、あなたの電話機がリモートサーバーから提供された証明書を信頼していないことを意味している可能性があります。 + サーバーの管理者が、これは想定されていることであると述べた場合は、以下のフィンガープリントが、管理者によるフィンガープリントと一致することを確認してください。 証明書はあなたの電話により信頼されていたものから変更されています。これはきわめて異常な事態です。この新しい証明書を承認しないことを強く推奨します。 - 証明書は以前信頼されていたものから信頼されていないものへと変更されています。サーバーがその証明書を更新した可能性があります。サーバーの管理者に連絡して、適切なフィンガープリントを確認してください。 + 証明書が以前に信頼されたものから信頼されていないものに変更されました。サーバーが証明書を更新した可能性があります。サーバーの管理者に連絡して、適切なフィンガープリントを確認してください。 サーバーの管理者が上記のものと一致するフィンガープリントを発行した場合にのみ、証明書を承認してください。 検索 - このアプリの情報をシステム設定で表示。 + このアプリケーションの情報をシステム設定で表示。 アプリの情報 自分の表示名を含むメッセージ 自分のユーザー名を含むメッセージ バージョン olmのバージョン - サードパーティーの使用に関する掲示 + 外部ライブラリーのライセンス ホーム画面 逃した通知があるルームをピン止め 未読メッセージがあるルームをピン止め @@ -320,14 +320,14 @@ 未認証 認証 他のセッションのユーザー設定で、以下を比較して承認してください: - ルームのディレクトリを選択 + ルームのディレクトリーを選択 サーバー名 %sサーバー上の全てのルーム 全てのローカルの%sルーム 端末のカメラを使用 コマンドエラー 認識されないコマンド:%s - + オフ 音量大 暗号化されたメッセージ 読み込んでいます… @@ -337,8 +337,8 @@ 全てのメッセージ ホーム画面にショートカットを作成 インラインURLプレビュー - 暗号鍵を要求している新しいセッション \'%s\' を追加しました。 - 未認証のセッション \'%s\' が暗号鍵を要求しています。 + 暗号鍵を要求している新しいセッション\'%s\'を追加しました。 + 未認証のセッション\'%s\'が暗号鍵を要求しています。 作成 ホーム ルーム @@ -358,25 +358,25 @@ %d件の新しいメッセージ アバター - ステッカーを送る + ステッカーを送信 ダウンロード システムアラート - 可能であれば、英語で説明文を記述してください。 + 可能であれば、英語で詳細を記述してください。 音声を送信 - スタンプを送信 - 現在、有効なステッカーパックがありません。 + ステッカーを送信 + 現在、ステッカーパックが有効になっていません。 \n \nいくつか追加しますか? - 申し訳ありません、この操作を完了するための外部アプリが見つかりません。 - あなたの他のセッションに暗号鍵を再要求する。 + 申し訳ありません。この操作を完了するための外部アプリケーションが見つかりません。 + あなたの他のセッションに暗号鍵を再要求。 鍵をこのセッションに送信できるように、メッセージを復号化できる他の端末で${app_name}を起動してください。 %d個選択済 ユーザーをメンションするとき、バイブレーションで通知 送信の前にメディアをプレビュー - アカウントを停止 - 自分のアカウントを停止 + アカウントを無効化 + 自分のアカウントを無効化 分析データを送信 ${app_name}はアプリを改善するため、匿名の分析データを収集します。 @@ -390,36 +390,36 @@ %d個のウィジェットが使用中 必要な変数が見つかりません。 - 動作を表示 + アクションを表示 指定したIDのユーザーをブロック 指定したIDのユーザーのブロックを解除 ユーザーの権限レベルを規定 指定したIDのユーザーの管理者権限を取り消す - 指定したユーザーを現在のルームに招待 - 指定されたアドレスのルームに参加 + 指定したIDのユーザーを現在のルームに招待 + 指定したアドレスのルームに参加 ルームから退出 - ルームの説明を設定 + ルームのトピックを設定 指定したIDのユーザーをこのルームから追放 表示するニックネームを変更 Markdown書式の入/切 Matrixアプリの管理を修正するには %1$sのホームサーバーを引き続き使用するには、利用規約を確認して同意する必要があります。 エラー - 今すぐ確認 - アカウントを停止 - この操作により、あなたのアカウントは永久に使えなくなります。あなたはログインできなくなり、誰も同じユーザーIDを再登録できなくなります。アカウントが参加している全てのルームから退出し、IDサーバーからアカウントの詳細は削除されます。 この操作は取り消せません。 + 確認 + アカウントを無効化 + この操作により、あなたのアカウントは永久に使えなくなります。ログインしたり同じユーザーIDを再登録したりすることはできなくなります。あなたのアカウントは参加している全てのルームから退出し、あなたのIDサーバーからアカウントの詳細が削除されます。<b>この操作は取り消せません。</b> \n -\nアカウントを停止しても、 デフォルトではあなたが送信したメッセージの履歴は消去されません。メッセージの履歴の消去を望む場合は、以下のボックスにチェックを入れてください。 +\nアカウントを無効化しても、<b>デフォルトではあなたが送信したメッセージの履歴は消去されません</b>。メッセージの履歴を消去する場合は、以下のボックスにチェックを入れてください。 \n -\nMatrixのメッセージの見え方は、電子メールと同様のものです。メッセージの履歴を消去すると、あなたが送信したメッセージは、新規または未登録のユーザーに共有されることはありませんが、既にメッセージを取得している登録ユーザーは、今後もそのコピーにアクセスできます。 - アカウントを停止するときに、自分の送信した全てのメッセージの履歴を消去してください(警告: この操作により、今後のユーザーは会話を不完全な形で見ることになります) - アカウントを停止 +\nMatrixのメッセージの見え方は、電子メールと同様のものです。メッセージの履歴を消去すると、あなたがこれまで送信したメッセージは、新規または未登録のユーザーに共有されることはありませんが、既にメッセージを取得している登録ユーザーは、今後もそのコピーにアクセスできます。 + アカウントを無効化する際、全ての送信済のメッセージを消去(警告:今後のユーザーには、不完全な会話が表示されます) + アカウントを無効化 パスワードを入力してください。 このルームは置き換えられており、アクティブではありません。 こちらから継続中の会話を確認 このルームは別の会話の続きです 以前のメッセージを表示するには、ここをクリックしてください - サービス管理者に連絡してください + サービス管理者に連絡 このホームサーバーはリソース制限の1つを超過しているため、 ユーザーがログインできなくなることがあります このホームサーバーはリソースの上限に達しました。 このホームサーバーは月間アクティブユーザーの上限に達しているため、 ユーザーがログインできなくなることがあります @@ -434,12 +434,12 @@ %d+ 展開 折りたたむ - 承諾 - このホームサーバーの方針を確認し承諾してください: + 同意 + このホームサーバーの運営方針を確認し、同意してください: 通話設定画面 着信に${app_name}の既定の着信音を使用 着信音 - 着信音を選んでください: + 着信音を選んでください: 会話から追放 鍵のバックアップ 鍵のバックアップを使用 @@ -449,7 +449,7 @@ リアルタイム性を重視して最適化 バックグラウンド同期を行わない 入力中通知を送信 - 文字入力中であることを他のメンバーに伝えます。 + 文字入力中であることを他のメンバーに表示。 開封確認メッセージを表示 開封確認メッセージをクリックすると、詳細な一覧を確認できます。 Enterキーでメッセージを送信 @@ -467,7 +467,7 @@ 暗号化されたメッセージにアクセスできなくなることを防ぐため、鍵の安全なバックアップはあなたのセッション全てで有効化してください。 暗号化されたメッセージは不要です 鍵をバックアップしています… - 続行しますか? + よろしいですか? バックアップ サインアウトする前に鍵をバックアップしないと、暗号化されたメッセージにアクセスできなくなります。 暗号鍵の管理 @@ -481,11 +481,11 @@ バージョン アルゴリズム 署名 - 通知に関する問題の解決 - システム設定。 + 通知に関する問題解決 + システムの設定。 アカウントの設定。 カスタム設定。 - 起動時に実行 + 端末起動時に開始 バックグラウンド制限の確認 編集 返信 @@ -501,14 +501,14 @@ 作成 名前 公開 - 誰でもこのルームに参加できるようになります + 誰でもこのルームに参加できます 一般 セキュリティーとプライバシー ヘルプと概要 ダイレクトメッセージ (編集済) - 会話を検索… - 全てのメッセージ (音量大) + 会話を絞り込む… + 全てのメッセージ(音量大) 全てのメッセージ メンションのみ ミュート @@ -525,7 +525,7 @@ いったん有効にすると、暗号化を無効にすることはできません。 セキュリティー 詳細を表示 - その他の設定 + その他 管理者としての操作 ルームの設定 通知 @@ -534,16 +534,16 @@ アップロード ルームから退出 - ルームから退室しています… + ルームから退出しています… 管理者 モデレーター カスタム - 招待者 + 招待中 ユーザー %1$sの管理者 %1$sのモデレーター - %1$sのデフォルトユーザー - %2$sのカスタム (%1$d) + %1$sの既定のユーザー + %2$sのカスタム(%1$d) タイムライン エンドツーエンド暗号化を有効にする… 暗号化を有効にする @@ -551,19 +551,19 @@ クロス署名は有効です \n秘密鍵は端末内にあります。 クロス署名は有効です -\n鍵は信頼されています +\n鍵は信頼されています。 \n秘密鍵は不明です - クロス署名は有効です + クロス署名は有効です。 \n鍵は信頼されていません クロス署名は無効です - 有効なセッション + 使用中のセッション 全てのセッションを表示 セッションを管理 このセッションからサインアウト %d件のアクティブなセッション - このログインを認証 + この端末を認証 QRコード はい いいえ @@ -572,11 +572,11 @@ アカウントデータ 削除… 削除の確認 - このイベントを削除してよろしいですか?ルーム名や説明の変更を削除すると、変更が取り消されますのでご注意ください。 - 暗号化は有効です - このルーム内でのメッセージはエンドツーエンド暗号化されます。詳細の確認や認証はユーザーのプロフィールをご確認ください。 + このイベントを削除してよろしいですか?ルーム名やトピックの変更を削除すると、変更が取り消されます。 + 暗号化が有効です + このルーム内でのメッセージはエンドツーエンドで暗号化されます。詳細の確認や認証はユーザーのプロフィールをご確認ください。 暗号化が有効になっていません - 通知設定 + 通知の設定 切断 サインアウトしてよろしいですか? 既読にする @@ -599,10 +599,10 @@ カメラ ギャラリー ステッカー - スパムメッセージです + スパムです 不適切なメッセージです その他の報告… - コンテンツを報告 + このコンテンツを報告 このコンテンツを報告する理由 報告 ユーザーを無視 @@ -610,9 +610,9 @@ 元の大きさのまま画像を送信 - 自分に電話をかけることはできません + 自分に電話を発信することはできません マークダウン書式 - メッセージ送信前にマークダウン書式を適用します。これにより、アスタリスクを使用して斜体のテキストを表示するなどの高度な書式設定が利用できます。 + メッセージ送信前にマークダウン書式を適用します。アスタリスクを使用して斜字体のテキストを表示するなどの高度な書式設定が利用できます。 音声とビデオ 国際電話番号形式で入力してください(電話番号の最初に「+」を付けてください) メールアドレス @@ -621,24 +621,24 @@ 電話番号 あなたのMatrixアカウントに登録されたメールアドレスと電話番号を管理 メールアドレスと電話番号 - 有効化 - このセッションで通知が無効化されています。 -\n${app_name} の設定をご確認ください。 - このセッションで通知は有効化されています。 + 有効にする + このセッションで通知が無効になっています。 +\n${app_name}の設定をご確認ください。 + このセッションで通知は有効になっています。 セッションの設定。 - 有効化 - あなたのアカウントで通知が無効化されています。 + 有効にする + あなたのアカウントで通知が無効になっています。 \nアカウント設定をご確認ください。 - あなたのアカウントで通知は有効化されています。 + あなたのアカウントで通知は有効になっています。 設定を開く - システム設定で通知が無効化されています。 -\nシステム設定をご確認ください。 - システム設定で通知は有効化されています。 + システム設定で通知が無効になっています。 +\nシステム設定を確認してください。 + システム設定で通知は有効になっています。 バッテリー最適化 %d秒 - 拡張設定 + 高度な設定 現在の言語 他の利用可能な言語 メッセージエディター @@ -650,8 +650,8 @@ ルームを作成しています… 招待されています %sからの招待 - 概ね完了しました。%sの画面にも同じシールドアイコンが表示されていますか? - 相手ユーザーの端末のコードをスキャンし、相互に安全性を認証 + 概ね完了しました。%sにも同じマークが表示されていますか? + 相手のユーザーの端末のコードをスキャンし、安全に相互を認証 相手のコードをスキャン スキャンできません 拒否 @@ -660,7 +660,7 @@ 意図しない通話を防止 SSLエラー。 SSLエラー:相手のIDが認証されていません。 - このURLからホームサーバーに接続できませんでした、ご確認ください + このURLからホームサーバーに接続できませんでした。URLを確認してください 有効なMatrixサーバーのアドレスではありません この電話番号は既に登録されています。 シングルサインオンを使用してサインイン @@ -675,12 +675,12 @@ 電話 サウンドデバイスを選択 リアルタイム接続を確立できませんでした。 -\nホームサーバーの管理者に、通話が正常に動作するためにTURNを設定するようご連絡ください。 +\n安定した通話のために、ホームサーバーの管理者にTURNサーバーの設定を依頼してください。 電話を切る 拒否 - 承諾 - ウィジェットを削除できませんでした - ウィジェットを追加できませんでした + 同意 + ウィジェットの削除に失敗しました + ウィジェットの追加に失敗しました ビデオ通話を開始 通話を開始する権限がありません このルームで通話を開始する権限がありません @@ -689,10 +689,10 @@ なし トピック ルーム名 - このルーム内のメッセージはエンドツーエンドで暗号化されていません。 + このルームのメッセージはエンドツーエンドで暗号化されていません。 ここでのメッセージはエンドツーエンドで暗号化されていません。 設定 - あなたにはこのルームの暗号化を有効にする権限がありません。 + このルームの暗号化を有効にする権限がありません。 未読メッセージ タイムラインでのスワイプによる返信を有効にする タイムラインで非表示のイベントを表示 @@ -708,7 +708,7 @@ インテグレーションが無効になっています インテグレーションマネージャー インテグレーションを許可 - FCMトークンが正常に取得されました: + FCMトークンを正常に取得しました: \n%1$s Firebaseトークン Playサービスを修正 @@ -719,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の送信 + 権限の変更 ルーム名の変更 履歴の見え方の変更 ルームの暗号化の有効化 @@ -748,7 +748,7 @@ ルームに関する変更を行うために必要な役割を選択 ルームの権限 権限 - ルームに関する変更を行うために必要な役割を表示し更新します。 + ルームに関する変更を行うために必要な役割を表示し更新。 ブロックを解除すると、ユーザーはルームに再び参加できるようになります。 ユーザーをブロック ブロックする理由 @@ -760,48 +760,48 @@ ユーザーを追放 このユーザーの招待をキャンセルしてよろしいですか? 招待をキャンセル - このユーザーを解除すると、そのユーザーからの全てのメッセージが再び表示されます。 + このユーザーの無視を解除すると、そのユーザーからの全てのメッセージが再び表示されます。 ユーザーの無視を解除 このユーザーを無視すると、あなたが共有しているルームからそのユーザーのメッセージが削除されます。 \n \nこの操作は、設定からいつでも元に戻すことができます。 ユーザーを無視 降格 - あなたは自分自身を降格させようとしています。この変更は取り消せません。あなたがルームの中で最後の特権ユーザーである場合、特権を再取得することはできません。 + あなたは自分自身を降格させようとしています。この変更は取り消せません。あなたがルームの中で最後の特権ユーザーである場合、特権を再取得することはできなくなります。 降格しますか? 招待をキャンセル - このルームは公開されていません。 招待がなければ再び参加することはできません。 + このルームは公開されていません。再度参加するには、招待が必要です。 連絡先へのアクセスを許可します。 QRコードをスキャンするには、カメラへのアクセスを許可する必要があります。 通話を保留しました %sが通話を保留しました 保留 - 通話をやり直す + 通話を再開 ビデオ通話が行われています… - 有効な認証情報がないため、権限がありません - ${app_name} 呼び出し失敗 - ルームディレクトリの全てのルームを表示(露骨なコンテンツのあるルームを含む)する。 + 有効な認証情報がないため、許可されていません + ${app_name}が呼び出しに失敗しました + ルームディレクトリーの全てのルームを表示(露骨なコンテンツのあるルームを含む)。 露骨なコンテンツのあるルームを表示 - ルームディレクトリ + ルームディレクトリー 新着情報 - 非公開 + 非公開にする 切り替える 追加 %1$sがエンドツーエンド暗号化(認識されていないアルゴリズム %2$s)を有効にしました。 エンドツーエンド暗号化(認識されていないアルゴリズム %1$s)を有効にしました。 - 会話を始める + 会話を開始 %1$sがエンドツーエンド暗号化を有効にしました。 エンドツーエンド暗号化を有効にしました。 - ゲストがルームに参加するのを拒否しました。 - %1$sはゲストがルームに参加するのを拒否しました。 - ゲストがルームに参加するのを拒否しました。 - %1$sはゲストがルームに参加するのを拒否しました。 + ゲストがルームに参加することを拒否しました。 + %1$sはゲストがルームに参加することを拒否しました。 + ゲストがルームに参加することを拒否しました。 + %1$sはゲストがルームに参加することを拒否しました。 ここにゲストが参加することを許可しました。 %1$sはここにゲストが参加することを許可しました。 - ゲストがルームに参加するのを許可しました。 - %1$sはゲストがルームに参加するのを許可しました。 - システムデフォルト - このルームのメインおよび代替のアドレスを変更しました。 + ゲストがルームに参加することを許可しました。 + %1$sはゲストがルームに参加することを許可しました。 + システムの既定 + このルームのメインおよび代替アドレスを変更しました。 このルームの代替アドレスを変更しました。 このルームの代替アドレス %1$s を削除しました。 @@ -850,7 +850,7 @@ %sがこのルームのサーバーのアクセス制御リストを変更しました。 ・IPリテラルに一致するサーバーはブロックされています。 - ・IPリテラルに一致するサーバーを許可します。 + ・IPリテラルに一致するサーバーを許可されています。 ・%sに一致するサーバーは許可されています。 ・%sに一致するサーバーはブロックされています。 %sがこのルームのサーバーアクセス制御リストを設定しました。 @@ -881,8 +881,8 @@ \n会話を読み込んでいます \n多くのルームに参加している場合、読み込みに時間がかかるかもしれません %1$sがこのルームから退出しました。理由:%2$s - このルームに参加しました。理由:%1$s - %1$sがこのルームに参加しました。理由:%2$s + 参加しました。理由:%1$s + %1$sが参加しました。理由:%2$s このルームに参加しました。理由:%1$s %1$sがこのルームに参加しました。理由:%2$s %1$sがあなたを招待しました。 理由:%2$s @@ -890,8 +890,8 @@ %1$sが%2$sを招待しました。 理由:%3$s あなたの招待です。理由:%1$s %1$sの招待です。理由:%2$s - メッセージを送っています… - メッセージを送りました + メッセージを送信しています… + メッセージを送信しました 初期同期: \nアカウントデータをインポートしています 初期同期: @@ -910,7 +910,7 @@ %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) 既定 @@ -933,12 +933,12 @@ %1$sにルームへの招待を送りました ルームのアバターを削除しました %1$sがルームのアバターを削除しました - ルームの説明を削除しました + ルームのトピックを削除しました ルーム名を削除しました - ディスカバリー設定を管理します。 + ディスカバリーの設定を管理。 ディスカバリー(発見) これにより、現在のキーまたはフレーズが置き換えられます。 - 新しいセキュリティーキーを生成するか、既存のバックアップに新しいセキュリティーフレーズを設定します。 + 新しいセキュリティーキーを生成するか、既存のバックアップに新しいセキュリティーフレーズを設定してください。 サーバー上の暗号鍵をバックアップして、暗号化されたメッセージとデータへのアクセスが失われるのを防ぎましょう。 メッセージ作成画面に絵文字キーボードを開くためのボタンを追加 絵文字キーボードを表示 @@ -946,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の色、振動、音を選択してください… 通知(サイレント)を設定 通話の通知を設定 @@ -973,17 +973,17 @@ \nこのエラーは${app_name}の管理外です。 これはいくつかの理由で発生する可能性があります。 後で再試行するとうまくいくかもしれません。システム設定でGoogle Playサービスのデータ使用量が制限されていないか、端末の時刻が正しいかどうかを確認してください。カスタムROMで生じることもあります。 ${app_name}はバッテリー最適化の影響を受けません。 制限を無効にする - 起動時の開始を有効にする + 端末起動時の開始を有効にする 端末を再起動するとサービスが開始します。 通知がクリックされました! 通知をクリックしてください。 通知が表示されない場合は、システム設定を確認してください。 - 通知を表示 + 通知の表示 通知を表示しています。 クリックしてください! プッシュ通知の受信に失敗しました。 アプリケーションを再インストールすると解決するかもしれません。 アプリケーションはプッシュ通知を受信しています アプリケーションはプッシュ通知を待機しています プッシュ通知のテスト - FCMトークンのホームサーバーへの登録に失敗しました: + FCMトークンのホームサーバーへの登録に失敗しました: \n%1$s FCMトークンがホームサーバーに登録されました。 トークンの登録 @@ -994,20 +994,20 @@ \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が閲覧済 メッセージをマークダウンとして解釈せず、プレーンテキストとして送信 ファイルとして保存 共有 @@ -1023,7 +1023,7 @@ ユーザー名を入力してください。 無視 共有 - 続行するには利用規約を承認する必要があります。 + 続行するには、このサービスの利用規約に同意する必要があります。 全てブロック 許可 ルームID @@ -1041,7 +1041,7 @@ 新しいイベント 不明なIP - %2$d個の鍵のうち%1$d個のインポートに成功。 + %2$d個の鍵のうち%1$d個をインポートしました。 鍵のバックアップを管理 鍵のエクスポートに成功しました @@ -1061,7 +1061,7 @@ %sはあなたを招待しています このルームでグループ通話を開始する権限がありません 音声通話を開始 - 安全バックアップを設定 + セキュアバックアップを設定 鍵のバックアップで管理 鍵のバックアップを使用 暗号化されたメッセージとデータへのアクセスが失われるのを防ぎましょう @@ -1070,9 +1070,9 @@ バックアップの状態を確認しています バックアップを削除しています… このセッションで鍵のバックアップを使用するには、パスフレーズまたはリカバリーキーでバックアップを復元してください。 - バックアップには未認証のセッション %s による不正な署名があります - バックアップには認証済のセッション %s による不正な署名があります - バックアップには未認証のセッション %s による有効な署名があります + バックアップには未認証のセッション %s による不正な署名があります。 + バックアップには認証済のセッション %s による不正な署名があります。 + バックアップには未認証のセッション %s による有効な署名があります。 バックアップには認証済のセッション %s による署名があります。 バックアップにはこのセッションによる有効な署名があります。 バックアップには%sというIDの不明のセッションによる署名があります。 @@ -1103,12 +1103,12 @@ リカバリーキーを入力 リカバリーキーを使用 ログアウトしたりこの端末を失くしたりすると、メッセージにアクセスできなくなる可能性があります。 - 続行しますか? + よろしいですか? 予期しないエラー リカバリーキー パスフレーズを使用してリカバリーキーを生成中です。数秒かかることがあります。 リカバリーキーを共有… - コピーをしてください + コピーしてください 中止 上書き 別のセッションで鍵のバックアップを既に設定しているようです。上書きしますか? @@ -1125,7 +1125,7 @@ 鍵のコピーを暗号化してホームサーバーに保存します。バックアップを保護するためにパスフレーズを設定してください。 \n \n最大限のセキュリティーを確保するために、アカウントのパスワードと異なるものに設定することが大切です。 - パスフレーズを使用してバックアップを保護します。 + パスフレーズを使用してバックアップを保護しましょう。 暗号化されたメッセージは、エンドツーエンドの暗号化によって保護されています。これらの暗号化されたメッセージを読むための鍵を持っているのは、あなたと受信者だけです。 \n \n鍵を失くさないよう、鍵を安全にバックアップしてください。 @@ -1134,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件の通知 @@ -1181,7 +1181,7 @@ 暗号化されたメッセージの復元 ルームのバージョン - ブロックされたユーザー%d人 + %d人のブロックされたユーザー このルームのあるスペースのメンバーは、誰でもこのルームを発見し参加できます。ルームをスペースに追加できるのは、ルームの管理者だけです。 スペースのメンバーのみ @@ -1191,23 +1191,23 @@ 非公開 不明のアクセス設定(%s) 誰でもルームにノックができ、メンバーがその参加を承認または拒否できます - 現在のルームディレクトリの見え方を取得できません(%1$s)。 - このルームを%1$sのルームディレクトリに公開しますか? + 現在のルームディレクトリーの見え方を取得できません(%1$s)。 + このルームを%1$sのルームディレクトリーに公開しますか? このアドレスを非公開にする このアドレスを公開 - アドレスを設定すれば、他のユーザーがあなたのホームサーバー (%1$s) を通じてこのルームを見つけられるようになります。 + アドレスを設定すると、他のユーザーがあなたのホームサーバー(%1$s)を通じてこのルームを見つけられるようになります。 ローカルアドレス - 新しい公開アドレス(例: #alias:server) + 新しい公開アドレス(例:#alias:server) 他の公開アドレスはまだありません。以下から追加できます。 他の公開アドレスはまだありません。 - \"%1$s\"を非公開にしますか? + アドレス\"%1$s\"を非公開にしますか? 公開 手動で新しいアドレスを公開 他の公開アドレス: - 公開アドレスを通して、どのサーバーのどのユーザーでも、このルームに参加できます。アドレスを公開するには、まずローカルアドレスとして設定する必要があります。 + 公開アドレスを通して、どのサーバーのユーザーでも、このルームに参加できます。アドレスを公開するには、まずローカルアドレスとして設定する必要があります。 公開アドレス - このルームのアドレスと、ルームディレクトリにおける見え方を管理できます。 - スペースのアドレスを管理できます。 + このルームのアドレスと、ルームディレクトリーにおける見え方を管理。 + このスペースのアドレスを管理。 スペースのアドレス ルームのアドレス ゲストの参加を許可 @@ -1219,27 +1219,27 @@ ホームサーバーAPIのURL アクセスを取り消す 表示 - このルームのメッセージはエンドツーエンドで暗号化されています。 + このチャットのメッセージはエンドツーエンドで暗号化されています。 ダイレクトメッセージ 新しいダイレクトメッセージを送信 - メールアドレス(任意) - メールアドレス - アカウント復旧用のメールアドレスを設定します。後からオプションで知人に見つけてもらえるようにできます。 + 電子メール(任意) + 電子メール + アカウント復旧用のメールアドレスを設定します。後からこのメールアドレスによって知人に見つけてもらえるようにできます(任意)。 メールアドレスを設定 メールアドレスを確認しました 発見可能なメールアドレス 続行するには利用規約を承認してください ホームサーバーの利用規約を承認したら、再試行してください。 - 次に + 次へ 次へ - 次に - 次に - 次に + 次へ + 次へ + 次へ ユーザー名を選択してください。 - ユーザー名かパスワードが正しくありません。入力されたパスワードがスペースによって開始しているか終了しているので、確認してください。 + ユーザー名やパスワードが正しくありません。入力されたパスワードがスペースで開始または終了しています。確認してください。 そのユーザー名は既に使用されています ユーザー名 - ユーザー名またはメールアドレス + ユーザー名または電子メール %sでサインイン %sでサインアップ %sで続行 @@ -1255,8 +1255,8 @@ カスタムと高度な設定 組織向けのプレミアムホスティング 組織向けのプレミアムホスティング - 最大のパブリックサーバーで、数百万人に無料で参加 - メールと同じように、アカウントには1つのホームがありますが、誰とでも話すことができます + 最大の公開サーバーで、数百万人に無料で参加 + 電子メールと同じように、アカウントには1つのホームがありますが、誰とでも話すことができます サーバーを選択 始めましょう エクスペリエンスを拡張およびカスタマイズ @@ -1266,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\"からエンドツーエンド暗号鍵をインポート。 鍵のバックアップデータの取得中にエラーが発生しました 信頼情報の取得中にエラーが発生しました ルームが作成されましたが、一部の招待が送信されていません。理由: @@ -1297,7 +1297,7 @@ \n%s ルームの設定 トピック - ルームの説明(任意) + ルームのトピック(任意) ルーム名 このルームはプレビューできません。参加しますか? 現在、このルームにはアクセスできません。 @@ -1308,7 +1308,7 @@ 不正な形式のイベントです。表示できません ルームの管理者によってモデレートされたイベント リアクション - リアクションを見る + リアクションを表示 リアクションを追加 同意 リアクション @@ -1319,14 +1319,14 @@ 未読メッセージはありません 未読はありません! %sがセッションの認証を要求しています - リトライ - 他のホームサーバーに接続しようとしているようですね。サインアウトしますか? + 再試行 + 他のホームサーバーに接続しようとしているようです。サインアウトしますか? IDサーバーを使用していません 不明なエラー このルームを含む参加済のスペース - このルームにアクセスできるスペースを決定します。スペースが選択されると、そのメンバーはルーム名を見つけて参加できます。 + このルームにアクセスできるスペースを選択してください。選択したスペースのメンバーはルーム名を検索し、参加できるようになります。 了解 - 完了しました! + 認証しました! メッセージの新しい鍵 暗号化されたメッセージを決して失わないために セキュアバックアップ @@ -1340,7 +1340,7 @@ アクセス可能なスペース スペースのメンバーに発見とアクセスを許可します。 スペース %s のメンバーが検索、プレビュー、参加できます。 - 非公開(招待のみ) + 非公開(招待者のみ参加可能) 既定のメディアソース 既定の圧縮率 ルームのアップグレード @@ -1356,7 +1356,7 @@ 自分の表示名 グループチャットで暗号化されたメッセージ 1対1のチャットで暗号化されたメッセージ - 以下がメッセージに含まれる場合に通知 + 以下の場合に通知 その他 メンションとキーワード 通知のデフォルト @@ -1366,32 +1366,32 @@ %d件の不在着信(音声) - 既定に設定して次回から確認しない + 既定に設定し、次回から確認しない 鍵の共有リクエストの履歴を送信 結果がありません - 自分に電話をかけることはできません。参加者が招待を受け入れるまでお待ちください + 自分に電話を発信することはできません。参加者が招待を受け入れるまでお待ちください ミーティングはJitsiのセキュリティーとパーミッションポリシーを使用します。ミーティング中は、現在ルームにいる全ての人に招待が表示されます。 権限がありません 音声メッセージを送信するには、マイクの権限を許可してください。 この操作を実行するには、システム設定からカメラの権限を許可してください。 この操作を実行するための権限がありません。システム設定から権限を付与してください。 IDサーバーに接続できませんでした - IDサーバーのURLを入力 + IDサーバーのURLを入力してください 同意する - 同意を撤回 + 同意を取り消す あなたの連絡先から他のユーザーを発見するために、メールアドレスや電話番号をこのIDサーバーに送信することに同意しています。 メールと電話番号を送信 - %sにメールを送りました。メールを確認してリンクをクリックしてください - %sにメールを送りました。メールの確認リンクをクリックしてください + %sにメールを送りました。メールを確認して承認リンクをクリックしてください + %sにメールを送りました。メールを確認して承認リンクをクリックしてください 発見可能な電話番号 IDサーバーとの接続を解除すると、他のユーザーによって見つけられなくなり、また、メールアドレスや電話で他のユーザーを招待することもできなくなります。 電話番号を追加すると、発見可能に設定する電話番号を選択できるようになります。 メールアドレスを追加すると、発見可能に設定するメールアドレスを選択できるようになります。 - 現在、IDサーバーを使用していません。自分の連絡先を見つけたり、連絡先から見つけてもらったりするには、以下でIDサーバーを設定してください。 - 現在%1$sを使って自分の連絡先を見つけたり、連絡先から見つけてもらったりできるようにしています。 + 現在、IDサーバーを使用していません。連絡先を見つけたり、連絡先から見つけてもらったりするには、以下でIDサーバーを設定してください。 + 現在%1$sを使用して、自分の連絡先を見つけたり、連絡先から見つけてもらったりできるようにしています。 IDサーバーを変更 IDサーバーの設定 - IDサーバーの切断 + IDサーバーから切断 IDサーバー ボット、ブリッジ、ウィジェット、ステッカーパックを使用 他の人が見つけられるように @@ -1399,18 +1399,18 @@ 編集履歴を表示 提案 リンクをクリップボードにコピーしました - メイン画面に未読通知専用のタブを追加する。 + メイン画面に未読通知専用のタブを追加。 ルーム名を検索 - 名前もしくはID (#例えば:matrix.org) - ルームディレクトリを見る + 名前もしくはID(#example:matrix.org) + ルームディレクトリーを見る 新しいルームを作成 お探しのものが見つかりませんか? - あなたの提案をここに書いてください - ご意見・ご感想をお聞かせください。 + 提案をここに書いてください + 意見・感想を聞かせてください。 提案する - フォーマット: - URL: - セッションの表示名: + フォーマット: + URL: + セッションの表示名: 以下のうちいずれかが流出、あるいはハッキングされた恐れがあります。 \n \n- あなたのパスワード @@ -1419,9 +1419,9 @@ \n- あなたの端末が使用しているインターネット接続 \n \n設定画面からパスワードとリカバリーキーを早急に変更することを推奨します。 - メールアドレス + 電子メール アドレス - 続行する + 続行 ファイル このユーザーはスペースから追放されます。 \n @@ -1442,7 +1442,7 @@ %sとのビデオ通話 呼び出しています… ホームサーバーを選択 - %sのURLにあるホームサーバーに接続できません。リンクを確認するか、手動でホームサーバーを選択してください。 + URL %s のホームサーバーに接続できません。リンクを確認するか、手動でホームサーバーを選択してください。 後で スペース スレッドから @@ -1452,7 +1452,7 @@ PINコードを有効にする これを「招待者のみ参加可能」に設定しました。 ルームの設定 - コンテンツが報告されました + コンテンツを報告しました ヘルプとサポート ヘルプ ${app_name}の運営方針 @@ -1477,7 +1477,7 @@ 種類 確認済 選択済 - ビデオ + 動画 画像 スクリーンショット 接続 @@ -1503,7 +1503,7 @@ 警告 警告 成功しました! - 続行する + 続行 警告! 位置情報 メディア @@ -1542,11 +1542,11 @@ 位置情報を共有 スペースに関する変更を行うために必要な役割を更新する権限がありません スペースに関する変更を行うために必要な役割を選択 - スペースに関する変更を行うために必要な役割を表示し更新します。 + スペースに関する変更を行うために必要な役割を表示し更新。 絞り込む スレッド スレッド - スペースをアップグレード + スペースのアップグレード スペース名の変更 スペースの権限 応答がありません @@ -1564,29 +1564,29 @@ ルーム名を設定 アカウントの設定 キーワード - ルームから退出しました! + ルームから退出しています! 吹き出しでメッセージを表示 電子メールによる通知 - セッションからサインアウトしました! + セッションからサインアウトしています! なし メンションとキーワードのみ ルームのスレッドを絞り込む 退出 携帯端末では、暗号化されたルームでのメンションとキーワードの通知は受信できません。 - ルームの暗号化の有効化 + スペースの暗号化の有効化 スペースのメインアドレスの変更 スペースのアバターの変更 アンケートを作成 アンケートを作成 - 暗号化が正しく設定されていないため、メッセージを送ることができません。クリックして設定を開いてください。 - 暗号化が正しく設定されていないため、メッセージを送ることができません。管理者に連絡して、暗号化を正しい状態に復元してください。 - %2$dの%1$d - あなたは既にこのスレッドを見ています! - ルームに表示 - ルームに表示 + 暗号化が正しく設定されていないため、メッセージを送信できません。クリックして設定を開いてください。 + 暗号化が正しく設定されていないため、メッセージを送信できません。管理者に連絡して、暗号化を正しい状態に復元してください。 + %2$d個のうち%1$d個 + 既にこのスレッドを表示しています! + ルーム内で表示 + ルーム内で表示 スレッドを表示 このルームへの参加は許可されていません - "トピック: " + "トピック: " トラブルシューティング 鍵のバックアップのバナーを閉じる キーワードに「%s」を含めることはできません @@ -1636,8 +1636,8 @@ 音声メッセージを録音 あなたのホームサーバーの運営方針 一番下に移動 - %sが読みました - %1$sと%2$sが読みました + %sが閲覧済 + %1$sと%2$sが閲覧済 ファイルを使用 暗号化を有効にしますか? キャンセルしました @@ -1667,20 +1667,20 @@ 新しいPINコード PINコードを再設定 PINコードを忘れましたか? - PINコードを入力 + PINコードを入力してください PINコードを確認 サードパーティー製ライブラリー これはいつでも設定から無効にできます 私たちは、情報を第三者と共有することはありません - 私たちは、アカウントのデータを記録したり分析したりすることはありません + 私たちは、アカウントのいかなるデータも記録したり分析したりすることは<b>ありません</b> ${app_name}の改善を手伝う このルームを「リンクを知っている人が参加可能」に設定しました。 - どのユーザーも無視していません + 無視しているユーザーはいません キーワードを入力するとリアクションを検索できます。 変更を加えませんでした %1$sは変更を加えませんでした - %d人のユーザーが読みました + %d人のユーザーが閲覧済 スペースへのアクセス このサーバーは運営方針を提供していません。 @@ -1697,7 +1697,7 @@ 連絡先を発見するには、連絡先のデータ(メールアドレスと電話番号)をあなたのIDサーバーに送信する必要があります。プライバシーの保護のため、データは送信前にハッシュ化されます。 メールアドレスと電話番号を%sに送信 このIDサーバーは運営方針を提供していません - IDサーバーの運営方針を隠す + IDサーバーの運営方針を表示しない IDサーバーの運営方針を表示 アカウントの新しいパスワードを設定… シェイクを検出しました! @@ -1710,18 +1710,18 @@ 復号エラーを自動的に報告する。 変更を有効にするにはアプリケーションの再起動が必要です。 LaTeXによる数学表記を有効にする - 以下が含まれる場合に通知 + 以下の場合に通知 アップグレードすると、このルームの新しいバージョンが作成されます。今ある全てのメッセージは、アーカイブしたルームに残ります。 誰がアクセスできますか? 通知は%1$sで管理できます。 暗号化されたルームでのメンションとキーワードによる通知は、携帯端末では利用できません。 ユーザーを無視し、そのメッセージを非表示に設定 - %sとのコマンドは認識されていますが、スレッドではサポートされていません。 + コマンド\"%s\"は認識されていますが、スレッドではサポートされていません。 誰でもスペースを発見し参加できます 法的情報 ユーザーに関する情報を表示 - このルームにおいてのみアバターを変更 - このルームにおいてのみ表示名を変更 + このルームでのみアバターを変更 + このルームでのみ表示名を変更 ユーザーの無視を解除し、以後のメッセージを表示 続行するには%sを入力してください 有効なリカバリーキーではありません @@ -1733,12 +1733,12 @@ アンケートの終了後に結果を公開 結果はアンケートを終了した後でのみ明らかにされます 以下で開く - 暗号化のアップグレードが利用可能です + 暗号化のアップグレードが利用できます SSSSキーをリカバリーキーから生成しています ${app_name} iOS \n${app_name} Android - ${app_name}ウェブ版 -\n${app_name}デスクトップ版 + ${app_name} ウェブ版 +\n${app_name} デスクトップ版 リカバリーキーを選択、直接入力、あるいはクリップボードからペースト リカバリーキーを使用 暗号化されたルームでのみサポート @@ -1775,8 +1775,8 @@ 録音を削除 音声メッセージがアクティブの間は返信や編集はできません このアンケートを削除してよろしいですか?一度削除すると復元することはできません。 - 共有データの取り扱いに失敗しました - 回転とクロップ + 共有データを取り扱えませんでした + 回転とトリミング ルームを探す 既存のルームとスペースを追加 スレッドのメッセージを有効にする @@ -1789,16 +1789,16 @@ \n既存のスペースを別のスペースに追加できます。 あなたのホームサーバーはまだスペースをサポートしていないようです 画像を追加 - このコンテンツは不適切な投稿として報告されています。 + このコンテンツを不適切な投稿として報告しました。 \n \nこのユーザーのコンテンツをこれ以上見たくなければ、ユーザーを無視してそのメッセージを非表示にできます。 - このコンテンツはスパムとして報告されています。 + このコンテンツをスパムとして報告しました。 \n \nこのユーザーのコンテンツをこれ以上見たくなければ、ユーザーを無視してそのメッセージを非表示にできます。 - このコンテンツが報告されています。 + このコンテンツを報告しました。 \n \nこのユーザーのコンテンツをこれ以上見たくなければ、ユーザーを無視してそのメッセージを非表示にできます。 - %1$sにより%2$sに + %1$sが%2$sにアップロード 質問あるいはトピック アンケートの質問あるいはトピック 少々お待ちください。少し時間がかかるかもしれません。 @@ -1851,13 +1851,13 @@ エンドツーエンドで暗号化されており、登録に電話番号は不要です。広告もデータ収集もありません。 会話の保存先を自分で決められ、自分で管理できる独立したコミュニケーション。Matrixをもとに。 オンライン上でも対面の会話と同じレベルでプライバシーを守る、安全で独立したコミュニケーション。 - 安全なメッセージ。 + 安全なメッセージのやりとり。 主導権を握るのは、あなたです。 ${app_name}の使用に関するヘルプ 詳細なログは、イライラシェイクでログを送信する際に、より多くのログを提供することで、開発者にとっての助けになります。有効にした場合でも、メッセージの内容やその他のプライベートな情報は記録されません。 ルームのアップグレードは高度な作業であり、不具合や欠けている機能、セキュリティー上の脆弱性がある場合に推奨されます。 \nアップグレードは通常、ルームがサーバー上で処理される仕方にだけ影響します。 - 一度有効にしたルームの暗号化は無効にすることはできません。暗号化されたルームで送信されたメッセージは、サーバーからは見ることができず、そのルームのメンバーだけが見ることができます。暗号化を有効にすると、多くのボットやブリッジが正常に動作しなくなる場合があります。 + 一度有効にしたルームの暗号化は無効にすることはできません。暗号化されたルームで送信されたメッセージは、サーバーからは閲覧できず、そのルームのメンバーだけが閲覧できます。暗号化を有効にすると、多くのボットやブリッジが正常に動作しなくなる可能性があります。 %sして、このルームを皆に紹介しましょう。 このコードを共有し、スキャンして追加してもらい、会話を始めましょう。 正しい参加者が%sにアクセスできるようにしましょう。 @@ -1884,8 +1884,8 @@ 合計%1$d票の投票に基づく最終結果 - 新しいセッションが認証されました。セッションは暗号化されたメッセージにアクセスでき、他のユーザーには信頼済として表示されます。 - このルームを同じホームサーバー上で組織内のチームとのコラボレーションにのみ使用するなら、このオプションを有効にするといいかもしれません。これは後から変更できません。 + 新しいセッションが認証されました。セッションは暗号化されたメッセージにアクセスすることができます。また、セッションは他のユーザーに「信頼済」として表示されます。 + このルームを同じホームサーバー上で組織内のチームとのコラボレーションにのみ使用する場合、このオプションを有効にするといいかもしれません。これは後から変更できません。 %sに属していないユーザーによるこのルームへの参加を、今後永久に拒否 プレーンテキストメッセージの前に ( ͡° ͜ʖ ͡°) を付ける このメールアドレスのドメインの登録は許可されていません @@ -1898,16 +1898,16 @@ デバッグ用の情報を画面に表示 初期同期中… 説明文が短すぎます - サインインして暗号鍵を取り戻さなければ、暗号化されたメッセージにアクセスできなくなります。 + サインインして暗号鍵を復旧しないと、暗号化されたメッセージにアクセスできなくなります。 再サインイン - 有効なホームサーバーを発見できません。識別子を確認してください + 有効なホームサーバーを発見できません。IDを確認してください どこかのホームサーバーで既にアカウントを登録している場合、以下でMatrix ID(例:@user:domain.com)とパスワードを使用してください。 入力したコードが正しくありません。確認してください。 - これは正しいユーザー識別子ではありません。正しいフォーマットは「@user:homeserver.org」です。 - パスワードをお忘れの場合、戻ってパスワードを再設定してください。 + これは正しいユーザーIDではありません。正しいフォーマットは「@user:homeserver.org」です。 + パスワードを忘れた場合、戻ってパスワードを再設定してください。 メールボックスを確認してください カスタムサーバーに接続 - 既にアカウントを持っています + 既にアカウントがあります 既存のサーバーに参加しますか? この質問をスキップ 友達と家族 @@ -1928,14 +1928,14 @@ 非公開で招待が必要なルームは表示されていません。 \nルームを追加する権限はありません。 非公開で招待が必要なルームは表示されていません。 - 知人に見つけてもらえるように電話番号を設定できます。任意です。 + 知人に見つけてもらえるように電話番号を設定できます(任意)。 メッセージキー 復旧用のパスフレーズ - ニックネームの色を変更 + 表示名の色を変更 パスワードはまだ変更されていません。 \n \n変更作業を中止しますか? - %1$sに確認メールを送信しました。 + %1$sに認証メールを送信しました。 メールボックスを確認してください サインインに戻る 元の大きさのままメディアファイルを送信 @@ -1948,15 +1948,15 @@ 信頼されていません 認証の要求 %sがキャンセルしました - 既読 + 閲覧済 認証 - メールアドレスが正しくないようです + メールアドレスの形式が正しくありません 国際電話番号の形式を使用してください。 国際電話番号は「+」から始めてください コードを%1$sに送信しました。以下に入力して認証してください。 このメールアドレスはどのアカウントにも登録されていません パスワードを変更すると、全てのセッションでのエンドツーエンド暗号鍵がリセットされ、暗号化されたメッセージ履歴が読めなくなります。パスワードを再設定する前に、鍵のバックアップを設定するか、他のセッションからルームの鍵をエクスポートしておいてください。 - パスワードの再設定を確認するために確認メールを送信します。 + パスワードの再設定を確認するために認証メールを送信します。 このメールアドレスはどのアカウントにも登録されていません。 このアプリでは、このホームサーバーにアカウントを作成できません。 \n @@ -1964,12 +1964,12 @@ 申し訳ありませんが、このサーバーはアカウントの新規登録を受け入れていません。 このアプリではこのホームサーバーにサインインできません。このホームサーバーは次のサインインの方法に対応しています:%1$s \n -\nウェブクライエントを使用してサインインしますか? - %1$sを読み込み中にエラーが発生しました(%2$d) - 利用したいサーバーのアドレスを入力してください - 利用したいModular Elementまたはサーバーのアドレスを入力してください +\nウェブクライアントを使用してサインインしますか? + ページ %1$s を読み込んでいる際にエラーが発生しました(%2$d) + 使用したいサーバーのアドレスを入力してください + 使用したいModular Elementまたはサーバーのアドレスを入力してください 迷っていますか?%s - みんなと繋がる手助けをいたします。 + みんなと繋がる手助けをいたします 自分のコード 招待を%1$sと他%2$d人に送信しました @@ -2000,13 +2000,13 @@ QRコードがスキャンされていません! 内容を通知に表示 プッシュ通知は無効になっています - ユーザーのブロックを解除できませんでした + ユーザーのブロックの解除に失敗しました %1$sへの招待を取り消しますか? 連絡先を取得しています… RiotはElementになりました! このメッセージにアクセスできません アバターを設定 - IDサーバーのURLを入力 + IDサーバーのURLを入力してください マイクのミュートを解除 マイクをミュート %1$sを使用 @@ -2025,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 @@ -2052,10 +2052,10 @@ リクエストが多すぎます。%1$d秒後に再試行できます… - このホームサーバーは古いバージョンです。管理者にアップグレードを要請してください。続行できますが、いくつかの機能が正しく作動しない可能性があります。 + このホームサーバーは古いバージョンです。管理者にアップグレードを依頼してください。続行できますが、いくつかの機能が正しく作動しない可能性があります。 ホームサーバーのバージョンが古すぎます - ただいま%1$sにメールを送信しました。 -\nアカウント登録を続行するにはメール内のリンクをクリックしてください。 + %1$sにメールを送信しました。 +\nアカウント登録を続行するには、メール内のリンクをクリックしてください。 CAPTCHA認証を行ってください アカウントがまだ作成されていません。登録を中止しますか? %1$sにアカウント登録 @@ -2074,8 +2074,8 @@ サーバー上の暗号鍵をバックアップして、暗号化されたメッセージとデータへのアクセスが失われるのを防ぎましょう。 いまキャンセルすると、ログインできなくなった際に、暗号化されたメッセージとデータを失ってしまう可能性があります。 \n -\nまた、設定から、安全なバックアップの設定や鍵の管理を行うことができます。 - USBメモリーもしくはバックアップドライブに保存 +\n設定から、セキュアバックアップの設定や鍵の管理を行うこともできます。 + USBメモリーやバックアップ用のドライブに保存 鍵のバックアップの設定 自己署名キーを同期しています ユーザーキーを同期しています @@ -2087,12 +2087,12 @@ 鍵は既に最新です! 鍵をリセット 質問は空にできません - ここで送受信されるメッセージはエンドツーエンド暗号化されています。 + ここでのメッセージはエンドツーエンドで暗号化されています。 \n -\nメッセージは安全に保護されており、メッセージのロックを解除できる固有の鍵を持っているのはあなたと受信者だけです。 - この部屋のメッセージはエンドツーエンド暗号化されています。 +\nメッセージは安全に保護されており、メッセージのロックを解除するための固有の鍵は、あなたと受信者だけが持っています。 + このルームのメッセージはエンドツーエンドで暗号化されています。 \n -\nメッセージは安全に保護されており、メッセージのロックを解除できる固有の鍵を持っているのはあなたと受信者だけです。 +\nメッセージは安全に保護されており、メッセージのロックを解除するための固有の鍵は、あなたと受信者だけが持っています。 ステートキー リカバリーキーを以下に保存 送信者が意図的に鍵を送信しなかったため、このメッセージにアクセスすることができません @@ -2109,8 +2109,8 @@ ナビゲーションのメニューを開く 承諾しました %sが承諾しました - %sが認証済 - %sを認証する + %sを認証済 + %sを認証 絵文字を比較して認証 絵文字を比較して認証 対面でない場合は、代わりに絵文字を比較してください @@ -2124,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 @@ -2157,17 +2157,17 @@ 既存のセッションにアクセスできない場合 %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変更しました。 誰と使いますか? 作成するスペースの種類を選択してください 自分とチームメイトの非公開のスペース @@ -2177,9 +2177,9 @@ もう少しです!確認を待機しています… あと少しです!もう一方の端末は同じマークを表示していますか? %sを待機しています… - このユーザーがこのセッションを認証するまで、送受信されるメッセージには警告マークが付きます。手動で認証することも可能です。 + このユーザーがこのセッションを認証するまで、送受信されるメッセージには警告マークが付きます。手動で認証することもできます。 セッションの取得に失敗しました - チームの仲間を招待しましょう + 誰がチームの仲間ですか? %sを探せるようになります 私のスペース %1$s %2$s に参加してください スキップ @@ -2207,14 +2207,14 @@ 再認証が必要です 全てリセット 連絡先 - 認証をキャンセルしました。あらためて開始してください。 + 認証をキャンセルしました。改めて開始してください。 押し続けて録音し、離すと送信 PINコードを設定してください %d個のサーバーアクセス制御リストの変更 置き換えられたルームに参加 - このルームが発見できません。存在することを確認してください。 + このルームを発見できません。存在することを確認してください。 指紋や顔画像など、端末に固有の生体認証を有効にする。 絵文字で認証 テキストを使って手動で認証 @@ -2234,7 +2234,7 @@ 未読のメッセージ数のみを通知に表示。 2分間${app_name}を使用しないと、PINコードが要求されます。 🔐️ ${app_name}で話しましょう - 個人情報保護の観点から、${app_name}はハッシュ化されたメールアドレスと電話番号の送信のみをサポートしています。 + プライバシーの保護の観点から、${app_name}はハッシュ化されたメールアドレスと電話番号の送信のみをサポートしています。 アプリの名前を変更しました!アプリは最新版で、アカウントにはログイン済です。 ステートイベントを送信 ステートイベント @@ -2259,7 +2259,7 @@ \n \n続行してよろしいですか? このリンクを再確認してください - ログインを認証してください:%1$s + 新しいログインがあなたのアカウントにアクセスしています。ログインを認証してください:%1$s 機密ストレージのアクセスに失敗しました この設定を有効にすると、全てのアクティビティーにFLAG_SECUREを追加します。変更を有効にするにはアプリケーションの再起動が必要です。 このアカウントは無効化されています。 @@ -2267,7 +2267,7 @@ 印刷して安全な場所に保管 %2$sと%1$sが設定されました。 \n -\n安全な場所で保管してください!それらは、アクティブなセッションを全て失ってしまった際、暗号化されたメッセージや安全な情報のロックを解除するために必要となります。 +\n安全な場所で保管してください!アクティブなセッションを全て失ってしまった際、暗号化されたメッセージや安全な情報のロックを解除するために必要となります。 作成したアイデンティティーキーを公開しています アプリケーションのスクリーンショットを防ぐ 続行するには%1$sか%2$sを使用してください。 @@ -2307,7 +2307,7 @@ ビデオ通話が拒否されました 音声通話が拒否されました %1$sは通話を拒否しました - このデバイスを認証可能な他の端末が全くない場合にのみ、続行してください。 + この端末を認証できる他の端末が全くない場合にのみ、続行してください。 このセッションを信頼済として認証すると、暗号化されたメッセージにアクセスすることができます。このアカウントにサインインしなかった場合は、あなたのアカウントのセキュリティーが破られている可能性があります: アカウントのセキュリティーが破られている可能性があります 選択したスペースに追加 @@ -2328,7 +2328,7 @@ 保存して続行 設定画面からいつでもプロフィールを更新できます - これは後から変更できます。 + 表示名にプロフィール画像を追加しましょう プロフィール画像を追加 これは後から変更できます 表示名 @@ -2340,14 +2340,14 @@ 表示名を選択 あなたのアカウント %s が作成されました おめでとうございます! - 近日中にスレッドはベータ版となります。 + 近日中にスレッド機能はベータ版となります。 \n \nその準備として、この時点以前に作成されたスレッドは、通常の返信として表示するように変更します。 \n -\nスレッドはMatrixの仕様の一部になったため、これは一度限りの変更です。 - スレッドはベータ版になります 🎉 +\nスレッド機能はMatrixの仕様の一部になったため、これは一度限りの変更です。 + スレッド機能はベータ版になります 🎉 無効にする - スレッドについてのフィードバック + スレッド機能についてのフィードバック フィードバックを送信 ベータ版 ベータ版 @@ -2363,7 +2363,7 @@ ${app_name}をシンプルにするために、タブはオプションになりました。右上のメニューから管理できます。 新しいレイアウトにようこそ! アニメーション画像を自動再生 - エンドポイントのホームサーバーへの登録に失敗しました: + エンドポイントのホームサーバーへの登録に失敗しました: \n%1$s エンドポイントがホームサーバーに登録されました。 エンドポイントの登録 @@ -2371,14 +2371,14 @@ ${app_name}は通知の表示に権限が必要です。 \n権限を与えてください。 - %1$sと他%2$d名 + %1$sと他%2$d人 %1$sと%2$s ホームサーバーがサポートしていないため、スレッド機能は不安定かもしれません。スレッドのメッセージが安定して表示されないおそれがあります。%sスレッド機能を有効にしてよろしいですか? - スレッド(ベータ版) - スレッドを用いると、会話のテーマを保ったり、会話を追跡したりするのが容易になります。%sスレッドを有効にするとアプリケーションが再起動します。再起動には時間がかかる可能性があります。 - スレッド(ベータ版) - ${app_name}は通知を表示するために許可を必要としています。通知にはメッセージや招待などが表示されます。 + スレッド機能(ベータ版) + スレッド機能を使うと、会話のテーマを維持したり、会話を簡単に追跡したりすることができます。%sスレッド機能を有効にするとアプリケーションが再起動します。再起動には時間がかかる可能性があります。 + スレッド機能(ベータ版) + ${app_name}は、通知を表示するための権限を必要としています。通知にはメッセージや招待などが表示されます。 \n \n通知を表示するには、次のポップアップでアクセスを許可してください。 メールアドレスが認証されていません。メールボックスを確認してください @@ -2399,7 +2399,7 @@ このステップをスキップ 問題ありません! 進みましょう - ユーザー名 / メールアドレス / 電話番号 + ユーザー名 / 電子メール / 電話番号 あなたは人間ですか? %sに送信された手順に従ってください。 パスワードを再設定 @@ -2410,7 +2410,7 @@ メールアドレスを認証 コードを再送信 コードが%sに送信されました - 電話番号を確認してください + 電話番号を確認 全ての端末からサインアウト パスワードを再設定 パスワードは8文字以上に設定してください。 @@ -2427,7 +2427,7 @@ リッチテキストエディターを有効にする 最初のメッセージを送信する際にダイレクトメッセージを作成 遅延DMを有効にする - スペースがありません。 + まだスペースがありません。 新しいレイアウトを有効にする アクティビティー順 アルファベット順 @@ -2445,12 +2445,12 @@ \n \nアプリケーションが再起動します。再起動には時間がかかる可能性があります。 初期同期のリクエスト - %sの子スペースを折りたたむ - %sの子スペースを展開 + %sのサブスペースを折りたたむ + %sのサブスペースを展開 ルームを探す スペースを変更 ルームを作成 - チャットを開始 + 会話を開始 全ての会話 ${app_name}にようこそ、 \n%s。 @@ -2481,7 +2481,7 @@ 音声配信を終了しました。 %1$sが音声配信を終了しました。 - %1$dを選択しました + %1$d個選択済 有効にすると、このアプリケーションを使用している際にも、他のユーザーにオフラインとして表示されます。 最近のチャットをシステムの共有メニューに表示 @@ -2490,7 +2490,7 @@ 自動的に設定 フォントの大きさを選択 ⚠ 未認証の端末がこのルームにあります。あなたが送信するメッセージを復号化することはできません。 - このルームの未認証のセッションに暗号化されたメッセージを送信しない。 + このルームの未認証のセッションに対して暗号化されたメッセージを送信しない。 あなたのホームサーバーはスレッドの一覧表示をまだサポートしていません。 ここに新しいリクエストと招待が表示されます。 リッチテキストエディターを試してみる(プレーンテキストモードは近日公開) @@ -2524,7 +2524,7 @@ ルームのタイムラインで音声配信を録音して送信することを可能にします。 音声配信を有効にする 未読のメッセージがある場合は、ここに表示されます。 - 報告することはありません。 + 未読はありません。 クライアントの情報の保存を有効にする セッション名は連絡先にも表示されます。 セッション名を設定すると、端末をより簡単に認識できるようになります。 @@ -2704,7 +2704,7 @@ %sの利用規約と運営方針を確認してください サーバーの運営方針 問い合わせる - 自分でサーバーを運営したいですか? + 自分でサーバーを運営しますか? サーバーのURL ホームサーバーのアドレスを入力してください あなたのホームサーバーのアドレスを入力してください。ここにあなたの全てのデータがホストされます @@ -2713,7 +2713,7 @@ 8文字以上にしてください アカウントを作成 ホームに移動 - プロフィールを変更 + プロフィールを設定 ${app_name}は職場利用にも最適です。世界で最も安全な組織によって信頼されています。 音声配信 スペースの一覧を開く @@ -2746,11 +2746,11 @@ このホームサーバーは数字だけからなるユーザー名を承諾しません。 最初のメッセージを送信すると、%sを会話に招待 Nightly build - あなたの他の端末でコードをスキャンするか、もしくは反対に、このデバイスでスキャンしてください + あなたの他の端末でコードをスキャンするか、もしくは反対に、この端末でスキャンしてください Element Matrix Services(EMS)は、高速、安全でリアルタイムのコミュニケーション向きの、堅牢で安定したホスティングサービスです。<a href=\"${ftue_ems_url}\">element.io/ems</a>で方法を調べましょう。 アカウントにサインインするサーバー アカウントを作成するサーバー - スレッドは、改良した通知など新機能の追加作業中です。フィードバックをお聞かせください! + スレッド機能については、改良した通知など新機能の追加などを行っています。フィードバックをお聞かせください! 🔒 セキュリティーの設定で、全てのルームに関して認証済のセッションにのみ暗号化を行うよう設定しました。 プレゼンス(ステータス表示) 取り込み中 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 caf9913299..597dd90b2f 100644 --- a/libraries/ui-strings/src/main/res/values-sv/strings.xml +++ b/libraries/ui-strings/src/main/res/values-sv/strings.xml @@ -2919,4 +2919,5 @@ 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/strings.xml b/libraries/ui-strings/src/main/res/values/strings.xml index e690f06bbb..de3fa20916 100644 --- a/libraries/ui-strings/src/main/res/values/strings.xml +++ b/libraries/ui-strings/src/main/res/values/strings.xml @@ -3120,6 +3120,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? From f736b48a9241cfeecc9d6d4b0ed607af19970765 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 8 Feb 2023 09:56:55 +0100 Subject: [PATCH 63/96] Add test for `MessageComposerPresenter` --- .../textcomposer/MessageComposerPresenter.kt | 5 +- .../MessageComposerPresenterTest.kt | 190 ++++++++++++++++++ .../roomlist/RoomListPresenterTests.kt | 4 +- .../libraries/core/data/StableCharSequence.kt | 2 + .../matrixtest/room/FakeMatrixRoom.kt | 4 +- .../matrixtest/room/RoomSummaryFixture.kt | 6 +- .../matrixtest/timeline/FakeMatrixTimeline.kt | 1 + 7 files changed, 205 insertions(+), 7 deletions(-) create mode 100644 features/messages/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt 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/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..b822caff9e --- /dev/null +++ b/features/messages/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt @@ -0,0 +1,190 @@ +/* + * 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.core.A_ROOM_ID +import io.element.android.libraries.matrixtest.room.A_MESSAGE +import io.element.android.libraries.matrixtest.room.FakeMatrixRoom +import io.element.android.libraries.matrixtest.timeline.AN_EVENT_ID +import io.element.android.libraries.matrixtest.timeline.A_SENDER_NAME +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(A_ROOM_ID) + ) + 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(A_ROOM_ID) + ) + 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(A_ROOM_ID) + ) + 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(A_ROOM_ID) + ) + 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(A_ROOM_ID) + ) + 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(A_ROOM_ID) + ) + 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(A_ROOM_ID) + ) + 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() + } + } + +} + +fun anEditMode() = MessageComposerMode.Edit(AN_EVENT_ID, A_MESSAGE) +fun aReplyMode() = MessageComposerMode.Reply(A_SENDER_NAME, AN_EVENT_ID, A_MESSAGE) +fun aQuoteMode() = MessageComposerMode.Quote(AN_EVENT_ID, A_MESSAGE) 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 baa547b4bb..46c1597475 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 @@ -30,7 +30,7 @@ import io.element.android.libraries.matrix.core.SessionId import io.element.android.libraries.matrixtest.FakeMatrixClient import io.element.android.libraries.matrixtest.core.A_ROOM_ID import io.element.android.libraries.matrixtest.core.A_ROOM_ID_VALUE -import io.element.android.libraries.matrixtest.room.A_LAST_MESSAGE +import io.element.android.libraries.matrixtest.room.A_MESSAGE import io.element.android.libraries.matrixtest.room.A_ROOM_NAME import io.element.android.libraries.matrixtest.room.FakeRoomSummaryDataSource import io.element.android.libraries.matrixtest.room.aRoomSummaryFilled @@ -187,7 +187,7 @@ private val aRoomListRoomSummary = RoomListRoomSummary( name = A_ROOM_NAME, hasUnread = true, timestamp = A_FORMATTED_DATE, - lastMessage = A_LAST_MESSAGE, + lastMessage = A_MESSAGE, avatarData = AvatarData(name = A_ROOM_NAME), isPlaceholder = false, ) 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/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 7e46219126..7aac3b95a0 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 @@ -21,6 +21,7 @@ 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.timeline.FakeMatrixTimeline +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow @@ -51,7 +52,8 @@ class FakeMatrixRoom( } override suspend fun sendMessage(message: String): Result { - TODO("Not yet implemented") + delay(100) + return Result.success(Unit) } override suspend fun editMessage(originalEventId: EventId, message: String): Result { 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 index 0bed866300..79d65536bd 100644 --- 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 @@ -22,14 +22,14 @@ import io.element.android.libraries.matrix.room.RoomSummaryDetails import io.element.android.libraries.matrixtest.core.A_ROOM_ID const val A_ROOM_NAME = "aRoomName" -const val A_LAST_MESSAGE = "Last message" +const val A_MESSAGE = "Hello world!" fun aRoomSummaryFilled( roomId: RoomId = A_ROOM_ID, name: String = A_ROOM_NAME, isDirect: Boolean = false, avatarURLString: String? = null, - lastMessage: CharSequence? = A_LAST_MESSAGE, + lastMessage: CharSequence? = A_MESSAGE, lastMessageTimestamp: Long? = null, unreadNotificationCount: Int = 2, ) = RoomSummary.Filled( @@ -49,7 +49,7 @@ fun aRoomSummaryDetail( name: String = A_ROOM_NAME, isDirect: Boolean = false, avatarURLString: String? = null, - lastMessage: CharSequence? = A_LAST_MESSAGE, + lastMessage: CharSequence? = A_MESSAGE, lastMessageTimestamp: Long? = null, unreadNotificationCount: Int = 2, ) = RoomSummaryDetails( 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 665a2eccfb..17eb98cb50 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 @@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import org.matrix.rustcomponents.sdk.TimelineListener +const val A_SENDER_NAME = "Alice" const val AN_EVENT_ID_VALUE = "!anEventId" val AN_EVENT_ID = EventId(AN_EVENT_ID_VALUE) From c4195f64cb9f3bf397665d0f001c0a891e73547e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 8 Feb 2023 14:30:19 +0100 Subject: [PATCH 64/96] Convert RageshakeDataStore to an interface for testing purpose. --- .../PreferencesRageshakeDataStore.kt | 72 +++++++++++++++++++ .../rageshake/rageshake/RageshakeDataStore.kt | 52 ++------------ 2 files changed, 79 insertions(+), 45 deletions(-) create mode 100644 features/rageshake/src/main/kotlin/io/element/android/features/rageshake/rageshake/PreferencesRageshakeDataStore.kt 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/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() } From ef6f1e9f2b8d7aa501b6c9b2bd9d0d11e41b5de2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 8 Feb 2023 14:36:48 +0100 Subject: [PATCH 65/96] Convert Rageshake to an interface for testing purpose. --- app/build.gradle.kts | 1 + .../detection/RageshakeDetectionPresenter.kt | 4 +- .../rageshake/rageshake/DefaultRageShake.kt | 77 +++++++++++++++++++ .../features/rageshake/rageshake/RageShake.kt | 50 ++---------- 4 files changed, 87 insertions(+), 45 deletions(-) create mode 100644 features/rageshake/src/main/kotlin/io/element/android/features/rageshake/rageshake/DefaultRageShake.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4aa95c2f7c..305173d99f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -176,6 +176,7 @@ dependencies { implementation(libs.androidx.startup) implementation(libs.coil) implementation(libs.datetime) + implementation(libs.squareup.seismic) implementation(libs.dagger) kapt(libs.dagger.compiler) 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/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/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)?) } From 4b488250655a1e5f573f62bf5c0161fa3374e4c4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 8 Feb 2023 15:06:39 +0100 Subject: [PATCH 66/96] Add test for `RageshakePreferencesPresenter` --- features/rageshake/build.gradle.kts | 7 ++ .../preferences/FakeRageShake.kt} | 34 ++++--- .../preferences/FakeRageshakeDataStore.kt | 43 ++++++++ .../RageshakePreferencesPresenterTest.kt | 98 +++++++++++++++++++ 4 files changed, 169 insertions(+), 13 deletions(-) rename features/rageshake/src/test/kotlin/io/element/android/features/{login/ExampleUnitTest.kt => rageshake/preferences/FakeRageShake.kt} (51%) create mode 100644 features/rageshake/src/test/kotlin/io/element/android/features/rageshake/preferences/FakeRageshakeDataStore.kt create mode 100644 features/rageshake/src/test/kotlin/io/element/android/features/rageshake/preferences/RageshakePreferencesPresenterTest.kt diff --git a/features/rageshake/build.gradle.kts b/features/rageshake/build.gradle.kts index ecb333289b..d1aee8d989 100644 --- a/features/rageshake/build.gradle.kts +++ b/features/rageshake/build.gradle.kts @@ -45,6 +45,13 @@ dependencies { 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) + androidTestImplementation(libs.test.junitext) } diff --git a/features/rageshake/src/test/kotlin/io/element/android/features/login/ExampleUnitTest.kt b/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/preferences/FakeRageShake.kt similarity index 51% rename from features/rageshake/src/test/kotlin/io/element/android/features/login/ExampleUnitTest.kt rename to features/rageshake/src/test/kotlin/io/element/android/features/rageshake/preferences/FakeRageShake.kt index ee6363e624..6d0669cd7c 100644 --- a/features/rageshake/src/test/kotlin/io/element/android/features/login/ExampleUnitTest.kt +++ b/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/preferences/FakeRageShake.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,27 @@ * limitations under the License. */ -package io.element.android.features.login +package io.element.android.features.rageshake.preferences -import org.junit.Assert.assertEquals -import org.junit.Test +import io.element.android.features.rageshake.rageshake.RageShake -/** - * 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) +const val A_SENSITIVITY = 1f + +class FakeRageShake( + private var isAvailableValue: Boolean = true +) : RageShake { + + override fun isAvailable() = isAvailableValue + + override fun start(sensitivity: Float) { + } + + override fun stop() { + } + + override fun setSensitivity(sensitivity: Float) { + } + + override fun setInterceptor(interceptor: (() -> Unit)?) { } } 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) + } + } +} + From e8c005de9a4bf0dd4ecb88e5c5168eb0ceffda42 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 8 Feb 2023 15:11:06 +0100 Subject: [PATCH 67/96] Convert CrashDataStore to an interface for testing purpose. --- .../rageshake/crash/CrashDataStore.kt | 59 ++------------ .../crash/PreferencesCrashDataStore.kt | 77 +++++++++++++++++++ .../crash/VectorUncaughtExceptionHandler.kt | 2 +- 3 files changed, 85 insertions(+), 53 deletions(-) create mode 100644 features/rageshake/src/main/kotlin/io/element/android/features/rageshake/crash/PreferencesCrashDataStore.kt 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 /** From 101ed1f272e3dd466445666f44b795f1dd47d539 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 8 Feb 2023 15:23:47 +0100 Subject: [PATCH 68/96] Add test for `CrashDetectionPresenter` --- .../crash/ui/CrashDetectionPresenterTest.kt | 89 +++++++++++++++++++ .../rageshake/crash/ui/FakeCrashDataStore.kt | 48 ++++++++++ 2 files changed, 137 insertions(+) create mode 100644 features/rageshake/src/test/kotlin/io/element/android/features/rageshake/crash/ui/CrashDetectionPresenterTest.kt create mode 100644 features/rageshake/src/test/kotlin/io/element/android/features/rageshake/crash/ui/FakeCrashDataStore.kt 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 = "" + } +} From 48bd5fd48ef4dfa49b9aa79f519df9fec4a64b66 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 8 Feb 2023 16:01:49 +0100 Subject: [PATCH 69/96] Add test for `ActionListPresenter` --- .../actionlist/ActionListPresenterTest.kt | 176 ++++++++++++++++++ .../matrixtest/timeline/FakeMatrixTimeline.kt | 1 + 2 files changed, 177 insertions(+) create mode 100644 features/messages/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt 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..742463d5ed --- /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.room.A_MESSAGE +import io.element.android.libraries.matrixtest.timeline.AN_EVENT_ID +import io.element.android.libraries.matrixtest.timeline.A_SENDER_ID +import io.element.android.libraries.matrixtest.timeline.A_SENDER_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_SENDER_ID, + senderDisplayName = A_SENDER_NAME, + senderAvatar = AvatarData(), + content = content, + sentTime = "", + isMine = isMine, + reactionsState = TimelineItemReactions(persistentListOf()) +) 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 17eb98cb50..4a24e168dc 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 @@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.emptyFlow import org.matrix.rustcomponents.sdk.TimelineListener const val A_SENDER_NAME = "Alice" +const val A_SENDER_ID = "@alice:server.org" const val AN_EVENT_ID_VALUE = "!anEventId" val AN_EVENT_ID = EventId(AN_EVENT_ID_VALUE) From f03e7666572b193b7b65bc7103a5ec539cd1d124 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 8 Feb 2023 16:05:21 +0100 Subject: [PATCH 70/96] Improve coverage of `TemplatePresenter` --- .../features/template/TemplatePresenterTests.kt | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) 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 34cc73ba53..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 @@ -21,7 +21,7 @@ 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 @@ -29,15 +29,24 @@ 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) + } } } From e2b4056493dd25a6a64994e8b6b2c035fe6dcb15 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 8 Feb 2023 16:15:12 +0100 Subject: [PATCH 71/96] Convert BugReporter to an interface for testing purpose. Cannot use `@ContributesBinding(AppScope::class)`, so provide the implementation in AppModule. --- .../io/element/android/x/di/AppModule.kt | 19 + .../rageshake/bugreport/BugReportPresenter.kt | 9 +- .../rageshake/reporter/BugReporter.kt | 507 +---------------- .../rageshake/reporter/BugReporterListener.kt | 46 ++ .../rageshake/reporter/DefaultBugReporter.kt | 520 ++++++++++++++++++ 5 files changed, 596 insertions(+), 505 deletions(-) mode change 100755 => 100644 features/rageshake/src/main/kotlin/io/element/android/features/rageshake/reporter/BugReporter.kt create mode 100644 features/rageshake/src/main/kotlin/io/element/android/features/rageshake/reporter/BugReporterListener.kt create mode 100755 features/rageshake/src/main/kotlin/io/element/android/features/rageshake/reporter/DefaultBugReporter.kt 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..c41ae86dff 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,10 @@ import android.content.Context import com.squareup.anvil.annotations.ContributesTo import dagger.Module import dagger.Provides +import io.element.android.features.rageshake.crash.CrashDataStore +import io.element.android.features.rageshake.reporter.BugReporter +import io.element.android.features.rageshake.reporter.DefaultBugReporter +import io.element.android.features.rageshake.screenshot.ScreenshotHolder import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext @@ -58,4 +62,19 @@ object AppModule { diffUpdateDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() ) } + + @Provides + fun provideBugReporter( + @ApplicationContext context: Context, + screenshotHolder: ScreenshotHolder, + crashDataStore: CrashDataStore, + coroutineDispatchers: CoroutineDispatchers, + ): BugReporter { + return DefaultBugReporter( + context = context, + screenshotHolder = screenshotHolder, + crashDataStore = crashDataStore, + coroutineDispatchers = coroutineDispatchers, + ) + } } 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..2b45a02941 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 @@ -27,6 +27,7 @@ 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 +46,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 @@ -126,7 +127,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, 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..bb90206a92 --- /dev/null +++ b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/reporter/DefaultBugReporter.kt @@ -0,0 +1,520 @@ +/* + * 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 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) { + 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") + } + } +} From e954dccad948ddd2ebec958e7d35a0549924947c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 8 Feb 2023 17:25:05 +0100 Subject: [PATCH 72/96] Convert ScreenshotHolder to an interface for testing purpose. --- .../screenshot/DefaultScreenshotHolder.kt | 46 +++++++++++++++++++ .../rageshake/screenshot/ScreenshotHolder.kt | 28 ++--------- 2 files changed, 51 insertions(+), 23 deletions(-) create mode 100644 features/rageshake/src/main/kotlin/io/element/android/features/rageshake/screenshot/DefaultScreenshotHolder.kt 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..53b6291bdb --- /dev/null +++ b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/screenshot/DefaultScreenshotHolder.kt @@ -0,0 +1,46 @@ +/* + * 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 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 getFile() = file.takeIf { it.exists() && it.length() > 0 } + + 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..c570f0665d 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,11 @@ 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 getFile(): File? + fun reset() } From 31eb86ebe48438fcd36113d7e4abdbee5c023ad0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 8 Feb 2023 18:42:17 +0100 Subject: [PATCH 73/96] Add test for `BugReportPresenter` --- .../rageshake/bugreport/BugReportPresenter.kt | 5 +- .../rageshake/logs/VectorFileLogger.kt | 4 +- .../rageshake/reporter/DefaultBugReporter.kt | 9 +- .../screenshot/DefaultScreenshotHolder.kt | 8 +- .../rageshake/screenshot/ScreenshotHolder.kt | 3 +- .../bugreport/BugReportPresenterTest.kt | 247 ++++++++++++++++++ .../rageshake/bugreport/FakeBugReporter.kt | 70 +++++ .../bugreport/FakeScreenshotHolder.kt | 30 +++ 8 files changed, 366 insertions(+), 10 deletions(-) create mode 100644 features/rageshake/src/test/kotlin/io/element/android/features/rageshake/bugreport/BugReportPresenterTest.kt create mode 100644 features/rageshake/src/test/kotlin/io/element/android/features/rageshake/bugreport/FakeBugReporter.kt create mode 100644 features/rageshake/src/test/kotlin/io/element/android/features/rageshake/bugreport/FakeScreenshotHolder.kt 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 2b45a02941..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,7 +23,6 @@ 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 @@ -73,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 @@ -150,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/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/reporter/DefaultBugReporter.kt b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/reporter/DefaultBugReporter.kt index bb90206a92..cfc0c68561 100755 --- 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 @@ -18,6 +18,8 @@ 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 @@ -153,7 +155,7 @@ class DefaultBugReporter @Inject constructor( val gzippedFiles = ArrayList() val vectorFileLogger = VectorFileLogger.getFromTimber() - if (withDevicesLogs) { + if (withDevicesLogs && vectorFileLogger != null) { val files = vectorFileLogger.getLogFiles() files.mapNotNullTo(gzippedFiles) { f -> if (!mIsCancelled) { @@ -254,7 +256,10 @@ class DefaultBugReporter @Inject constructor( mBugReportFiles.addAll(gzippedFiles) if (withScreenshot) { - screenshotHolder.getFile()?.let { screenshotFile -> + screenshotHolder.getFileUri() + ?.toUri() + ?.toFile() + ?.let { screenshotFile -> try { builder.addFormDataPart( "file", 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 index 53b6291bdb..31eeac39a6 100644 --- 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 @@ -18,6 +18,7 @@ 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 @@ -38,7 +39,12 @@ class DefaultScreenshotHolder @Inject constructor( file.writeBitmap(data, Bitmap.CompressFormat.PNG, 85) } - override fun getFile() = file.takeIf { it.exists() && it.length() > 0 } + 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 c570f0665d..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 @@ -17,10 +17,9 @@ package io.element.android.features.rageshake.screenshot import android.graphics.Bitmap -import java.io.File interface ScreenshotHolder { fun writeBitmap(data: Bitmap) - fun getFile(): File? + 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..8737415d6c --- /dev/null +++ b/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/bugreport/BugReportPresenterTest.kt @@ -0,0 +1,247 @@ +/* + * 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 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(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_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..a1a2c613a7 --- /dev/null +++ b/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/bugreport/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.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 kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +const val A_REASON = "There has been a failure" + +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_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 +} From bed290a376782a64ed1ced69587b3cef770f0fed Mon Sep 17 00:00:00 2001 From: bmarty Date: Thu, 9 Feb 2023 06:06:20 +0000 Subject: [PATCH 74/96] Import strings from Element Android --- libraries/ui-strings/src/main/res/values/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libraries/ui-strings/src/main/res/values/strings.xml b/libraries/ui-strings/src/main/res/values/strings.xml index de3fa20916..17fbadd776 100644 --- a/libraries/ui-strings/src/main/res/values/strings.xml +++ b/libraries/ui-strings/src/main/res/values/strings.xml @@ -3503,7 +3503,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 From a3348ca71cda4970d6bba0f457140858deaf3a5f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 9 Feb 2023 09:52:03 +0100 Subject: [PATCH 75/96] Improve coverage for `TimelinePresenter` --- .../timeline/TimelinePresenterTest.kt | 26 ++++++++++++++++++- .../matrixtest/timeline/FakeMatrixTimeline.kt | 5 +--- 2 files changed, 26 insertions(+), 5 deletions(-) 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 index a1106f738e..1234c8619c 100644 --- 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 @@ -22,6 +22,8 @@ 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.FakeMatrixClient import io.element.android.libraries.matrixtest.core.A_ROOM_ID import io.element.android.libraries.matrixtest.room.FakeMatrixRoom @@ -43,7 +45,7 @@ class TimelinePresenterTest { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.timelineItems.size).isEqualTo(0) + assertThat(initialState.timelineItems).isEmpty() } } @@ -90,4 +92,26 @@ class TimelinePresenterTest { assertThat(withoutHighlightedState.highlightedEventId).isNull() } } + + @Test + fun `present - test callback`() = runTest { + val matrixTimeline = FakeMatrixTimeline() + val matrixRoom = FakeMatrixRoom(A_ROOM_ID, 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/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 4a24e168dc..417489dc83 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 @@ -30,10 +30,7 @@ const val AN_EVENT_ID_VALUE = "!anEventId" val AN_EVENT_ID = EventId(AN_EVENT_ID_VALUE) class FakeMatrixTimeline : MatrixTimeline { - - override var callback: MatrixTimeline.Callback? - get() = null - set(value) {} + override var callback: MatrixTimeline.Callback? = null private var hasMoreToLoadValue: Boolean = true From 87b616296535e78df8e09623f9a4621b480a4f7d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 9 Feb 2023 09:52:34 +0100 Subject: [PATCH 76/96] Cleanup --- .../element/android/features/roomlist/RoomListPresenterTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 46c1597475..ad2e02fb51 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 @@ -131,7 +131,7 @@ class RoomListPresenterTests { skipItems(1) // Filter update val withFilteredRoomState = awaitItem() assertThat(withFilteredRoomState.filter).isEqualTo("tada") - assertThat(withFilteredRoomState.roomList.size).isEqualTo(0) + assertThat(withFilteredRoomState.roomList).isEmpty() } } From 3b4a4694ab1aeb071cebd8099adcab85b45e78e2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 9 Feb 2023 10:34:45 +0100 Subject: [PATCH 77/96] Add test for `RageshakeDetectionPresenter` --- .../RageshakeDetectionPresenterTest.kt | 152 ++++++++++++++++++ .../rageshake/preferences/FakeRageShake.kt | 5 + 2 files changed, 157 insertions(+) create mode 100644 features/rageshake/src/test/kotlin/io/element/android/features/rageshake/detection/RageshakeDetectionPresenterTest.kt 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..6b0d1e38f7 --- /dev/null +++ b/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/detection/RageshakeDetectionPresenterTest.kt @@ -0,0 +1,152 @@ +/* + * 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 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 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 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(Exception("Error"))) + ) + 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.Error(Exception("Error"))) + ) + assertThat(awaitItem().showDialog).isTrue() + initialState.eventSink.invoke(RageshakeDetectionEvents.Disable) + skipItems(1) + assertThat(awaitItem().showDialog).isFalse() + assertThat(rageshakeDataStore.isEnabled().first()).isFalse() + } + } +} 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 index 6d0669cd7c..fbabdaac5d 100644 --- 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 @@ -24,6 +24,8 @@ class FakeRageShake( private var isAvailableValue: Boolean = true ) : RageShake { + private var interceptor: (() -> Unit)? = null + override fun isAvailable() = isAvailableValue override fun start(sensitivity: Float) { @@ -36,5 +38,8 @@ class FakeRageShake( } override fun setInterceptor(interceptor: (() -> Unit)?) { + this.interceptor = interceptor } + + fun triggerPhoneRageshake() = interceptor?.invoke() } From 987d4d6c086ad352ea2eb1c5b1f0f2612d6d4e82 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 9 Feb 2023 10:43:37 +0100 Subject: [PATCH 78/96] Less verbose provider. --- .../kotlin/io/element/android/x/di/AppModule.kt | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) 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 c41ae86dff..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,10 +20,8 @@ import android.content.Context import com.squareup.anvil.annotations.ContributesTo import dagger.Module import dagger.Provides -import io.element.android.features.rageshake.crash.CrashDataStore import io.element.android.features.rageshake.reporter.BugReporter import io.element.android.features.rageshake.reporter.DefaultBugReporter -import io.element.android.features.rageshake.screenshot.ScreenshotHolder import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext @@ -64,17 +62,5 @@ object AppModule { } @Provides - fun provideBugReporter( - @ApplicationContext context: Context, - screenshotHolder: ScreenshotHolder, - crashDataStore: CrashDataStore, - coroutineDispatchers: CoroutineDispatchers, - ): BugReporter { - return DefaultBugReporter( - context = context, - screenshotHolder = screenshotHolder, - crashDataStore = crashDataStore, - coroutineDispatchers = coroutineDispatchers, - ) - } + fun providesBugReporter(bugReporter: DefaultBugReporter): BugReporter = bugReporter } From 165697a80e2ab190c00513249e1985fd7b107a68 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 9 Feb 2023 12:04:31 +0100 Subject: [PATCH 79/96] Fix dependency issue. --- app/build.gradle.kts | 2 -- features/rageshake/build.gradle.kts | 2 +- libraries/dateformatter/build.gradle.kts | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 305173d99f..f5a0799676 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -175,8 +175,6 @@ dependencies { implementation(libs.androidx.activity.compose) implementation(libs.androidx.startup) implementation(libs.coil) - implementation(libs.datetime) - implementation(libs.squareup.seismic) implementation(libs.dagger) kapt(libs.dagger.compiler) diff --git a/features/rageshake/build.gradle.kts b/features/rageshake/build.gradle.kts index d1aee8d989..17160b37ab 100644 --- a/features/rageshake/build.gradle.kts +++ b/features/rageshake/build.gradle.kts @@ -40,7 +40,7 @@ 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) diff --git a/libraries/dateformatter/build.gradle.kts b/libraries/dateformatter/build.gradle.kts index 817435f8ac..60ecd95052 100644 --- a/libraries/dateformatter/build.gradle.kts +++ b/libraries/dateformatter/build.gradle.kts @@ -34,7 +34,7 @@ android { implementation(libs.dagger) implementation(projects.libraries.di) implementation(projects.anvilannotations) - implementation(libs.datetime) + api(libs.datetime) ksp(libs.showkase.processor) testImplementation(libs.test.junit) From 11260400329b13affddd606c68602930e23e5a79 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 9 Feb 2023 12:19:04 +0100 Subject: [PATCH 80/96] Add test for `PreferencesRootPresenter` --- features/preferences/build.gradle.kts | 7 +++ .../features/preferences/ExampleUnitTest.kt | 32 ----------- .../preferences/root/FakeRageShake.kt | 44 +++++++++++++++ .../root/FakeRageshakeDataStore.kt | 46 ++++++++++++++++ .../root/PreferencesRootPresenterTest.kt | 53 +++++++++++++++++++ 5 files changed, 150 insertions(+), 32 deletions(-) delete mode 100644 features/preferences/src/test/kotlin/io/element/android/features/preferences/ExampleUnitTest.kt create mode 100644 features/preferences/src/test/kotlin/io/element/android/features/preferences/root/FakeRageShake.kt create mode 100644 features/preferences/src/test/kotlin/io/element/android/features/preferences/root/FakeRageshakeDataStore.kt create mode 100644 features/preferences/src/test/kotlin/io/element/android/features/preferences/root/PreferencesRootPresenterTest.kt 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/test/kotlin/io/element/android/features/preferences/ExampleUnitTest.kt b/features/preferences/src/test/kotlin/io/element/android/features/preferences/ExampleUnitTest.kt deleted file mode 100644 index 3b615c83e9..0000000000 --- a/features/preferences/src/test/kotlin/io/element/android/features/preferences/ExampleUnitTest.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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.preferences - -import org.junit.Assert.assertEquals -import org.junit.Test - -/** - * 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) - } -} 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) + } + } +} From afbae0a15d29d5ec4a7200198c9c71d0a9acc164 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 9 Feb 2023 12:30:53 +0100 Subject: [PATCH 81/96] Add test for `RootPresenter` --- app/build.gradle.kts | 7 ++ .../element/android/x/root/FakeBugReporter.kt | 71 +++++++++++++++ .../android/x/root/FakeCrashDataStore.kt | 50 +++++++++++ .../element/android/x/root/FakeRageShake.kt | 44 +++++++++ .../android/x/root/FakeRageshakeDataStore.kt | 46 ++++++++++ .../android/x/root/FakeScreenshotHolder.kt | 31 +++++++ .../android/x/root/RootPresenterTest.kt | 89 +++++++++++++++++++ 7 files changed, 338 insertions(+) create mode 100644 app/src/test/kotlin/io/element/android/x/root/FakeBugReporter.kt create mode 100644 app/src/test/kotlin/io/element/android/x/root/FakeCrashDataStore.kt create mode 100644 app/src/test/kotlin/io/element/android/x/root/FakeRageShake.kt create mode 100644 app/src/test/kotlin/io/element/android/x/root/FakeRageshakeDataStore.kt create mode 100644 app/src/test/kotlin/io/element/android/x/root/FakeScreenshotHolder.kt create mode 100644 app/src/test/kotlin/io/element/android/x/root/RootPresenterTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f5a0799676..0ee91861a0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -178,4 +178,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/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..f6cc6c8960 --- /dev/null +++ b/app/src/test/kotlin/io/element/android/x/root/FakeBugReporter.kt @@ -0,0 +1,71 @@ +/* + * 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 kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +const val A_REASON = "There has been a failure" + +// 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_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, + ) + } +} From 059d8ec6ce47fa037f042ab2377a455f0b2bccbd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 9 Feb 2023 14:27:48 +0100 Subject: [PATCH 82/96] More tests for `RageshakeDetectionPresenterTest` --- features/rageshake/build.gradle.kts | 1 + .../RageshakeDetectionPresenterTest.kt | 43 ++++++++++++++++++- gradle/libs.versions.toml | 2 +- 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/features/rageshake/build.gradle.kts b/features/rageshake/build.gradle.kts index 17160b37ab..45cc9cdd64 100644 --- a/features/rageshake/build.gradle.kts +++ b/features/rageshake/build.gradle.kts @@ -52,6 +52,7 @@ dependencies { 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/test/kotlin/io/element/android/features/rageshake/detection/RageshakeDetectionPresenterTest.kt b/features/rageshake/src/test/kotlin/io/element/android/features/rageshake/detection/RageshakeDetectionPresenterTest.kt index 6b0d1e38f7..f5fb8cbedc 100644 --- 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 @@ -18,6 +18,7 @@ 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 @@ -27,6 +28,7 @@ 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.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest @@ -83,7 +85,41 @@ class RageshakeDetectionPresenterTest { } @Test - fun `present - screenshot then dismiss`() = runTest { + 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) @@ -140,7 +176,7 @@ class RageshakeDetectionPresenterTest { rageshake.triggerPhoneRageshake() assertThat(awaitItem().takeScreenshot).isTrue() initialState.eventSink.invoke( - RageshakeDetectionEvents.ProcessScreenshot(ImageResult.Error(Exception("Error"))) + RageshakeDetectionEvents.ProcessScreenshot(ImageResult.Success(aBitmap())) ) assertThat(awaitItem().showDialog).isTrue() initialState.eventSink.invoke(RageshakeDetectionEvents.Disable) @@ -150,3 +186,6 @@ class RageshakeDetectionPresenterTest { } } } + +private fun aBitmap(): Bitmap = mockk() + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 036a9060be..8ed886b47f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" From e4722f43d3b63cb46a489f16a44bc9cf74a3d431 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 9 Feb 2023 15:19:32 +0100 Subject: [PATCH 83/96] Test reply and edit message --- .../MessageComposerPresenterTest.kt | 63 +++++++++++++++++++ .../matrixtest/room/FakeMatrixRoom.kt | 14 ++++- .../matrixtest/room/RoomSummaryFixture.kt | 2 + 3 files changed, 77 insertions(+), 2 deletions(-) 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 index b822caff9e..9a0123b044 100644 --- 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 @@ -25,7 +25,9 @@ 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.core.A_ROOM_ID +import io.element.android.libraries.matrixtest.room.ANOTHER_MESSAGE import io.element.android.libraries.matrixtest.room.A_MESSAGE +import io.element.android.libraries.matrixtest.room.A_REPLY import io.element.android.libraries.matrixtest.room.FakeMatrixRoom import io.element.android.libraries.matrixtest.timeline.AN_EVENT_ID import io.element.android.libraries.matrixtest.timeline.A_SENDER_NAME @@ -183,6 +185,67 @@ class MessageComposerPresenterTest { } } + @Test + fun `present - edit message`() = runTest { + val fakeMatrixRoom = FakeMatrixRoom(A_ROOM_ID) + 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(A_ROOM_ID) + 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)) + var 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) 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 7aac3b95a0..eae9995d13 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 @@ -56,12 +56,22 @@ class FakeMatrixRoom( 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) } override suspend fun redactEvent(eventId: EventId, reason: String?): Result { 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 index 79d65536bd..6e7fb1de32 100644 --- 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 @@ -23,6 +23,8 @@ import io.element.android.libraries.matrixtest.core.A_ROOM_ID const val A_ROOM_NAME = "aRoomName" const val A_MESSAGE = "Hello world!" +const val A_REPLY = "OK, I'll be there!" +const val ANOTHER_MESSAGE = "Hello universe!" fun aRoomSummaryFilled( roomId: RoomId = A_ROOM_ID, From f6afd4de2662973a8af2e396c93d9632d547a164 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 9 Feb 2023 16:12:22 +0100 Subject: [PATCH 84/96] Add test for `MessagesPresenter` --- .../features/messages/MessagePresenterTest.kt | 41 ---- .../messages/MessagesPresenterTest.kt | 178 ++++++++++++++++++ .../matrixtest/room/FakeMatrixRoom.kt | 7 +- 3 files changed, 184 insertions(+), 42 deletions(-) delete mode 100644 features/messages/src/test/kotlin/io/element/android/features/messages/MessagePresenterTest.kt create mode 100644 features/messages/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt diff --git a/features/messages/src/test/kotlin/io/element/android/features/messages/MessagePresenterTest.kt b/features/messages/src/test/kotlin/io/element/android/features/messages/MessagePresenterTest.kt deleted file mode 100644 index c50932f6b8..0000000000 --- a/features/messages/src/test/kotlin/io/element/android/features/messages/MessagePresenterTest.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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 kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runTest -import org.junit.Test - -class MessagePresenterTest { - @Test - fun `present - initial state`() = runTest { - /* - TO BE COMPLETED - val presenter = MessagesPresenter( - FakeMatrixClient(SessionId("sessionId")), - ) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized) - } - */ - } -} 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..fdd0a29923 --- /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.FakeMatrixClient +import io.element.android.libraries.matrixtest.core.A_ROOM_ID +import io.element.android.libraries.matrixtest.room.A_MESSAGE +import io.element.android.libraries.matrixtest.room.FakeMatrixRoom +import io.element.android.libraries.matrixtest.timeline.AN_EVENT_ID +import io.element.android.libraries.matrixtest.timeline.A_SENDER_ID +import io.element.android.libraries.matrixtest.timeline.A_SENDER_NAME +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(A_ROOM_ID) + 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(A_ROOM_ID) + ): 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_SENDER_ID, + senderDisplayName = A_SENDER_NAME, + senderAvatar = AvatarData(), + content = content, + sentTime = "", + isMine = isMine, + reactionsState = TimelineItemReactions(persistentListOf()) +) 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 eae9995d13..6600b0be9a 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 @@ -74,7 +74,12 @@ class FakeMatrixRoom( 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) } } From ce482a29bc314c3ac609d7a7565eb5b3fd8483f2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 9 Feb 2023 17:13:32 +0100 Subject: [PATCH 85/96] Update coverage thresholds --- build.gradle.kts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 6ee2173c49..53a7121cdf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -185,9 +185,9 @@ koverMerged { name = "Global minimum code coverage." target = kotlinx.kover.api.VerificationTarget.ALL bound { - minValue = 35 + minValue = 40 // Setting a max value, so that if coverage is bigger, it means that we have to change minValue. - maxValue = 40 + maxValue = 45 counter = kotlinx.kover.api.CounterType.LINE valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE } @@ -198,10 +198,11 @@ koverMerged { target = kotlinx.kover.api.VerificationTarget.CLASS overrideClassFilter { includes += "*Presenter" + excludes += "*TemplatePresenter" } bound { - minValue = 80 - counter = kotlinx.kover.api.CounterType.LINE + minValue = 90 + counter = kotlinx.kover.api.CounterType.INSTRUCTION valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE } } From 606cd3efc3d39b5c20ea85e93ced35073b835946 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 9 Feb 2023 17:44:15 +0100 Subject: [PATCH 86/96] Cleanup and centralize test data. --- .../changeserver/ChangeServerPresenterTest.kt | 2 +- .../login/root/LoginRootPresenterTest.kt | 22 +++++----- .../logout/LogoutPreferencePresenterTest.kt | 2 +- .../messages/MessagesPresenterTest.kt | 18 ++++---- .../actionlist/ActionListPresenterTest.kt | 12 +++--- .../MessageComposerPresenterTest.kt | 33 ++++++++------- .../timeline/TimelinePresenterTest.kt | 11 +++-- .../roomlist/RoomListPresenterTests.kt | 38 ++++++++++++++--- .../libraries/matrixtest/FakeMatrixClient.kt | 9 ++-- .../android/libraries/matrixtest/TestData.kt | 41 +++++++++++++++++++ .../auth/FakeAuthenticationService.kt | 9 +--- .../matrixtest/core/RoomIdFixture.kt | 22 ---------- .../matrixtest/room/FakeMatrixRoom.kt | 3 +- .../matrixtest/room/RoomSummaryFixture.kt | 9 ++-- .../matrixtest/timeline/FakeMatrixTimeline.kt | 5 --- 15 files changed, 135 insertions(+), 101 deletions(-) create mode 100644 libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/TestData.kt delete mode 100644 libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/core/RoomIdFixture.kt 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 index c8ff3de1ae..b22c03dc35 100644 --- 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 @@ -23,7 +23,7 @@ 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.auth.A_HOMESERVER +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 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 index ba3b5e644a..f9eed8d66f 100644 --- 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 @@ -23,12 +23,12 @@ 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.auth.A_FAILURE -import io.element.android.libraries.matrixtest.auth.A_HOMESERVER -import io.element.android.libraries.matrixtest.auth.A_HOMESERVER_2 -import io.element.android.libraries.matrixtest.auth.A_LOGIN -import io.element.android.libraries.matrixtest.auth.A_PASSWORD -import io.element.android.libraries.matrixtest.auth.A_SESSION_ID +import io.element.android.libraries.matrixtest.A_FAILURE +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_USER_NAME import io.element.android.libraries.matrixtest.auth.FakeAuthenticationService import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -60,13 +60,13 @@ class LoginRootPresenterTest { presenter.present() }.test { val initialState = awaitItem() - initialState.eventSink.invoke(LoginRootEvents.SetLogin(A_LOGIN)) + initialState.eventSink.invoke(LoginRootEvents.SetLogin(A_USER_NAME)) val loginState = awaitItem() - assertThat(loginState.formState).isEqualTo(LoginFormState(login = A_LOGIN, password = "")) + 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_LOGIN, password = A_PASSWORD)) + assertThat(loginAndPasswordState.formState).isEqualTo(LoginFormState(login = A_USER_NAME, password = A_PASSWORD)) assertThat(loginAndPasswordState.submitEnabled).isTrue() } } @@ -80,7 +80,7 @@ class LoginRootPresenterTest { presenter.present() }.test { val initialState = awaitItem() - initialState.eventSink.invoke(LoginRootEvents.SetLogin(A_LOGIN)) + initialState.eventSink.invoke(LoginRootEvents.SetLogin(A_USER_NAME)) initialState.eventSink.invoke(LoginRootEvents.SetPassword(A_PASSWORD)) skipItems(1) val loginAndPasswordState = awaitItem() @@ -102,7 +102,7 @@ class LoginRootPresenterTest { presenter.present() }.test { val initialState = awaitItem() - initialState.eventSink.invoke(LoginRootEvents.SetLogin(A_LOGIN)) + initialState.eventSink.invoke(LoginRootEvents.SetLogin(A_USER_NAME)) initialState.eventSink.invoke(LoginRootEvents.SetPassword(A_PASSWORD)) skipItems(1) val loginAndPasswordState = awaitItem() 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 index 1f1643d981..5367c21e4d 100644 --- 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 @@ -24,8 +24,8 @@ 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_FAILURE import io.element.android.libraries.matrixtest.FakeMatrixClient -import io.element.android.libraries.matrixtest.auth.A_FAILURE import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Test 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 index fdd0a29923..a16d9f7eb1 100644 --- 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 @@ -33,13 +33,13 @@ import io.element.android.features.messages.timeline.model.content.TimelineItemT 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.core.A_ROOM_ID -import io.element.android.libraries.matrixtest.room.A_MESSAGE import io.element.android.libraries.matrixtest.room.FakeMatrixRoom -import io.element.android.libraries.matrixtest.timeline.AN_EVENT_ID -import io.element.android.libraries.matrixtest.timeline.A_SENDER_ID -import io.element.android.libraries.matrixtest.timeline.A_SENDER_NAME import io.element.android.libraries.textcomposer.MessageComposerMode import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -119,7 +119,7 @@ class MessagesPresenterTest { @Test fun `present - handle action redact`() = runTest { - val matrixRoom = FakeMatrixRoom(A_ROOM_ID) + val matrixRoom = FakeMatrixRoom() val presenter = createMessagePresenter(matrixRoom) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -132,7 +132,7 @@ class MessagesPresenterTest { } private fun TestScope.createMessagePresenter( - matrixRoom: MatrixRoom = FakeMatrixRoom(A_ROOM_ID) + matrixRoom: MatrixRoom = FakeMatrixRoom() ): MessagesPresenter { val matrixClient = FakeMatrixClient() val messageComposerPresenter = MessageComposerPresenter( @@ -168,8 +168,8 @@ private fun aMessageEvent( content: TimelineItemContent = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null), ) = TimelineItem.MessageEvent( id = AN_EVENT_ID, - senderId = A_SENDER_ID, - senderDisplayName = A_SENDER_NAME, + senderId = A_USER_ID.value, + senderDisplayName = A_USER_NAME, senderAvatar = AvatarData(), content = content, sentTime = "", 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 index 742463d5ed..06d1486293 100644 --- 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 @@ -29,10 +29,10 @@ import io.element.android.features.messages.timeline.model.content.TimelineItemC 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.room.A_MESSAGE -import io.element.android.libraries.matrixtest.timeline.AN_EVENT_ID -import io.element.android.libraries.matrixtest.timeline.A_SENDER_ID -import io.element.android.libraries.matrixtest.timeline.A_SENDER_NAME +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 @@ -166,8 +166,8 @@ private fun aMessageEvent( content: TimelineItemContent, ) = TimelineItem.MessageEvent( id = AN_EVENT_ID, - senderId = A_SENDER_ID, - senderDisplayName = A_SENDER_NAME, + senderId = A_USER_ID.value, + senderDisplayName = A_USER_NAME, senderAvatar = AvatarData(), content = content, sentTime = "", 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 index 9a0123b044..8278acd13e 100644 --- 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 @@ -24,13 +24,12 @@ 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.core.A_ROOM_ID -import io.element.android.libraries.matrixtest.room.ANOTHER_MESSAGE -import io.element.android.libraries.matrixtest.room.A_MESSAGE -import io.element.android.libraries.matrixtest.room.A_REPLY +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.matrixtest.timeline.AN_EVENT_ID -import io.element.android.libraries.matrixtest.timeline.A_SENDER_NAME import io.element.android.libraries.textcomposer.MessageComposerMode import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -41,7 +40,7 @@ class MessageComposerPresenterTest { fun `present - initial state`() = runTest { val presenter = MessageComposerPresenter( this, - FakeMatrixRoom(A_ROOM_ID) + FakeMatrixRoom() ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -58,7 +57,7 @@ class MessageComposerPresenterTest { fun `present - toggle fullscreen`() = runTest { val presenter = MessageComposerPresenter( this, - FakeMatrixRoom(A_ROOM_ID) + FakeMatrixRoom() ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -77,7 +76,7 @@ class MessageComposerPresenterTest { fun `present - change message`() = runTest { val presenter = MessageComposerPresenter( this, - FakeMatrixRoom(A_ROOM_ID) + FakeMatrixRoom() ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -98,7 +97,7 @@ class MessageComposerPresenterTest { fun `present - change mode to edit`() = runTest { val presenter = MessageComposerPresenter( this, - FakeMatrixRoom(A_ROOM_ID) + FakeMatrixRoom() ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -128,7 +127,7 @@ class MessageComposerPresenterTest { fun `present - change mode to reply`() = runTest { val presenter = MessageComposerPresenter( this, - FakeMatrixRoom(A_ROOM_ID) + FakeMatrixRoom() ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -148,7 +147,7 @@ class MessageComposerPresenterTest { fun `present - change mode to quote`() = runTest { val presenter = MessageComposerPresenter( this, - FakeMatrixRoom(A_ROOM_ID) + FakeMatrixRoom() ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -168,7 +167,7 @@ class MessageComposerPresenterTest { fun `present - send message`() = runTest { val presenter = MessageComposerPresenter( this, - FakeMatrixRoom(A_ROOM_ID) + FakeMatrixRoom() ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -187,7 +186,7 @@ class MessageComposerPresenterTest { @Test fun `present - edit message`() = runTest { - val fakeMatrixRoom = FakeMatrixRoom(A_ROOM_ID) + val fakeMatrixRoom = FakeMatrixRoom() val presenter = MessageComposerPresenter( this, fakeMatrixRoom @@ -218,7 +217,7 @@ class MessageComposerPresenterTest { @Test fun `present - reply message`() = runTest { - val fakeMatrixRoom = FakeMatrixRoom(A_ROOM_ID) + val fakeMatrixRoom = FakeMatrixRoom() val presenter = MessageComposerPresenter( this, fakeMatrixRoom @@ -230,7 +229,7 @@ class MessageComposerPresenterTest { assertThat(initialState.text).isEqualTo(StableCharSequence("")) val mode = aReplyMode() initialState.eventSink.invoke(MessageComposerEvents.SetMode(mode)) - var state = awaitItem() + val state = awaitItem() assertThat(state.mode).isEqualTo(mode) assertThat(state.text).isEqualTo(StableCharSequence("")) assertThat(state.isSendButtonVisible).isFalse() @@ -249,5 +248,5 @@ class MessageComposerPresenterTest { } fun anEditMode() = MessageComposerMode.Edit(AN_EVENT_ID, A_MESSAGE) -fun aReplyMode() = MessageComposerMode.Reply(A_SENDER_NAME, 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/TimelinePresenterTest.kt b/features/messages/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt index 1234c8619c..ee73dc260b 100644 --- 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 @@ -24,10 +24,9 @@ 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.core.A_ROOM_ID import io.element.android.libraries.matrixtest.room.FakeMatrixRoom -import io.element.android.libraries.matrixtest.timeline.AN_EVENT_ID import io.element.android.libraries.matrixtest.timeline.FakeMatrixTimeline import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -39,7 +38,7 @@ class TimelinePresenterTest { val presenter = TimelinePresenter( testCoroutineDispatchers(), FakeMatrixClient(), - FakeMatrixRoom(A_ROOM_ID) + FakeMatrixRoom() ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -52,7 +51,7 @@ class TimelinePresenterTest { @Test fun `present - load more`() = runTest { val matrixTimeline = FakeMatrixTimeline() - val matrixRoom = FakeMatrixRoom(A_ROOM_ID, matrixTimeline = matrixTimeline) + val matrixRoom = FakeMatrixRoom(matrixTimeline = matrixTimeline) val presenter = TimelinePresenter( testCoroutineDispatchers(), FakeMatrixClient(), @@ -73,7 +72,7 @@ class TimelinePresenterTest { @Test fun `present - set highlighted event`() = runTest { val matrixTimeline = FakeMatrixTimeline() - val matrixRoom = FakeMatrixRoom(A_ROOM_ID, matrixTimeline = matrixTimeline) + val matrixRoom = FakeMatrixRoom(matrixTimeline = matrixTimeline) val presenter = TimelinePresenter( testCoroutineDispatchers(), FakeMatrixClient(), @@ -96,7 +95,7 @@ class TimelinePresenterTest { @Test fun `present - test callback`() = runTest { val matrixTimeline = FakeMatrixTimeline() - val matrixRoom = FakeMatrixRoom(A_ROOM_ID, matrixTimeline = matrixTimeline) + val matrixRoom = FakeMatrixRoom(matrixTimeline = matrixTimeline) val presenter = TimelinePresenter( testCoroutineDispatchers(), FakeMatrixClient(), 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 ad2e02fb51..e15c08fecc 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 @@ -27,11 +27,13 @@ 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.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.core.A_ROOM_ID -import io.element.android.libraries.matrixtest.core.A_ROOM_ID_VALUE -import io.element.android.libraries.matrixtest.room.A_MESSAGE -import io.element.android.libraries.matrixtest.room.A_ROOM_NAME import io.element.android.libraries.matrixtest.room.FakeRoomSummaryDataSource import io.element.android.libraries.matrixtest.room.aRoomSummaryFilled import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -55,6 +57,32 @@ class RoomListPresenterTests { assertThat(initialState.matrixUser).isNull() val withUserState = awaitItem() 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(Exception("Error")), + userAvatarURLString = Result.failure(Exception("Error")), + ), + 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) } } @@ -182,7 +210,7 @@ class RoomListPresenterTests { private const val A_FORMATTED_DATE = "formatted_date" private val aRoomListRoomSummary = RoomListRoomSummary( - id = A_ROOM_ID_VALUE, + id = A_ROOM_ID.value, roomId = A_ROOM_ID, name = A_ROOM_NAME, hasUnread = true, 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 c7aab0952a..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 @@ -23,7 +23,6 @@ import io.element.android.libraries.matrix.core.UserId import io.element.android.libraries.matrix.media.MediaResolver import io.element.android.libraries.matrix.room.MatrixRoom import io.element.android.libraries.matrix.room.RoomSummaryDataSource -import io.element.android.libraries.matrixtest.auth.A_SESSION_ID import io.element.android.libraries.matrixtest.media.FakeMediaResolver import io.element.android.libraries.matrixtest.room.FakeMatrixRoom import io.element.android.libraries.matrixtest.room.FakeRoomSummaryDataSource @@ -32,6 +31,8 @@ import org.matrix.rustcomponents.sdk.MediaSource 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 { @@ -62,14 +63,14 @@ class FakeMatrixClient( logoutFailure?.let { throw it } } - override fun userId(): UserId = UserId("") + 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..cd186c50c2 --- /dev/null +++ b/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/TestData.kt @@ -0,0 +1,41 @@ +/* + * 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" + +val A_FAILURE = Throwable("error") 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 index 936bd01545..ab4935ede3 100644 --- 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 @@ -19,17 +19,12 @@ 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 -const val A_HOMESERVER = "matrix.org" -const val A_HOMESERVER_2 = "matrix-client.org" -const val A_SESSION_ID = "sessionId" -const val A_LOGIN = "login" -const val A_PASSWORD = "password" -val A_FAILURE = Throwable("error") - class FakeAuthenticationService : MatrixAuthenticationService { private var homeserver: String = A_HOMESERVER private var loginError: Throwable? = null diff --git a/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/core/RoomIdFixture.kt b/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/core/RoomIdFixture.kt deleted file mode 100644 index 5b62ad383a..0000000000 --- a/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/core/RoomIdFixture.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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.core - -import io.element.android.libraries.matrix.core.RoomId - -const val A_ROOM_ID_VALUE = "!aRoomId" -val A_ROOM_ID = RoomId(A_ROOM_ID_VALUE) 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 6600b0be9a..18e033b87d 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,13 +20,14 @@ 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 = "", 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 index 6e7fb1de32..41d9f6d524 100644 --- 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 @@ -19,12 +19,9 @@ 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.core.A_ROOM_ID - -const val A_ROOM_NAME = "aRoomName" -const val A_MESSAGE = "Hello world!" -const val A_REPLY = "OK, I'll be there!" -const val ANOTHER_MESSAGE = "Hello universe!" +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, 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 417489dc83..c768a46ad7 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 @@ -24,11 +24,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import org.matrix.rustcomponents.sdk.TimelineListener -const val A_SENDER_NAME = "Alice" -const val A_SENDER_ID = "@alice:server.org" -const val AN_EVENT_ID_VALUE = "!anEventId" -val AN_EVENT_ID = EventId(AN_EVENT_ID_VALUE) - class FakeMatrixTimeline : MatrixTimeline { override var callback: MatrixTimeline.Callback? = null From a57a6f45bb51010e3184de5b0f93763c3c97d423 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 9 Feb 2023 18:09:03 +0100 Subject: [PATCH 87/96] Exclude generated classes from code coverage metrics. --- build.gradle.kts | 18 ++++++++++++------ .../features/messages/timeline/TimelineView.kt | 5 ----- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 53a7121cdf..73c043783c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -165,12 +165,18 @@ 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*", + "*_Module*", + "*ComposableSingletons$*", + "*_AssistedFactory_Impl*", + "*BuildConfig", + // Other ) ) } 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 a6d6d8a2af..bc8a02c30b 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 @@ -352,11 +352,6 @@ internal fun TimelineLoadingMoreIndicator() { } } -class MessagesItemGroupPositionToMessagesTimelineItemContentProvider : - PairCombinedPreviewParameter( - TimelineItemGroupPositionProvider() to MessagesTimelineItemContentProvider() - ) - @Preview @Composable fun LoginRootScreenLightPreview( From 5875de1c51f4151dd1f17816f8460456370ffe44 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 9 Feb 2023 18:21:25 +0100 Subject: [PATCH 88/96] Rename classes (not a State) --- .../features/onboarding/OnBoardingScreen.kt | 8 ++++---- ...plashCarouselState.kt => SplashCarouselData.kt} | 2 +- ...tateFactory.kt => SplashCarouselDataFactory.kt} | 14 +++++++------- 3 files changed, 12 insertions(+), 12 deletions(-) rename features/onboarding/src/main/kotlin/io/element/android/features/onboarding/{SplashCarouselState.kt => SplashCarouselData.kt} (96%) rename features/onboarding/src/main/kotlin/io/element/android/features/onboarding/{SplashCarouselStateFactory.kt => SplashCarouselDataFactory.kt} (90%) 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 dc8362563e..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 @@ -59,8 +59,8 @@ 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) } Box( modifier = modifier @@ -92,7 +92,7 @@ fun OnBoardingScreen( state = pagerState, ) { page -> // Our page content - OnBoardingPage(carrouselState.items[page]) + OnBoardingPage(carrouselData.items[page]) } HorizontalPagerIndicator( pagerState = pagerState, @@ -118,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( From 996537a89072e6cf7764da309791d207ce742928 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 9 Feb 2023 18:22:16 +0100 Subject: [PATCH 89/96] Add a test --- .../features/rageshake/bugreport/BugReportPresenterTest.kt | 1 + 1 file changed, 1 insertion(+) 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 index 8737415d6c..bb367b15b3 100644 --- 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 @@ -190,6 +190,7 @@ class BugReportPresenterTest { 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) From 5e842dd68380f334ddee9696124fbad13646467e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 9 Feb 2023 18:23:28 +0100 Subject: [PATCH 90/96] Add rule for minimum test coverage on States --- build.gradle.kts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index 73c043783c..af0cbaaa08 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -212,5 +212,18 @@ koverMerged { 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 + } + } } } From 285ae6083bf5973ac34cc99d540f7e442fc235cb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 9 Feb 2023 18:28:05 +0100 Subject: [PATCH 91/96] Exclude Node classes from code coverage metrics. --- build.gradle.kts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index af0cbaaa08..2ea2678c8b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -171,12 +171,17 @@ koverMerged { "anvil.hint.merge.*", "anvil.module.*", "com.airbnb.android.showkase*", - "*_Factory*", - "*_Module*", + "*_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$*", ) ) } From 3142b884c50b226ff172625660ecc989ce5b0eb0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 9 Feb 2023 18:41:58 +0100 Subject: [PATCH 92/96] Move test data to TestData.kt --- .../kotlin/io/element/android/x/root/FakeBugReporter.kt | 5 ++--- .../android/features/login/root/LoginRootPresenterTest.kt | 6 +++--- .../features/logout/LogoutPreferencePresenterTest.kt | 6 +++--- .../features/rageshake/bugreport/BugReportPresenterTest.kt | 3 ++- .../android/features/rageshake/bugreport/FakeBugReporter.kt | 5 ++--- .../rageshake/detection/RageshakeDetectionPresenterTest.kt | 3 ++- .../android/features/roomlist/RoomListPresenterTests.kt | 5 +++-- .../io/element/android/libraries/matrixtest/TestData.kt | 5 ++++- 8 files changed, 21 insertions(+), 17 deletions(-) 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 index f6cc6c8960..c6f7000cdf 100644 --- a/app/src/test/kotlin/io/element/android/x/root/FakeBugReporter.kt +++ b/app/src/test/kotlin/io/element/android/x/root/FakeBugReporter.kt @@ -19,12 +19,11 @@ 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 -const val A_REASON = "There has been a failure" - // TODO Remove this duplicated class when we will rework modules. class FakeBugReporter(val mode: FakeBugReporterMode = FakeBugReporterMode.Success) : BugReporter { override fun sendBugReport( @@ -49,7 +48,7 @@ class FakeBugReporter(val mode: FakeBugReporterMode = FakeBugReporterMode.Succes when (mode) { FakeBugReporterMode.Success -> Unit FakeBugReporterMode.Failure -> { - listener?.onUploadFailed(A_REASON) + listener?.onUploadFailed(A_FAILURE_REASON) return@launch } FakeBugReporterMode.Cancel -> { 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 index f9eed8d66f..3fa20ae93a 100644 --- 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 @@ -23,11 +23,11 @@ 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_FAILURE 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 @@ -106,12 +106,12 @@ class LoginRootPresenterTest { initialState.eventSink.invoke(LoginRootEvents.SetPassword(A_PASSWORD)) skipItems(1) val loginAndPasswordState = awaitItem() - authenticationService.givenLoginError(A_FAILURE) + 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_FAILURE)) + assertThat(loggedInState.loggedInState).isEqualTo(LoggedInState.ErrorLoggingIn(A_THROWABLE)) } } 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 index 5367c21e4d..e84226b3e8 100644 --- 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 @@ -24,7 +24,7 @@ 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_FAILURE +import io.element.android.libraries.matrixtest.A_THROWABLE import io.element.android.libraries.matrixtest.FakeMatrixClient import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -71,12 +71,12 @@ class LogoutPreferencePresenterTest { presenter.present() }.test { val initialState = awaitItem() - matrixClient.givenLogoutError(A_FAILURE) + 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_FAILURE)) + assertThat(successState.logoutAction).isEqualTo(Async.Failure(A_THROWABLE)) } } } 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 index bb367b15b3..484c3bd1b0 100644 --- 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 @@ -25,6 +25,7 @@ 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 @@ -218,7 +219,7 @@ class BugReportPresenterTest { assertThat(awaitItem().sendingProgress).isEqualTo(0.5f) // Failure assertThat(awaitItem().sendingProgress).isEqualTo(0f) - assertThat((awaitItem().sending as Async.Failure).error.message).isEqualTo(A_REASON) + assertThat((awaitItem().sending as Async.Failure).error.message).isEqualTo(A_FAILURE_REASON) } } 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 index a1a2c613a7..29977d7a95 100644 --- 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 @@ -19,12 +19,11 @@ 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 -const val A_REASON = "There has been a failure" - class FakeBugReporter(val mode: FakeBugReporterMode = FakeBugReporterMode.Success) : BugReporter { override fun sendBugReport( coroutineScope: CoroutineScope, @@ -48,7 +47,7 @@ class FakeBugReporter(val mode: FakeBugReporterMode = FakeBugReporterMode.Succes when (mode) { FakeBugReporterMode.Success -> Unit FakeBugReporterMode.Failure -> { - listener?.onUploadFailed(A_REASON) + listener?.onUploadFailed(A_FAILURE_REASON) return@launch } FakeBugReporterMode.Cancel -> { 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 index f5fb8cbedc..1ef8941bff 100644 --- 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 @@ -28,6 +28,7 @@ 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 @@ -142,7 +143,7 @@ class RageshakeDetectionPresenterTest { rageshake.triggerPhoneRageshake() assertThat(awaitItem().takeScreenshot).isTrue() initialState.eventSink.invoke( - RageshakeDetectionEvents.ProcessScreenshot(ImageResult.Error(Exception("Error"))) + RageshakeDetectionEvents.ProcessScreenshot(ImageResult.Error(AN_EXCEPTION)) ) assertThat(awaitItem().showDialog).isTrue() initialState.eventSink.invoke(RageshakeDetectionEvents.Dismiss) 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 e15c08fecc..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 @@ -28,6 +28,7 @@ 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 @@ -69,8 +70,8 @@ class RoomListPresenterTests { val presenter = RoomListPresenter( FakeMatrixClient( SessionId("sessionId"), - userDisplayName = Result.failure(Exception("Error")), - userAvatarURLString = Result.failure(Exception("Error")), + userDisplayName = Result.failure(AN_EXCEPTION), + userAvatarURLString = Result.failure(AN_EXCEPTION), ), createDateFormatter() ) 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 index cd186c50c2..970a3882ab 100644 --- 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 @@ -38,4 +38,7 @@ const val A_SESSION_ID = "sessionId" const val AN_AVATAR_URL = "mxc://data" -val A_FAILURE = Throwable("error") +const val A_FAILURE_REASON = "There has been a failure" +val A_THROWABLE = Throwable(A_FAILURE_REASON) +val AN_EXCEPTION = Exception(A_FAILURE_REASON) + From 4475265403b4ccc1ff0d9a2f9eb0e70fe7ae91b2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 9 Feb 2023 18:43:17 +0100 Subject: [PATCH 93/96] Global coverage is now 45.7 :rocket: --- build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 2ea2678c8b..0869ecc6ca 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -196,9 +196,9 @@ koverMerged { name = "Global minimum code coverage." target = kotlinx.kover.api.VerificationTarget.ALL bound { - minValue = 40 + minValue = 45 // Setting a max value, so that if coverage is bigger, it means that we have to change minValue. - maxValue = 45 + maxValue = 50 counter = kotlinx.kover.api.CounterType.LINE valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE } From a04908819a7aed3dc11153ad3c3d789a446bca2d Mon Sep 17 00:00:00 2001 From: bmarty Date: Fri, 10 Feb 2023 06:06:37 +0000 Subject: [PATCH 94/96] Import strings from Element Android --- libraries/ui-strings/src/main/res/values/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/ui-strings/src/main/res/values/strings.xml b/libraries/ui-strings/src/main/res/values/strings.xml index 17fbadd776..2058d13d1d 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 From c9df2619da8016fa06a63182fa6d911f3b4a277d Mon Sep 17 00:00:00 2001 From: bmarty Date: Tue, 14 Feb 2023 06:06:21 +0000 Subject: [PATCH 95/96] Import strings from Element Android --- libraries/ui-strings/src/main/res/values/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/ui-strings/src/main/res/values/strings.xml b/libraries/ui-strings/src/main/res/values/strings.xml index 2058d13d1d..66c66bbcbe 100644 --- a/libraries/ui-strings/src/main/res/values/strings.xml +++ b/libraries/ui-strings/src/main/res/values/strings.xml @@ -3211,6 +3211,7 @@ Displaying polls Load more polls Error fetching polls. + View poll in timeline Share location From f3926b00f526620dd1fa7f610900c17aff0e5dd7 Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Tue, 14 Feb 2023 17:09:04 +0100 Subject: [PATCH 96/96] Add EXF issues to the X-Plorer project --- .github/workflows/triage-labelled.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) 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 }}