diff --git a/.gitattributes b/.gitattributes
index 0542767eff..2062142284 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1 +1,2 @@
**/snapshots/**/*.png filter=lfs diff=lfs merge=lfs -text
+**/docs/images-lfs/*.png filter=lfs diff=lfs merge=lfs -text
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 657dc7c772..9579e81997 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -2,7 +2,8 @@ name: APK Build
on:
workflow_dispatch:
- pull_request: { }
+ pull_request:
+ merge_group:
push:
branches: [ develop ]
@@ -13,14 +14,17 @@ env:
jobs:
debug:
- name: Build debug APKs
+ name: Build APKs
runs-on: ubuntu-latest
- if: github.ref != 'refs/heads/main'
+ # Skip for `main` and the merge queue if the branch is up to date with `develop`
+ if: github.ref != 'refs/heads/main' && github.event.merge_group.base_ref != 'refs/heads/develop'
strategy:
+ matrix:
+ variant: [debug, release, nightly, samples]
fail-fast: false
# Allow all jobs on develop. Just one per PR.
concurrency:
- group: ${{ github.ref == 'refs/heads/develop' && format('build-develop-{0}', github.sha) || format('build-debug-{0}', github.ref) }}
+ group: ${{ github.ref == 'refs/heads/develop' && format('build-develop-{0}-{1}', matrix.variant, github.sha) || format('build-{0}-{1}', matrix.variant, github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v3
@@ -38,12 +42,14 @@ jobs:
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug APK
+ if: ${{ matrix.variant == 'debug' }}
env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
run: ./gradlew assembleDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
- - name: Upload debug APKs
+ - name: Upload APK APKs
+ if: ${{ matrix.variant == 'debug' }}
uses: actions/upload-artifact@v3
with:
name: elementx-debug
@@ -55,12 +61,12 @@ jobs:
continue-on-error: true
env:
token: ${{ secrets.DIAWI_TOKEN }}
- if: ${{ github.event_name == 'pull_request' && env.token != '' }}
+ if: ${{ matrix.variant == 'debug' && github.event_name == 'pull_request' && env.token != '' }}
with:
token: ${{ env.token }}
file: app/build/outputs/apk/debug/app-arm64-v8a-debug.apk
- name: Add or update PR comment with QR Code to download APK.
- if: ${{ github.event_name == 'pull_request' && steps.diawi.conclusion == 'success' }}
+ if: ${{ matrix.variant == 'debug' && github.event_name == 'pull_request' && steps.diawi.conclusion == 'success' }}
uses: NejcZdovc/comment-pr@v2
with:
message: |
@@ -72,8 +78,11 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Compile release sources
+ if: ${{ matrix.variant == 'release' }}
run: ./gradlew compileReleaseSources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
- name: Compile nightly sources
+ if: ${{ matrix.variant == 'nightly' }}
run: ./gradlew compileNightlySources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
- name: Compile samples minimal
+ if: ${{ matrix.variant == 'samples' }}
run: ./gradlew :samples:minimal:assemble $CI_GRADLE_ARG_PROPERTIES
diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml
index 223a273b68..4d997ec632 100644
--- a/.github/workflows/danger.yml
+++ b/.github/workflows/danger.yml
@@ -1,17 +1,19 @@
name: Danger CI
-on: [pull_request]
+on: [pull_request, merge_group]
jobs:
build:
runs-on: ubuntu-latest
+ # Don't run in the merge queue again if the branch is up to date with `develop`
+ if: github.event.merge_group.base_ref != 'refs/heads/develop'
name: Danger main check
steps:
- uses: actions/checkout@v3
- run: |
npm install --save-dev @babel/plugin-transform-flow-strip-types
- name: Danger
- uses: danger/danger-js@11.2.6
+ uses: danger/danger-js@11.2.8
with:
args: "--dangerfile ./tools/danger/dangerfile.js"
env:
diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml
index 7b68c0077d..c1e478b15c 100644
--- a/.github/workflows/gradle-wrapper-validation.yml
+++ b/.github/workflows/gradle-wrapper-validation.yml
@@ -1,12 +1,15 @@
name: "Validate Gradle Wrapper"
on:
- pull_request: { }
+ pull_request:
+ merge_group:
push:
branches: [ main, develop ]
jobs:
validation:
name: "Validation"
+ # Don't run in the merge queue again if the branch is up to date with `develop`
+ if: github.event.merge_group.base_ref != 'refs/heads/develop'
runs-on: ubuntu-latest
# No concurrency required, this is a prerequisite to other actions and should run every time.
steps:
diff --git a/.github/workflows/maestro.yml b/.github/workflows/maestro.yml
index d3274d0b14..74fb1cfc83 100644
--- a/.github/workflows/maestro.yml
+++ b/.github/workflows/maestro.yml
@@ -40,12 +40,6 @@ jobs:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
- - name: Upload debug APKs
- uses: actions/upload-artifact@v3
- with:
- name: elementx-debug
- path: |
- app/build/outputs/apk/debug/*.apk
- uses: mobile-dev-inc/action-maestro-cloud@v1.4.1
with:
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml
index ecc40760be..1efa0ae215 100644
--- a/.github/workflows/quality.yml
+++ b/.github/workflows/quality.yml
@@ -2,7 +2,8 @@ name: Code Quality Checks
on:
workflow_dispatch:
- pull_request: { }
+ pull_request:
+ merge_group:
push:
branches: [ main, develop ]
@@ -15,6 +16,8 @@ jobs:
checkScript:
name: Search for forbidden patterns
runs-on: ubuntu-latest
+ # Don't run in the merge queue again if the branch is up to date with `develop`
+ if: github.event.merge_group.base_ref != 'refs/heads/develop'
steps:
- uses: actions/checkout@v3
- name: Run code quality check suite
@@ -23,6 +26,8 @@ jobs:
check:
name: Project Check Suite
runs-on: ubuntu-latest
+ # Don't run in the merge queue again if the branch is up to date with `develop`
+ if: github.event.merge_group.base_ref != 'refs/heads/develop'
# Allow all jobs on main and develop. Just one per PR.
concurrency:
group: ${{ github.ref == 'refs/heads/main' && format('check-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-develop-{0}', github.sha) || format('check-{0}', github.ref) }}
@@ -65,7 +70,7 @@ jobs:
yarn add danger-plugin-lint-report --dev
- name: Danger lint
if: always()
- uses: danger/danger-js@11.2.6
+ uses: danger/danger-js@11.2.8
with:
args: "--dangerfile ./tools/danger/dangerfile-lint.js"
env:
diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml
new file mode 100644
index 0000000000..24e5b5ad9b
--- /dev/null
+++ b/.github/workflows/sonar.yml
@@ -0,0 +1,51 @@
+name: Code Quality Checks
+
+on:
+ workflow_dispatch:
+ pull_request:
+ merge_group:
+ push:
+ branches: [ main, develop ]
+
+# Enrich gradle.properties for CI/CD
+env:
+ GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -XX:MaxMetaspaceSize=512m -Dkotlin.daemon.jvm.options="-Xmx2g" -Dkotlin.incremental=false
+ CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon --warn
+
+jobs:
+ sonar:
+ name: Project Check Suite
+ runs-on: ubuntu-latest
+ # Don't run in the merge queue again if the branch is up to date with `develop`
+ if: github.event.merge_group.base_ref != 'refs/heads/develop'
+ # Allow all jobs on main and develop. Just one per PR.
+ concurrency:
+ group: ${{ github.ref == 'refs/heads/main' && format('sonar-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('sonar-develop-{0}', github.sha) || format('sonar-{0}', github.ref) }}
+ cancel-in-progress: true
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ # Ensure we are building the branch and not the branch after being merged on develop
+ # https://github.com/actions/checkout/issues/881
+ ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
+ - name: Use JDK 17
+ uses: actions/setup-java@v3
+ with:
+ distribution: 'temurin' # See 'Supported distributions' for available options
+ java-version: '17'
+ - name: Configure gradle
+ uses: gradle/gradle-build-action@v2.7.0
+ with:
+ cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
+ - name: 🔊 Publish results to Sonar
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }}
+ if: ${{ always() && env.SONAR_TOKEN != '' && env.ORG_GRADLE_PROJECT_SONAR_LOGIN != '' }}
+ run: ./gradlew sonar $CI_GRADLE_ARG_PROPERTIES
+ - name: Prepare Danger
+ if: always()
+ run: |
+ npm install --save-dev @babel/core
+ npm install --save-dev @babel/plugin-transform-flow-strip-types
+ yarn add danger-plugin-lint-report --dev
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index e4f2300b4c..97a739f747 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -2,7 +2,8 @@ name: Test
on:
workflow_dispatch:
- pull_request: { }
+ pull_request:
+ merge_group:
push:
branches: [ main, develop ]
@@ -15,6 +16,8 @@ jobs:
tests:
name: Runs unit tests
runs-on: ubuntu-latest
+ # Don't run in the merge queue again if the branch is up to date with `develop`
+ if: github.event.merge_group.base_ref != 'refs/heads/develop'
# Allow all jobs on main and develop. Just one per PR.
concurrency:
diff --git a/.github/workflows/validate-lfs.yml b/.github/workflows/validate-lfs.yml
index 25fe50359c..63ded8f4e1 100644
--- a/.github/workflows/validate-lfs.yml
+++ b/.github/workflows/validate-lfs.yml
@@ -1,10 +1,12 @@
name: Validate Git LFS
-on: [pull_request]
+on: [pull_request, merge_group]
jobs:
build:
runs-on: ubuntu-latest
+ # Don't run in the merge queue again if the branch is up to date with `develop`
+ if: github.event.merge_group.base_ref != 'refs/heads/develop'
name: Validate
steps:
- uses: nschloe/action-cached-lfs-checkout@v1.2.1
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index 9a55c2de1f..fdf8d994a6 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,6 @@
-./gradlew check +./tools/quality/check.shSome separate commands can also be run, see below. +#### detekt + +
+./gradlew detekt ++ #### ktlint
@@ -153,7 +158,7 @@ Make sure the following commands execute without any error:
### 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.
+Element X is currently supported on Android Marshmallow (API 23+): please test your change on an Android device (or Android emulator) running with API 23. 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.
@@ -166,7 +171,18 @@ For instance, when updating the image `src` of an ImageView, please also conside
### 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.
+When adding or editing `@Composable`, make sure that you create an internal function annotated with `@DayNightPreviews`, with a name suffixed by `Preview`, and having `ElementPreview` as the root composable.
+
+Example:
+```kotlin
+@DayNightPreviews
+@Composable
+internal fun PinIconPreview() = ElementPreview {
+ PinIcon()
+}
+```
+
+This will allow to preview the composable in both light and dark mode in Android Studio. This will also automatically add UI tests. The GitHub action [Record screenshots](https://github.com/vector-im/element-x-android/actions/workflows/recordScreenshots.yml) has to be run to record the new screenshots. The PR reviewer can trigger this for you if you're not part of the core team.
### Authors
diff --git a/README.md b/README.md
index e31acf87b8..f24efcd828 100644
--- a/README.md
+++ b/README.md
@@ -3,14 +3,18 @@
[](https://sonarcloud.io/summary/new_code?id=vector-im_element-x-android)
[](https://sonarcloud.io/summary/new_code?id=vector-im_element-x-android)
[](https://codecov.io/github/vector-im/element-x-android)
-[](https://matrix.to/#/#element-android:matrix.org)
-[](https://translate.element.io/engage/element-android/?utm_source=widget)
+[](https://matrix.to/#/#element-x-android:matrix.org)
+[](https://localazy.com/p/element)
-# element-x-android
+# Element X Android
-ElementX Android is a [Matrix](https://matrix.org/) Android Client provided by [Element](https://element.io/). This app is currently in a pre-alpha release stage with only basic functionality.
+Element X Android is a [Matrix](https://matrix.org/) Android Client provided by [element.io](https://element.io/). This app is currently in a pre-alpha release stage with only basic functionalities.
-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 6+. The UI layer is written using Jetpack compose.
+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 6+. The UI layer is written using [Jetpack Compose](https://developer.android.com/jetpack/compose), and the navigation is managed using [Appyx](https://github.com/bumble-tech/appyx).
+
+Learn more about why we are building Element X in our blog post: [https://element.io/blog/element-x-experience-the-future-of-element/](https://element.io/blog/element-x-experience-the-future-of-element/).
+
+## Table of contents
@@ -28,24 +32,41 @@ The application is a total rewrite of [Element-Android](https://github.com/vecto
Here are some early screenshots of 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.
+Element X 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.
## Status
-This project is in work in progress. The app does not cover yet all functionalities we expect.
+This project is in work in progress. The app does not cover yet all functionalities we expect. The list of supported features can be found in [this issue](https://github.com/vector-im/element-x-android/issues/911).
## Contributing
-Please see our [contribution guide](CONTRIBUTING.md).
+Want to get actively involved in the project? You're more than welcome! A good way to start is to check the issues that are labelled with the [good first issue](https://github.com/vector-im/element-x-android/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) label. Let us know by commenting the issue that you're starting working on it.
-Come chat with the community in the dedicated Matrix [room](https://matrix.to/#/#element-android:matrix.org).
+But first make sure to read our [contribution guide](CONTRIBUTING.md) first.
+
+You can also come chat with the community in the Matrix [room](https://matrix.to/#/#element-x-android:matrix.org) dedicated to the project.
## Build instructions
@@ -54,9 +75,9 @@ Makes sure to select the `app` configuration when building (as we also have samp
## 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.
+When you are experiencing an issue on Element X Android, please first search in [GitHub issues](https://github.com/vector-im/element-x-android/issues)
+and then in [#element-x-android:matrix.org](https://matrix.to/#/#element-x-android:matrix.org).
+If after your research you still have a question, ask at [#element-x-android:matrix.org](https://matrix.to/#/#element-x-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 from the application settings. This is especially recommended when you encounter a crash.
## Copyright & License
diff --git a/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt b/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt
index ec3259fb7c..da8592771c 100644
--- a/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt
+++ b/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt
@@ -24,8 +24,7 @@ import io.element.android.x.di.DaggerAppComponent
import io.element.android.x.info.logApplicationInfo
import io.element.android.x.initializer.CrashInitializer
import io.element.android.x.initializer.EmojiInitializer
-import io.element.android.x.initializer.MatrixInitializer
-import io.element.android.x.initializer.TimberInitializer
+import io.element.android.x.initializer.TracingInitializer
class ElementXApplication : Application(), DaggerComponentOwner {
@@ -39,8 +38,7 @@ class ElementXApplication : Application(), DaggerComponentOwner {
appComponent = DaggerAppComponent.factory().create(applicationContext)
AppInitializer.getInstance(this).apply {
initializeComponent(CrashInitializer::class.java)
- initializeComponent(TimberInitializer::class.java)
- initializeComponent(MatrixInitializer::class.java)
+ initializeComponent(TracingInitializer::class.java)
initializeComponent(EmojiInitializer::class.java)
}
logApplicationInfo()
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 4d75d8601e..5fb3523d6e 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
@@ -17,11 +17,15 @@
package io.element.android.x.di
import com.squareup.anvil.annotations.ContributesTo
+import io.element.android.features.rageshake.api.reporter.BugReporter
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
import io.element.android.libraries.di.AppScope
+import io.element.android.libraries.matrix.api.tracing.TracingService
@ContributesTo(AppScope::class)
interface AppBindings {
fun mainDaggerComponentOwner(): MainDaggerComponentsOwner
fun snackbarDispatcher(): SnackbarDispatcher
+ fun tracingService(): TracingService
+ fun bugReporter(): BugReporter
}
diff --git a/app/src/main/kotlin/io/element/android/x/icon/IconPreview.kt b/app/src/main/kotlin/io/element/android/x/icon/IconPreview.kt
index 49c2cc5782..52e3af1aab 100644
--- a/app/src/main/kotlin/io/element/android/x/icon/IconPreview.kt
+++ b/app/src/main/kotlin/io/element/android/x/icon/IconPreview.kt
@@ -28,7 +28,7 @@ import io.element.android.x.R
@Preview
@Composable
-fun IconPreview(
+internal fun IconPreview(
modifier: Modifier = Modifier,
) {
Box(modifier = modifier) {
@@ -39,7 +39,7 @@ fun IconPreview(
@Preview
@Composable
-fun RoundIconPreview(
+internal fun RoundIconPreview(
modifier: Modifier = Modifier,
) {
Box(modifier = modifier.clip(shape = CircleShape)) {
diff --git a/app/src/main/kotlin/io/element/android/x/initializer/MatrixInitializer.kt b/app/src/main/kotlin/io/element/android/x/initializer/MatrixInitializer.kt
deleted file mode 100644
index 5eebc88756..0000000000
--- a/app/src/main/kotlin/io/element/android/x/initializer/MatrixInitializer.kt
+++ /dev/null
@@ -1,36 +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.x.initializer
-
-import android.content.Context
-import androidx.startup.Initializer
-import io.element.android.libraries.matrix.impl.tracing.setupTracing
-import io.element.android.libraries.matrix.api.tracing.TracingConfigurations
-import io.element.android.x.BuildConfig
-
-class MatrixInitializer : Initializer {
-
- override fun create(context: Context) {
- if (BuildConfig.DEBUG) {
- setupTracing(TracingConfigurations.debug)
- } else {
- setupTracing(TracingConfigurations.release)
- }
- }
-
- override fun dependencies(): List>> = listOf(TimberInitializer::class.java)
-}
diff --git a/app/src/main/kotlin/io/element/android/x/initializer/TracingInitializer.kt b/app/src/main/kotlin/io/element/android/x/initializer/TracingInitializer.kt
new file mode 100644
index 0000000000..068d439994
--- /dev/null
+++ b/app/src/main/kotlin/io/element/android/x/initializer/TracingInitializer.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.x.initializer
+
+import android.content.Context
+import androidx.startup.Initializer
+import io.element.android.libraries.architecture.bindings
+import io.element.android.libraries.matrix.api.tracing.TracingConfiguration
+import io.element.android.libraries.matrix.api.tracing.TracingFilterConfigurations
+import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration
+import io.element.android.x.BuildConfig
+import io.element.android.x.di.AppBindings
+import timber.log.Timber
+
+class TracingInitializer : Initializer {
+
+ override fun create(context: Context) {
+ val appBindings = context.bindings()
+ val tracingService = appBindings.tracingService()
+ val bugReporter = appBindings.bugReporter()
+ Timber.plant(tracingService.createTimberTree())
+ val tracingConfiguration = if (BuildConfig.DEBUG) {
+ TracingConfiguration(
+ filterConfiguration = TracingFilterConfigurations.debug,
+ writesToLogcat = true,
+ writesToFilesConfiguration = WriteToFilesConfiguration.Disabled
+ )
+ } else {
+ TracingConfiguration(
+ filterConfiguration = TracingFilterConfigurations.release,
+ writesToLogcat = false,
+ writesToFilesConfiguration = WriteToFilesConfiguration.Enabled(
+ directory = bugReporter.logDirectory().absolutePath,
+ filenamePrefix = "logs"
+ )
+ )
+ }
+ bugReporter.cleanLogDirectoryIfNeeded()
+ tracingService.setupTracing(tracingConfiguration)
+ }
+
+ override fun dependencies(): List>> = mutableListOf()
+}
diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts
index 6abc3c656b..cffd318fb1 100644
--- a/appnav/build.gradle.kts
+++ b/appnav/build.gradle.kts
@@ -65,6 +65,8 @@ dependencies {
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test)
+ testImplementation(projects.features.networkmonitor.test)
+ testImplementation(projects.tests.testutils)
testImplementation(projects.features.rageshake.test)
testImplementation(projects.features.rageshake.impl)
testImplementation(projects.services.appnavstate.test)
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt
index 64c9ec7c4f..e55f059d14 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt
@@ -58,7 +58,8 @@ class LoggedInEventProcessor @Inject constructor(
.filter { it }
.onEach {
displayMessage(CommonStrings.common_verification_complete)
- }.launchIn(this)
+ }
+ .launchIn(this)
}
}
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
index 4130e5da23..7943151a5e 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
@@ -44,14 +44,14 @@ import io.element.android.appnav.loggedin.LoggedInNode
import io.element.android.appnav.room.RoomFlowNode
import io.element.android.appnav.room.RoomLoadedFlowNode
import io.element.android.features.createroom.api.CreateRoomEntryPoint
+import io.element.android.features.ftue.api.FtueEntryPoint
+import io.element.android.features.ftue.api.state.FtueState
import io.element.android.features.invitelist.api.InviteListEntryPoint
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.preferences.api.PreferencesEntryPoint
import io.element.android.features.roomlist.api.RoomListEntryPoint
import io.element.android.features.verifysession.api.VerifySessionEntryPoint
-import io.element.android.features.ftue.api.FtueEntryPoint
-import io.element.android.features.ftue.api.state.FtueState
import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
@@ -69,10 +69,12 @@ import io.element.android.libraries.matrix.ui.di.MatrixUIBindings
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
import io.element.android.services.appnavstate.api.AppNavigationStateService
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
+import timber.log.Timber
@ContributesNode(AppScope::class)
class LoggedInFlowNode @AssistedInject constructor(
@@ -100,13 +102,13 @@ class LoggedInFlowNode @AssistedInject constructor(
) {
interface Callback : Plugin {
- fun onOpenBugReport() = Unit
+ fun onOpenBugReport()
}
interface LifecycleCallback : NodeLifecycleCallback {
- fun onFlowCreated(identifier: String, client: MatrixClient) = Unit
+ fun onFlowCreated(identifier: String, client: MatrixClient)
- fun onFlowReleased(identifier: String, client: MatrixClient) = Unit
+ fun onFlowReleased(identifier: String, client: MatrixClient)
}
data class Inputs(
@@ -123,7 +125,6 @@ class LoggedInFlowNode @AssistedInject constructor(
override fun onBuilt() {
super.onBuilt()
-
lifecycle.subscribe(
onCreate = {
plugins().forEach { it.onFlowCreated(id, inputs.matrixClient) }
@@ -138,14 +139,12 @@ class LoggedInFlowNode @AssistedInject constructor(
backstack.push(NavTarget.Ftue)
}
},
- onResume = {
- lifecycleScope.launch {
- syncService.startSync()
+ onStop = {
+ //Counterpart startSync is done in observeSyncStateAndNetworkStatus method.
+ coroutineScope.launch {
+ syncService.stopSync()
}
},
- onPause = {
- syncService.stopSync()
- },
onDestroy = {
plugins().forEach { it.onFlowReleased(id, inputs.matrixClient) }
appNavigationStateService.onLeavingSpace(id)
@@ -153,22 +152,23 @@ class LoggedInFlowNode @AssistedInject constructor(
loggedInFlowProcessor.stopObserving()
}
)
-
observeSyncStateAndNetworkStatus()
}
+ @OptIn(FlowPreview::class)
private fun observeSyncStateAndNetworkStatus() {
lifecycleScope.launch {
- repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
combine(
- syncService.syncState,
+ // small debounce to avoid spamming startSync when the state is changing quickly in case of error.
+ syncService.syncState.debounce(100),
networkMonitor.connectivity
) { syncState, networkStatus ->
- syncState == SyncState.Error && networkStatus == NetworkStatus.Online
+ Pair(syncState, networkStatus)
}
- .distinctUntilChanged()
- .collect { restartSync ->
- if (restartSync) {
+ .collect { (syncState, networkStatus) ->
+ Timber.d("Sync state: $syncState, network status: $networkStatus")
+ if (syncState != SyncState.Running && networkStatus == NetworkStatus.Online) {
syncService.startSync()
}
}
@@ -305,7 +305,8 @@ class LoggedInFlowNode @AssistedInject constructor(
override fun onFtueFlowFinished() {
backstack.pop()
}
- }).build()
+ })
+ .build()
}
}
}
@@ -350,3 +351,4 @@ class LoggedInFlowNode @AssistedInject constructor(
backstack.push(NavTarget.InviteList)
}
}
+
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt
index 8910cc3976..6d386a17e5 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt
@@ -21,16 +21,27 @@ import android.os.Build
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import io.element.android.features.networkmonitor.api.NetworkMonitor
+import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.MatrixClient
+import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter
import io.element.android.libraries.push.api.PushService
+import kotlinx.coroutines.delay
import javax.inject.Inject
+private const val DELAY_BEFORE_SHOWING_SYNC_SPINNER_IN_MILLIS = 1500L
+
class LoggedInPresenter @Inject constructor(
private val matrixClient: MatrixClient,
private val permissionsPresenterFactory: PermissionsPresenter.Factory,
+ private val networkMonitor: NetworkMonitor,
private val pushService: PushService,
) : Presenter {
@@ -53,18 +64,25 @@ class LoggedInPresenter @Inject constructor(
pushService.registerWith(matrixClient, pushProvider, distributor)
}
- val syncState = matrixClient.syncService().syncState.collectAsState()
+ val roomListState by matrixClient.roomListService.state.collectAsState()
+ val networkStatus by networkMonitor.connectivity.collectAsState()
val permissionsState = postNotificationPermissionsPresenter.present()
-
- // fun handleEvents(event: LoggedInEvents) {
- // when (event) {
- // }
- // }
-
+ var showSyncSpinner by remember {
+ mutableStateOf(false)
+ }
+ LaunchedEffect(roomListState, networkStatus) {
+ showSyncSpinner = when {
+ networkStatus == NetworkStatus.Offline -> false
+ roomListState == RoomListService.State.Running -> false
+ else -> {
+ delay(DELAY_BEFORE_SHOWING_SYNC_SPINNER_IN_MILLIS)
+ true
+ }
+ }
+ }
return LoggedInState(
- syncState = syncState.value,
+ showSyncSpinner = showSyncSpinner,
permissionsState = permissionsState,
- // eventSink = ::handleEvents
)
}
}
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt
index 075242cddb..bb06952a50 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt
@@ -16,11 +16,9 @@
package io.element.android.appnav.loggedin
-import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.permissions.api.PermissionsState
data class LoggedInState(
- val syncState: SyncState,
+ val showSyncSpinner: Boolean,
val permissionsState: PermissionsState,
- // val eventSink: (LoggedInEvents) -> Unit
)
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt
index e8a8a4762c..3cfb03f123 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt
@@ -17,22 +17,20 @@
package io.element.android.appnav.loggedin
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.permissions.api.createDummyPostNotificationPermissionsState
open class LoggedInStateProvider : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
- aLoggedInState(),
- aLoggedInState(syncState = SyncState.Idle),
+ aLoggedInState(false),
+ aLoggedInState(true),
// Add other state here
)
}
fun aLoggedInState(
- syncState: SyncState = SyncState.Running,
+ showSyncSpinner: Boolean = true,
) = LoggedInState(
- syncState = syncState,
+ showSyncSpinner = showSyncSpinner,
permissionsState = createDummyPostNotificationPermissionsState(),
- // eventSink = {}
)
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt
index 60784ea4ed..0ade93a795 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt
@@ -47,7 +47,7 @@ fun LoggedInView(
modifier = Modifier
.padding(top = 8.dp)
.align(Alignment.TopCenter),
- syncState = state.syncState,
+ isVisible = state.showSyncSpinner,
)
PermissionsView(
state = state.permissionsState,
@@ -58,7 +58,7 @@ fun LoggedInView(
@DayNightPreviews
@Composable
-fun LoggedInViewPreview(@PreviewParameter(LoggedInStateProvider::class) state: LoggedInState) = ElementPreview {
+internal fun LoggedInViewPreview(@PreviewParameter(LoggedInStateProvider::class) state: LoggedInState) = ElementPreview {
LoggedInView(
state = state
)
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/SyncStateView.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/SyncStateView.kt
index 5108bb8716..6d045a431a 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/SyncStateView.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/SyncStateView.kt
@@ -38,19 +38,18 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
-import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun SyncStateView(
- syncState: SyncState,
+ isVisible: Boolean,
modifier: Modifier = Modifier
) {
val animationSpec = spring(stiffness = 500F)
AnimatedVisibility(
modifier = modifier,
- visible = syncState.mustBeVisible(),
+ visible = isVisible,
enter = fadeIn(animationSpec = animationSpec),
exit = fadeOut(animationSpec = animationSpec),
) {
@@ -60,15 +59,15 @@ fun SyncStateView(
) {
Row(
modifier = Modifier
- .background(color = ElementTheme.colors.bgSubtleSecondary)
- .padding(horizontal = 24.dp, vertical = 10.dp),
+ .background(color = ElementTheme.colors.bgSubtleSecondary)
+ .padding(horizontal = 24.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
CircularProgressIndicator(
modifier = Modifier
- .progressSemantics()
- .size(12.dp),
+ .progressSemantics()
+ .size(12.dp),
color = ElementTheme.colors.textPrimary,
strokeWidth = 1.5.dp,
)
@@ -82,20 +81,13 @@ fun SyncStateView(
}
}
-private fun SyncState.mustBeVisible() = when (this) {
- SyncState.Idle -> true /* Cold start of the app */
- SyncState.Running -> false
- SyncState.Error -> false /* In this case, the network error banner can be displayed */
- SyncState.Terminated -> true /* The app is resumed and the sync is started again */
-}
-
@DayNightPreviews
@Composable
-fun SyncStateViewPreview() = ElementPreview {
+internal fun SyncStateViewPreview() = ElementPreview {
// Add a box to see the shadow
Box(modifier = Modifier.padding(24.dp)) {
SyncStateView(
- syncState = SyncState.Idle
+ isVisible = true
)
}
}
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/LoadingRoomNodeView.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/LoadingRoomNodeView.kt
index ae5ae4f8db..558f64424a 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/room/LoadingRoomNodeView.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/room/LoadingRoomNodeView.kt
@@ -16,19 +16,13 @@
package io.element.android.appnav.room
-import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@@ -38,7 +32,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
-import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
+import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
@@ -47,7 +41,6 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre
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.theme.placeholderBackground
import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings
@@ -102,20 +95,7 @@ private fun LoadingRoomTopBar(
BackButton(onClick = onBackClicked)
},
title = {
- Row(
- verticalAlignment = Alignment.CenterVertically
- ) {
- Box(
- modifier = Modifier
- .size(AvatarSize.TimelineRoom.dp)
- .align(Alignment.CenterVertically)
- .background(color = ElementTheme.colors.placeholderBackground, shape = CircleShape)
- )
- Spacer(modifier = Modifier.width(8.dp))
- PlaceholderAtom(width = 20.dp, height = 7.dp)
- Spacer(modifier = Modifier.width(7.dp))
- PlaceholderAtom(width = 45.dp, height = 7.dp)
- }
+ IconTitlePlaceholdersRowMolecule(iconSize = AvatarSize.TimelineRoom.dp)
},
windowInsets = WindowInsets(0.dp),
)
@@ -123,12 +103,12 @@ private fun LoadingRoomTopBar(
@Preview
@Composable
-fun LoadingRoomNodeViewLightPreview(@PreviewParameter(LoadingRoomStateProvider::class) state: LoadingRoomState) =
+internal fun LoadingRoomNodeViewLightPreview(@PreviewParameter(LoadingRoomStateProvider::class) state: LoadingRoomState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
-fun LoadingRoomNodeViewDarkPreview(@PreviewParameter(LoadingRoomStateProvider::class) state: LoadingRoomState) =
+internal fun LoadingRoomNodeViewDarkPreview(@PreviewParameter(LoadingRoomStateProvider::class) state: LoadingRoomState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt
index 20ec9f48b4..661d3c5433 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt
@@ -96,7 +96,8 @@ class RoomFlowNode @AssistedInject constructor(
} else {
backstack.newRoot(NavTarget.Loading)
}
- }.launchIn(lifecycleScope)
+ }
+ .launchIn(lifecycleScope)
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt
index 24ec9795f7..d00c4791f7 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt
@@ -75,8 +75,8 @@ class RoomLoadedFlowNode @AssistedInject constructor(
}
interface LifecycleCallback : NodeLifecycleCallback {
- fun onFlowCreated(identifier: String, room: MatrixRoom) = Unit
- fun onFlowReleased(identifier: String, room: MatrixRoom) = Unit
+ fun onFlowCreated(identifier: String, room: MatrixRoom)
+ fun onFlowReleased(identifier: String, room: MatrixRoom)
}
data class Inputs(
@@ -115,7 +115,8 @@ class RoomLoadedFlowNode @AssistedInject constructor(
room.updateMembers()
.onFailure {
Timber.e(it, "Fail to fetch members for room ${room.roomId}")
- }.onSuccess {
+ }
+ .onSuccess {
Timber.v("Success fetching members for room ${room.roomId}")
}
}
diff --git a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt
index a811d0283d..4abc89e7ee 100644
--- a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt
+++ b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt
@@ -20,13 +20,18 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
+import io.element.android.features.networkmonitor.api.NetworkStatus
+import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.libraries.matrix.api.MatrixClient
+import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.test.FakeMatrixClient
+import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter
import io.element.android.libraries.push.api.PushService
import io.element.android.libraries.pushproviders.api.Distributor
import io.element.android.libraries.pushproviders.api.PushProvider
+import io.element.android.tests.testutils.consumeItemsUntilPredicate
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -42,14 +47,33 @@ class LoggedInPresenterTest {
}
}
- private fun createPresenter(): LoggedInPresenter {
+ @Test
+ fun `present - show sync spinner`() = runTest {
+ val roomListService = FakeRoomListService()
+ val presenter = createPresenter(roomListService, NetworkStatus.Online)
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ assertThat(initialState.showSyncSpinner).isFalse()
+ consumeItemsUntilPredicate { it.showSyncSpinner }
+ roomListService.postState(RoomListService.State.Running)
+ consumeItemsUntilPredicate { !it.showSyncSpinner }
+ }
+ }
+
+ private fun createPresenter(
+ roomListService: RoomListService = FakeRoomListService(),
+ networkStatus: NetworkStatus = NetworkStatus.Offline
+ ): LoggedInPresenter {
return LoggedInPresenter(
- matrixClient = FakeMatrixClient(),
+ matrixClient = FakeMatrixClient(roomListService = roomListService),
permissionsPresenterFactory = object : PermissionsPresenter.Factory {
override fun create(permission: String): PermissionsPresenter {
return NoopPermissionsPresenter()
}
},
+ networkMonitor = FakeNetworkMonitor(networkStatus),
pushService = object : PushService {
override fun notificationStyleChanged() {
}
diff --git a/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingRoomStateFlowFactoryTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingRoomStateFlowFactoryTest.kt
index 17b6f6deb9..f56367e5f8 100644
--- a/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingRoomStateFlowFactoryTest.kt
+++ b/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingRoomStateFlowFactoryTest.kt
@@ -18,12 +18,12 @@ package io.element.android.appnav.room
import app.cash.turbine.test
import com.google.common.truth.Truth
-import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource
+import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
-import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
+import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -47,29 +47,29 @@ class LoadingRoomStateFlowFactoryTest {
@Test
fun `flow should emit Loading and then Loaded when there is a room in cache after SS is loaded`() = runTest {
val room = FakeMatrixRoom(sessionId= A_SESSION_ID, roomId = A_ROOM_ID)
- val roomSummaryDataSource = FakeRoomSummaryDataSource()
- val matrixClient = FakeMatrixClient(A_SESSION_ID, roomSummaryDataSource = roomSummaryDataSource)
+ val roomListService = FakeRoomListService()
+ val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService)
val flowFactory = LoadingRoomStateFlowFactory(matrixClient)
flowFactory
.create(this, A_ROOM_ID)
.test {
Truth.assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading)
matrixClient.givenGetRoomResult(A_ROOM_ID, room)
- roomSummaryDataSource.postLoadingState(RoomSummaryDataSource.LoadingState.Loaded(1))
+ roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
Truth.assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loaded(room))
}
}
@Test
fun `flow should emit Loading and then Error when there is no room in cache after SS is loaded`() = runTest {
- val roomSummaryDataSource = FakeRoomSummaryDataSource()
- val matrixClient = FakeMatrixClient(A_SESSION_ID, roomSummaryDataSource = roomSummaryDataSource)
+ val roomListService = FakeRoomListService()
+ val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService)
val flowFactory = LoadingRoomStateFlowFactory(matrixClient)
flowFactory
.create(this, A_ROOM_ID)
.test {
Truth.assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading)
- roomSummaryDataSource.postLoadingState(RoomSummaryDataSource.LoadingState.Loaded(1))
+ roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
Truth.assertThat(awaitItem()).isEqualTo(LoadingRoomState.Error)
}
}
diff --git a/build.gradle.kts b/build.gradle.kts
index 9272514899..3dfef189e2 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,9 +1,11 @@
+import com.google.devtools.ksp.gradle.KspTask
import kotlinx.kover.api.KoverTaskExtension
+import org.apache.tools.ant.taskdefs.optional.ReplaceRegExp
import org.jetbrains.kotlin.cli.common.toBooleanLenient
buildscript {
dependencies {
- classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.22")
+ classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0")
classpath("com.google.gms:google-services:4.3.15")
}
}
@@ -56,7 +58,7 @@ allprojects {
// activate all available (even unstable) rules.
allRules = true
// point to your custom config defining rules to run, overwriting default behavior
- config = files("$rootDir/tools/detekt/detekt.yml")
+ config.from(files("$rootDir/tools/detekt/detekt.yml"))
}
dependencies {
detektPlugins("io.nlopez.compose.rules:detekt:0.1.12")
@@ -343,3 +345,21 @@ subprojects {
tasks.findByName("recordPaparazziDebug")?.dependsOn(removeOldScreenshotsTask)
tasks.findByName("recordPaparazziRelease")?.dependsOn(removeOldScreenshotsTask)
}
+
+// Workaround for https://github.com/airbnb/Showkase/issues/335
+subprojects {
+ tasks.withType() {
+ doLast {
+ fileTree(buildDir).apply { include("**/*ShowkaseExtension*.kt") }.files.forEach { file ->
+ ReplaceRegExp().apply {
+ setMatch("public fun Showkase.getMetadata")
+ setReplace("@Suppress(\"DEPRECATION\") public fun Showkase.getMetadata")
+ setFlags("g")
+ setByLine(true)
+ setFile(file)
+ execute()
+ }
+ }
+ }
+ }
+}
diff --git a/changelog.d/1064.wip b/changelog.d/1064.wip
new file mode 100644
index 0000000000..f3d8af5133
--- /dev/null
+++ b/changelog.d/1064.wip
@@ -0,0 +1 @@
+[Poll] Add feature flag in developer options
diff --git a/changelog.d/769.feature b/changelog.d/769.feature
new file mode 100644
index 0000000000..8df765c27c
--- /dev/null
+++ b/changelog.d/769.feature
@@ -0,0 +1 @@
+Allow cancelling media upload
diff --git a/docs/_developer_onboarding.md b/docs/_developer_onboarding.md
index 9198137577..8a587b5a08 100644
--- a/docs/_developer_onboarding.md
+++ b/docs/_developer_onboarding.md
@@ -145,7 +145,7 @@ Then you can launch the build script from the matrix-rust-components-kotlin repo
- `-m` Option to select the gradle module to build. Default is sdk.
- `-t` Option to to select an android target to build against. Default will build for all targets.
-So for example to build the sdk against aarch64-linux-android target and copy the generated aar to ElementX project:
+So for example to build the sdk against aarch64-linux-android target and copy the generated aar to Element X project:
```shell
./scripts/build.sh -p [YOUR MATRIX RUST SDK PATH] -t aarch64-linux-android -o [YOUR element-x-android PATH]/libraries/rustsdk/matrix-rust-sdk.aar
@@ -313,7 +313,7 @@ suffix `Presenter`,states MUST have a suffix `State`, etc. Also we want to have
### Push
-**Note** Firebase Push is not yet implemented on the project.
+**Note** Firebase is implemented, but Unified Push is not yet fully implemented on the project, so this is not possible to choose this push provider in the app at the moment.
Please see the dedicated [documentation](notifications.md) for more details.
@@ -342,8 +342,7 @@ We have 3 tests frameworks in place, and this should be sufficient to guarantee
file [TemplateView.kt](../features/template/src/main/kotlin/io/element/android/features/template/TemplateView.kt). We create PreviewProvider to provide
different states. See for instance the
file [TemplateStateProvider.kt](../features/template/src/main/kotlin/io/element/android/features/template/TemplateStateProvider.kt)
- - Tests on presenter with [Molecule](https://github.com/cashapp/molecule) and [Turbine](https://github.com/cashapp/turbine). See in the template the
- class [TemplatePresenterTests](../features/template/src/test/kotlin/io/element/android/features/template/TemplatePresenterTests.kt).
+- Tests on presenter with [Molecule](https://github.com/cashapp/molecule) and [Turbine](https://github.com/cashapp/turbine). See in the template the class [TemplatePresenterTests](../features/template/src/test/kotlin/io/element/android/features/template/TemplatePresenterTests.kt).
**Note** For now we want to avoid using class mocking (with library such as *mockk*), because this should be not necessary. We prefer to create Fake
implementation of our interfaces. Mocking can be used to mock Android framework classes though, such as `Bitmap` for instance.
diff --git a/docs/images-lfs/screen_1_dark.png b/docs/images-lfs/screen_1_dark.png
new file mode 100644
index 0000000000..8bdcd59305
--- /dev/null
+++ b/docs/images-lfs/screen_1_dark.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4515f7589c422197a82672cdc3e64814ccca9a9a022b806facee44dd67d51ff2
+size 1116864
diff --git a/docs/images-lfs/screen_1_light.png b/docs/images-lfs/screen_1_light.png
new file mode 100644
index 0000000000..8eba38af82
--- /dev/null
+++ b/docs/images-lfs/screen_1_light.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2afb667a8b679f4395c28407b014f074e8b74745f6ecdd6ad699a6650cee2bc7
+size 771160
diff --git a/docs/images-lfs/screen_2_dark.png b/docs/images-lfs/screen_2_dark.png
new file mode 100644
index 0000000000..9a0102940f
--- /dev/null
+++ b/docs/images-lfs/screen_2_dark.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:910f9ab58a197a16b1295cd6b65d406ebfff1298c9c128a326edf1ec834d7fa7
+size 332936
diff --git a/docs/images-lfs/screen_2_light.png b/docs/images-lfs/screen_2_light.png
new file mode 100644
index 0000000000..1dd3106e5c
--- /dev/null
+++ b/docs/images-lfs/screen_2_light.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f7b80b9124c5c04c9db3824b68ab35883d41d05918abd415f7565f87d2713cfa
+size 338455
diff --git a/docs/images-lfs/screen_3_dark.png b/docs/images-lfs/screen_3_dark.png
new file mode 100644
index 0000000000..ccc17333e7
--- /dev/null
+++ b/docs/images-lfs/screen_3_dark.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dadff79ae955ab1da5c0a0567960fc567526ad0d8d2ea418d8adacf3a7a400cc
+size 243201
diff --git a/docs/images-lfs/screen_3_light.png b/docs/images-lfs/screen_3_light.png
new file mode 100644
index 0000000000..2116f1dc4c
--- /dev/null
+++ b/docs/images-lfs/screen_3_light.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e2f328b3e8bf2ebe4abc4c130e28f6fc2351f5ac339be0819e5fddb657f4a560
+size 246731
diff --git a/docs/images-lfs/screen_4_dark.png b/docs/images-lfs/screen_4_dark.png
new file mode 100644
index 0000000000..5bd122a9ea
--- /dev/null
+++ b/docs/images-lfs/screen_4_dark.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:519fb54f0070833b2a4671e1f0fc7b2ec60930d69b592b8a760f52a73dc4fe38
+size 132247
diff --git a/docs/images-lfs/screen_4_light.png b/docs/images-lfs/screen_4_light.png
new file mode 100644
index 0000000000..ee82f3be0a
--- /dev/null
+++ b/docs/images-lfs/screen_4_light.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f683a228d7168c75d6f42c353c1a0e169cb22cb8fa6696e9e3ffeb80854b2441
+size 131610
diff --git a/docs/images/screen1.png b/docs/images/screen1.png
deleted file mode 100644
index 9f9d7747ff..0000000000
Binary files a/docs/images/screen1.png and /dev/null differ
diff --git a/docs/images/screen2.png b/docs/images/screen2.png
deleted file mode 100644
index a5733003d6..0000000000
Binary files a/docs/images/screen2.png and /dev/null differ
diff --git a/docs/images/screen3.png b/docs/images/screen3.png
deleted file mode 100644
index 3edb49d086..0000000000
Binary files a/docs/images/screen3.png and /dev/null differ
diff --git a/docs/images/screen4.png b/docs/images/screen4.png
deleted file mode 100644
index 53da801a1b..0000000000
Binary files a/docs/images/screen4.png and /dev/null differ
diff --git a/docs/nightly_build.md b/docs/nightly_build.md
index 9abd59a67b..91ea10b530 100644
--- a/docs/nightly_build.md
+++ b/docs/nightly_build.md
@@ -10,11 +10,11 @@
## Configuration
-The nightly build will contain what's on develop, in release mode, for the main variant. It is signed using a dedicated signature, and has a dedicated appId (`io.element.android.x.nightly`), so it can be installed along with the production version of ElementX Android. The only other difference compared to ElementX Android is a different app name. We do not want to change the app name since it will also affect some strings in the app, and we do want to do that. (TODO today, the app name is changed.)
+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 Element X Android. The only other difference compared to ElementX Android is a different app name. We do not want to change the app name since it will also affect some strings in the app, and we do want to do that. (TODO today, the app name is changed.)
Nightly builds are built and released to Firebase every days, and automatically.
-This is recommended to exclusively use this app, with your main account, instead of ElementX Android, and fallback to ElementX Android just in case of regression, to discover as soon as possible any regression, and report it to the team. To avoid double notification, you may want to disable the notification from the Element Android production version. Just open Element Android, navigate to `Settings/Notifications` and uncheck `Enable notifications for this session` (TODO Not supported yet).
+This is recommended to exclusively use this app, with your main account, instead of Element X 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.
diff --git a/docs/screenshot_testing.md b/docs/screenshot_testing.md
index 37299af7fc..79ecad20dd 100644
--- a/docs/screenshot_testing.md
+++ b/docs/screenshot_testing.md
@@ -13,7 +13,7 @@
## Overview
- Screenshot tests are tests which record the content of a rendered screen and verify subsequent runs to check if the screen renders differently.
-- ElementX uses [Paparazzi](https://github.com/cashapp/paparazzi) to render, record and verify Composable. All Composable Preview will be use to make screenshot test, thanks to the usage of [Showkase](https://github.com/airbnb/Showkase).
+- Element X uses [Paparazzi](https://github.com/cashapp/paparazzi) to render, record and verify Composable. All Composable Preview will be use to make screenshot test, thanks to the usage of [Showkase](https://github.com/airbnb/Showkase).
- The screenshot verification occurs on every pull request as part of the `tests.yml` workflow.
## Setup
@@ -30,6 +30,14 @@ If installed correctly, `git push` and `git pull` will now include LFS content.
## Recording
+Recording of screenshots is done by triggering the GitHub action [Record screenshots](https://github.com/vector-im/element-x-android/actions/workflows/recordScreenshots.yml), to avoid differences of generated binary files (png images) depending on developers' environment.
+
+So basically, you will create a branch, do some commits with your work on it, then push your branch, trigger the GitHub action to record the screenshots (only if you think preview may have changed), and finally create a pull request. The GitHub action will record the screenshots and commit the changes to the branch.
+
+You can still record the screenshots locally, but please do not commit the changes.
+
+To record the screenshot locally, run the following command:
+
```shell
./gradlew recordPaparazziDebug
```
diff --git a/fastlane/metadata/android/en-US/changelogs/40001020.txt b/fastlane/metadata/android/en-US/changelogs/40001020.txt
new file mode 100644
index 0000000000..8aacdd89af
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/40001020.txt
@@ -0,0 +1,2 @@
+First release of Element X 🚀!
+Full changelog: https://github.com/vector-im/element-x-android/releases
diff --git a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt
index f6d77226b9..5aa9287d86 100644
--- a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt
+++ b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt
@@ -81,12 +81,12 @@ fun buildAnnotatedStringWithColoredPart(
@Preview
@Composable
-fun AnalyticsPreferencesViewLightPreview(@PreviewParameter(AnalyticsPreferencesStateProvider::class) state: AnalyticsPreferencesState) =
+internal fun AnalyticsPreferencesViewLightPreview(@PreviewParameter(AnalyticsPreferencesStateProvider::class) state: AnalyticsPreferencesState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
-fun AnalyticsPreferencesViewDarkPreview(@PreviewParameter(AnalyticsPreferencesStateProvider::class) state: AnalyticsPreferencesState) =
+internal fun AnalyticsPreferencesViewDarkPreview(@PreviewParameter(AnalyticsPreferencesStateProvider::class) state: AnalyticsPreferencesState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt
index a27e6e7399..54b9add785 100644
--- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt
+++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt
@@ -54,6 +54,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart
import io.element.android.libraries.designsystem.theme.components.Button
+import io.element.android.libraries.designsystem.theme.components.ButtonSize
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
@@ -188,29 +189,28 @@ private fun AnalyticsOptInFooter(
modifier = modifier,
) {
Button(
+ text = stringResource(id = CommonStrings.action_ok),
onClick = onTermsAccepted,
modifier = Modifier.fillMaxWidth(),
- ) {
- Text(text = stringResource(id = CommonStrings.action_ok))
- }
+ )
TextButton(
+ text = stringResource(id = CommonStrings.action_not_now),
+ size = ButtonSize.Medium,
onClick = onTermsDeclined,
modifier = Modifier.fillMaxWidth(),
- ) {
- Text(text = stringResource(id = CommonStrings.action_not_now))
- }
+ )
}
}
@Preview
@Composable
-fun AnalyticsOptInViewLightPreview(@PreviewParameter(AnalyticsOptInStateProvider::class) state: AnalyticsOptInState) = ElementPreviewLight {
+internal fun AnalyticsOptInViewLightPreview(@PreviewParameter(AnalyticsOptInStateProvider::class) state: AnalyticsOptInState) = ElementPreviewLight {
ContentToPreview(state)
}
@Preview
@Composable
-fun AnalyticsOptInViewDarkPreview(@PreviewParameter(AnalyticsOptInStateProvider::class) state: AnalyticsOptInState) = ElementPreviewDark {
+internal fun AnalyticsOptInViewDarkPreview(@PreviewParameter(AnalyticsOptInStateProvider::class) state: AnalyticsOptInState) = ElementPreviewDark {
ContentToPreview(state)
}
diff --git a/features/analytics/impl/src/main/res/values-de/translations.xml b/features/analytics/impl/src/main/res/values-de/translations.xml
index 979048344f..44890927a4 100644
--- a/features/analytics/impl/src/main/res/values-de/translations.xml
+++ b/features/analytics/impl/src/main/res/values-de/translations.xml
@@ -1,10 +1,10 @@
- "Wir erfassen und analysieren ""keine"" Account-Daten"
+ "Wir werden keine personenbezogenen Daten aufzeichnen oder auswerten"
"Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."
- "Sie können alle unsere Nutzerbedingungen %1$s lesen."
+ "Du kannst alle unsere Nutzerbedingungen %1$s lesen."
"hier"
- "Sie können die Analyse jederzeit in den Einstellungen deaktivieren"
+ "Du kannst dies jederzeit deaktivieren"
"Wir geben ""keine"" Informationen an Dritte weiter"
- "Helfen Sie %1$s zu verbessern"
+ "Hilf uns, %1$s zu verbessern"
diff --git a/features/analytics/impl/src/main/res/values-ru/translations.xml b/features/analytics/impl/src/main/res/values-ru/translations.xml
new file mode 100644
index 0000000000..805cba6bf2
--- /dev/null
+++ b/features/analytics/impl/src/main/res/values-ru/translations.xml
@@ -0,0 +1,10 @@
+
+
+ "Мы не будем записывать или профилировать какие-либо персональные данные"
+ "Предоставлять анонимные данные об использовании, чтобы помочь нам выявить проблемы."
+ "Вы можете ознакомиться со всеми нашими условиями %1$s."
+ "здесь"
+ "Вы можете отключить эту функцию в любое время"
+ "Мы не будем передавать ваши данные третьим лицам"
+ "Помогите улучшить %1$s"
+
diff --git a/features/analytics/impl/src/main/res/values-zh-rTW/translations.xml b/features/analytics/impl/src/main/res/values-zh-rTW/translations.xml
new file mode 100644
index 0000000000..3259b10fbd
--- /dev/null
+++ b/features/analytics/impl/src/main/res/values-zh-rTW/translations.xml
@@ -0,0 +1,4 @@
+
+
+ "您可以在任何時候關閉它"
+
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt
index da1f43391b..e5f701036e 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt
@@ -28,7 +28,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
-import androidx.compose.ui.unit.dp
import io.element.android.features.createroom.impl.R
import io.element.android.features.createroom.impl.components.UserListView
import io.element.android.features.createroom.impl.userlist.UserListEvents
@@ -36,7 +35,6 @@ import io.element.android.features.createroom.impl.userlist.UserListState
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
-import io.element.android.libraries.designsystem.theme.aliasButtonText
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
@@ -103,16 +101,11 @@ fun AddPeopleViewTopBar(
},
navigationIcon = { BackButton(onClick = onBackPressed) },
actions = {
+ val textActionResId = if (hasSelectedUsers) CommonStrings.action_next else CommonStrings.action_skip
TextButton(
- modifier = Modifier.padding(horizontal = 8.dp),
+ text = stringResource(id = textActionResId),
onClick = onNextPressed,
- ) {
- val textActionResId = if (hasSelectedUsers) CommonStrings.action_next else CommonStrings.action_skip
- Text(
- text = stringResource(id = textActionResId),
- style = ElementTheme.typography.aliasButtonText,
- )
- }
+ )
}
)
}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/RoomPrivacyOption.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/RoomPrivacyOption.kt
index 1d900cea8c..302f72d514 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/RoomPrivacyOption.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/RoomPrivacyOption.kt
@@ -93,11 +93,11 @@ fun RoomPrivacyOption(
@Preview
@Composable
-fun RoomPrivacyOptionLightPreview() = ElementPreviewLight { ContentToPreview() }
+internal fun RoomPrivacyOptionLightPreview() = ElementPreviewLight { ContentToPreview() }
@Preview
@Composable
-fun RoomPrivacyOptionDarkPreview() = ElementPreviewDark { ContentToPreview() }
+internal fun RoomPrivacyOptionDarkPreview() = ElementPreviewDark { ContentToPreview() }
@Composable
private fun ContentToPreview() {
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchMultipleUsersResultItem.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchMultipleUsersResultItem.kt
index 7b726e08a2..be6a7e0a79 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchMultipleUsersResultItem.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchMultipleUsersResultItem.kt
@@ -22,7 +22,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
-import io.element.android.libraries.designsystem.theme.components.Divider
+import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.matrix.ui.components.CheckableMatrixUserRow
import io.element.android.libraries.matrix.ui.components.CheckableUnresolvedUserRow
import io.element.android.libraries.matrix.ui.components.aMatrixUser
@@ -63,11 +63,11 @@ internal fun SearchMultipleUsersResultItemPreview() = ElementThemedPreview { Con
private fun ContentToPreview() {
Column {
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false), isUserSelected = false)
- Divider()
+ HorizontalDivider()
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false), isUserSelected = true)
- Divider()
+ HorizontalDivider()
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true), isUserSelected = false)
- Divider()
+ HorizontalDivider()
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true), isUserSelected = true)
}
}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchSingleUserResultItem.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchSingleUserResultItem.kt
index 72ebee9615..69b528f61c 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchSingleUserResultItem.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchSingleUserResultItem.kt
@@ -23,7 +23,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
-import io.element.android.libraries.designsystem.theme.components.Divider
+import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
import io.element.android.libraries.matrix.ui.components.UnresolvedUserRow
import io.element.android.libraries.matrix.ui.components.aMatrixUser
@@ -59,7 +59,7 @@ internal fun SearchSingleUserResultItemPreview() = ElementThemedPreview { Conten
private fun ContentToPreview() {
Column {
SearchSingleUserResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false))
- Divider()
+ HorizontalDivider()
SearchSingleUserResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true))
}
}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchUserBar.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchUserBar.kt
index fdcd8900b4..be2ba4ddb6 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchUserBar.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchUserBar.kt
@@ -35,7 +35,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
-import io.element.android.libraries.designsystem.theme.components.Divider
+import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.designsystem.theme.components.SearchBar
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.user.MatrixUser
@@ -117,7 +117,7 @@ fun SearchUserBar(
}
)
if (index < users.lastIndex) {
- Divider()
+ HorizontalDivider()
}
}
} else {
@@ -128,7 +128,7 @@ fun SearchUserBar(
onClick = { onUserSelected(searchResult.matrixUser) }
)
if (index < users.lastIndex) {
- Divider()
+ HorizontalDivider()
}
}
}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt
index 9ac8f0fbde..d1f29559bd 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt
@@ -29,6 +29,7 @@ import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectableGroup
+import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetValue
@@ -43,6 +44,7 @@ import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
@@ -55,7 +57,6 @@ import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
-import io.element.android.libraries.designsystem.theme.aliasButtonText
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
@@ -193,15 +194,10 @@ fun ConfigureRoomToolbar(
navigationIcon = { BackButton(onClick = onBackPressed) },
actions = {
TextButton(
- modifier = Modifier.padding(horizontal = 8.dp),
+ text = stringResource(CommonStrings.action_create),
enabled = isNextActionEnabled,
onClick = onNextPressed,
- ) {
- Text(
- text = stringResource(CommonStrings.action_create),
- style = ElementTheme.typography.aliasButtonText,
- )
- }
+ )
}
)
}
@@ -247,6 +243,9 @@ fun RoomTopic(
placeholder = stringResource(CommonStrings.common_topic_placeholder),
onValueChange = onTopicChanged,
maxLines = 3,
+ keyboardOptions = KeyboardOptions(
+ capitalization = KeyboardCapitalization.Sentences,
+ ),
)
}
@@ -277,12 +276,12 @@ private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier =
@Preview
@Composable
-fun ConfigureRoomViewLightPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) =
+internal fun ConfigureRoomViewLightPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
-fun ConfigureRoomViewDarkPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) =
+internal fun ConfigureRoomViewDarkPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt
index d1484b7a4f..7a3bf6ef03 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt
@@ -41,7 +41,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider"Inviter des amis sur Element"
"Inviter des personnes"
"Une erreur s\'est produite lors de la création du salon"
- "Les messages dans ce salon sont chiffrés. Une fopis activé, le chiffrement ne peut pas être désactivé."
+ "Les messages dans ce salon sont chiffrés. Une fois activé, le chiffrement ne peut pas être désactivé."
"Salon privé (sur invitation uniquement)"
"Les messages ne sont pas chiffrés et n\'importe qui peut les lire. Vous pouvez activer le chiffrement ultérieurement."
"Salon public (n’importe qui)"
diff --git a/features/createroom/impl/src/main/res/values-ru/translations.xml b/features/createroom/impl/src/main/res/values-ru/translations.xml
new file mode 100644
index 0000000000..7837f9e7de
--- /dev/null
+++ b/features/createroom/impl/src/main/res/values-ru/translations.xml
@@ -0,0 +1,15 @@
+
+
+ "Новая комната"
+ "Пригласите друзей в Element"
+ "Пригласить людей"
+ "Произошла ошибка при создании комнаты"
+ "Сообщения в этой комнате зашифрованы. Отключить шифрование впоследствии невозможно."
+ "Приватная комната (только по приглашению)"
+ "Сообщения не зашифрованы, и каждый может их прочитать. Вы можете включить шифрование позже."
+ "Публичная комната (любой)"
+ "Название комнаты"
+ "Тема (необязательно)"
+ "Произошла ошибка при попытке открытия комнаты"
+ "Создать комнату"
+
diff --git a/features/createroom/impl/src/main/res/values-zh-rTW/translations.xml b/features/createroom/impl/src/main/res/values-zh-rTW/translations.xml
new file mode 100644
index 0000000000..dd8afaf2e7
--- /dev/null
+++ b/features/createroom/impl/src/main/res/values-zh-rTW/translations.xml
@@ -0,0 +1,7 @@
+
+
+ "邀請朋友使用 Element"
+ "聊天室名稱"
+ "主題(非必填)"
+ "建立聊天室"
+
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/WelcomeView.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/WelcomeView.kt
index 7397e5ecc5..eebef5b3f2 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/WelcomeView.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/WelcomeView.kt
@@ -97,9 +97,11 @@ fun WelcomeView(
}
},
footer = {
- Button(modifier = Modifier.fillMaxWidth(), onClick = onContinueClicked) {
- Text(text = stringResource(CommonStrings.action_continue))
- }
+ Button(
+ text = stringResource(CommonStrings.action_continue),
+ modifier = Modifier.fillMaxWidth(),
+ onClick = onContinueClicked
+ )
Spacer(modifier = Modifier.height(32.dp))
}
)
diff --git a/features/ftue/impl/src/main/res/values-ru/translations.xml b/features/ftue/impl/src/main/res/values-ru/translations.xml
new file mode 100644
index 0000000000..db4fcd21fc
--- /dev/null
+++ b/features/ftue/impl/src/main/res/values-ru/translations.xml
@@ -0,0 +1,9 @@
+
+
+ "Звонки, опросы, поиск и многое другое будут добавлены позже в этом году."
+ "История сообщений для зашифрованных комнат в этом обновлении будет недоступна."
+ "Мы будем рады услышать ваше мнение, сообщите нам об этом через страницу настроек."
+ "Поехали!"
+ "Вот что вам необходимо знать:"
+ "Добро пожаловать в %1$s!"
+
diff --git a/features/ftue/impl/src/main/res/values-sk/translations.xml b/features/ftue/impl/src/main/res/values-sk/translations.xml
index 1c22039a26..2438518334 100644
--- a/features/ftue/impl/src/main/res/values-sk/translations.xml
+++ b/features/ftue/impl/src/main/res/values-sk/translations.xml
@@ -1,6 +1,6 @@
- "Hovory, zdieľanie polohy, vyhľadávanie a ďalšie funkcie pribudnú neskôr v tomto roku."
+ "Hovory, ankety, vyhľadávanie a ďalšie funkcie pribudnú neskôr v tomto roku."
"História správ pre zašifrované miestnosti nebude v tejto aktualizácii k dispozícii."
"Radi by sme od vás počuli, dajte nám vedieť, čo si myslíte, prostredníctvom stránky nastavení."
"Poďme na to!"
diff --git a/features/ftue/impl/src/main/res/values-zh-rTW/translations.xml b/features/ftue/impl/src/main/res/values-zh-rTW/translations.xml
new file mode 100644
index 0000000000..6c5d482cb8
--- /dev/null
+++ b/features/ftue/impl/src/main/res/values-zh-rTW/translations.xml
@@ -0,0 +1,4 @@
+
+
+ "開始吧!"
+
diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt
index 21a57b48a7..8c43b815ae 100644
--- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt
+++ b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt
@@ -36,7 +36,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
-import io.element.android.libraries.matrix.api.room.RoomSummary
+import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.api.extensions.toAnalyticsJoinedRoom
@@ -56,8 +56,9 @@ class InviteListPresenter @Inject constructor(
@Composable
override fun present(): InviteListState {
val invites by client
- .roomSummaryDataSource
- .inviteRooms()
+ .roomListService
+ .invites()
+ .summaries
.collectAsState()
var seenInvites by remember { mutableStateOf>(emptySet()) }
@@ -152,8 +153,7 @@ class InviteListPresenter @Inject constructor(
client.getRoom(roomId)?.use {
it.leave().getOrThrow()
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId)
- }
- Unit
+ }.let { }
}.runCatchingUpdatingState(declinedAction)
}
diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListView.kt b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListView.kt
index e2e5927a66..9e64e5bc9f 100644
--- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListView.kt
+++ b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListView.kt
@@ -43,7 +43,7 @@ import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
-import io.element.android.libraries.designsystem.theme.components.Divider
+import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
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
@@ -86,7 +86,6 @@ fun InviteListView(
title = stringResource(titleResource),
submitText = stringResource(CommonStrings.action_decline),
cancelText = stringResource(CommonStrings.action_cancel),
- emphasizeSubmitButton = true,
onSubmitClicked = { state.eventSink(InviteListEvents.ConfirmDeclineInvite) },
onDismiss = { state.eventSink(InviteListEvents.CancelDeclineInvite) }
)
@@ -162,7 +161,7 @@ fun InviteListContent(
)
if (index != state.inviteList.lastIndex) {
- Divider()
+ HorizontalDivider()
}
}
}
diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/components/InviteSummaryRow.kt b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/components/InviteSummaryRow.kt
index c2bbd5b023..c677fe158f 100644
--- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/components/InviteSummaryRow.kt
+++ b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/components/InviteSummaryRow.kt
@@ -20,7 +20,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
-import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@@ -49,8 +48,8 @@ import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAto
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
-import io.element.android.libraries.designsystem.theme.aliasButtonText
import io.element.android.libraries.designsystem.theme.components.Button
+import io.element.android.libraries.designsystem.theme.components.ButtonSize
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.theme.ElementTheme
@@ -133,23 +132,19 @@ internal fun DefaultInviteSummaryRow(
// CTAs
Row(Modifier.padding(top = 12.dp)) {
OutlinedButton(
- content = { Text(stringResource(CommonStrings.action_decline), style = ElementTheme.typography.aliasButtonText) },
+ text = stringResource(CommonStrings.action_decline),
onClick = onDeclineClicked,
- modifier = Modifier
- .weight(1f)
- .heightIn(max = 36.dp),
- contentPadding = PaddingValues(horizontal = 24.dp, vertical = 0.dp),
+ modifier = Modifier.weight(1f),
+ size = ButtonSize.Medium,
)
Spacer(modifier = Modifier.width(12.dp))
Button(
- content = { Text(stringResource(CommonStrings.action_accept), style = ElementTheme.typography.aliasButtonText) },
+ text = stringResource(CommonStrings.action_accept),
onClick = onAcceptClicked,
- modifier = Modifier
- .weight(1f)
- .heightIn(max = 36.dp),
- contentPadding = PaddingValues(horizontal = 24.dp, vertical = 0.dp),
+ modifier = Modifier.weight(1f),
+ size = ButtonSize.Medium,
)
}
}
diff --git a/features/invitelist/impl/src/main/res/values-de/translations.xml b/features/invitelist/impl/src/main/res/values-de/translations.xml
index 1e2fcc2e86..2cec59d6a0 100644
--- a/features/invitelist/impl/src/main/res/values-de/translations.xml
+++ b/features/invitelist/impl/src/main/res/values-de/translations.xml
@@ -1,8 +1,8 @@
- "Möchten Sie den Beitritt zu %1$s wirklich ablehnen?"
+ "Möchtest du den Beitritt zu %1$s wirklich ablehnen?"
"Einladung ablehnen"
- "Möchten Sie den Chat mit %1$s wirklich ablehnen?"
+ "Möchtest du den privaten Chat mit %1$s wirklich ablehnen?"
"Chat ablehnen"
"Keine Einladungen"
"%1$s (%2$s) hat dich eingeladen"
diff --git a/features/invitelist/impl/src/main/res/values-ru/translations.xml b/features/invitelist/impl/src/main/res/values-ru/translations.xml
new file mode 100644
index 0000000000..0f1c30cb09
--- /dev/null
+++ b/features/invitelist/impl/src/main/res/values-ru/translations.xml
@@ -0,0 +1,9 @@
+
+
+ "Вы уверены, что хотите отклонить приглашение в %1$s?"
+ "Отклонить приглашение"
+ "Вы уверены, что хотите отказаться от приватного общения с %1$s?"
+ "Отклонить чат"
+ "Нет приглашений"
+ "%1$s (%2$s) пригласил вас"
+
diff --git a/features/invitelist/impl/src/test/kotlin/io/element/android/features/invitelist/impl/InviteListPresenterTests.kt b/features/invitelist/impl/src/test/kotlin/io/element/android/features/invitelist/impl/InviteListPresenterTests.kt
index 03e0f46de8..f3eef2784c 100644
--- a/features/invitelist/impl/src/test/kotlin/io/element/android/features/invitelist/impl/InviteListPresenterTests.kt
+++ b/features/invitelist/impl/src/test/kotlin/io/element/android/features/invitelist/impl/InviteListPresenterTests.kt
@@ -30,8 +30,8 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState
-import io.element.android.libraries.matrix.api.room.RoomSummary
-import io.element.android.libraries.matrix.api.room.RoomSummaryDetails
+import io.element.android.libraries.matrix.api.roomlist.RoomSummary
+import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
@@ -40,7 +40,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
-import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
+import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager
import io.element.android.services.analytics.api.AnalyticsService
@@ -51,9 +51,9 @@ class InviteListPresenterTests {
@Test
fun `present - starts empty, adds invites when received`() = runTest {
- val roomSummaryDataSource = FakeRoomSummaryDataSource()
+ val roomListService = FakeRoomListService()
val presenter = createPresenter(
- FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
+ FakeMatrixClient(roomListService = roomListService)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -61,7 +61,7 @@ class InviteListPresenterTests {
val initialState = awaitItem()
Truth.assertThat(initialState.inviteList).isEmpty()
- roomSummaryDataSource.postInviteRooms(listOf(aRoomSummary()))
+ roomListService.postInviteRooms(listOf(aRoomSummary()))
val withInviteState = awaitItem()
Truth.assertThat(withInviteState.inviteList.size).isEqualTo(1)
@@ -72,9 +72,9 @@ class InviteListPresenterTests {
@Test
fun `present - uses user ID and avatar for direct invites`() = runTest {
- val roomSummaryDataSource = FakeRoomSummaryDataSource().withDirectChatInvitation()
+ val roomListService = FakeRoomListService().withDirectChatInvitation()
val presenter = createPresenter(
- FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
+ FakeMatrixClient(roomListService = roomListService)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -98,9 +98,9 @@ class InviteListPresenterTests {
@Test
fun `present - includes sender details for room invites`() = runTest {
- val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
+ val roomListService = FakeRoomListService().withRoomInvitation()
val presenter = createPresenter(
- FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
+ FakeMatrixClient(roomListService = roomListService)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -122,10 +122,10 @@ class InviteListPresenterTests {
@Test
fun `present - shows confirm dialog for declining direct chat invites`() = runTest {
- val roomSummaryDataSource = FakeRoomSummaryDataSource().withDirectChatInvitation()
+ val roomListService = FakeRoomListService().withDirectChatInvitation()
val presenter = InviteListPresenter(
FakeMatrixClient(
- roomSummaryDataSource = roomSummaryDataSource,
+ roomListService = roomListService,
),
FakeSeenInvitesStore(),
FakeAnalyticsService(),
@@ -148,9 +148,9 @@ class InviteListPresenterTests {
@Test
fun `present - shows confirm dialog for declining room invites`() = runTest {
- val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
+ val roomListService = FakeRoomListService().withRoomInvitation()
val presenter = createPresenter(
- FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
+ FakeMatrixClient(roomListService = roomListService)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -169,9 +169,9 @@ class InviteListPresenterTests {
@Test
fun `present - hides confirm dialog when cancelling`() = runTest {
- val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
+ val roomListService = FakeRoomListService().withRoomInvitation()
val presenter = createPresenter(
- FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
+ FakeMatrixClient(roomListService = roomListService)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -190,10 +190,10 @@ class InviteListPresenterTests {
@Test
fun `present - declines invite after confirming`() = runTest {
- val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
+ val roomListService = FakeRoomListService().withRoomInvitation()
val fakeNotificationDrawerManager = FakeNotificationDrawerManager()
val client = FakeMatrixClient(
- roomSummaryDataSource = roomSummaryDataSource,
+ roomListService = roomListService,
)
val room = FakeMatrixRoom()
val presenter = createPresenter(client = client, notificationDrawerManager = fakeNotificationDrawerManager)
@@ -217,9 +217,9 @@ class InviteListPresenterTests {
@Test
fun `present - declines invite after confirming and sets state on error`() = runTest {
- val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
+ val roomListService = FakeRoomListService().withRoomInvitation()
val client = FakeMatrixClient(
- roomSummaryDataSource = roomSummaryDataSource,
+ roomListService = roomListService,
)
val room = FakeMatrixRoom()
val presenter = createPresenter(client)
@@ -247,9 +247,9 @@ class InviteListPresenterTests {
@Test
fun `present - dismisses declining error state`() = runTest {
- val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
+ val roomListService = FakeRoomListService().withRoomInvitation()
val client = FakeMatrixClient(
- roomSummaryDataSource = roomSummaryDataSource,
+ roomListService = roomListService,
)
val room = FakeMatrixRoom()
val presenter = createPresenter(client)
@@ -279,10 +279,10 @@ class InviteListPresenterTests {
@Test
fun `present - accepts invites and sets state on success`() = runTest {
- val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
+ val roomListService = FakeRoomListService().withRoomInvitation()
val fakeNotificationDrawerManager = FakeNotificationDrawerManager()
val client = FakeMatrixClient(
- roomSummaryDataSource = roomSummaryDataSource,
+ roomListService = roomListService,
)
val room = FakeMatrixRoom()
val presenter = createPresenter(client = client, notificationDrawerManager = fakeNotificationDrawerManager)
@@ -303,9 +303,9 @@ class InviteListPresenterTests {
@Test
fun `present - accepts invites and sets state on error`() = runTest {
- val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
+ val roomListService = FakeRoomListService().withRoomInvitation()
val client = FakeMatrixClient(
- roomSummaryDataSource = roomSummaryDataSource,
+ roomListService = roomListService,
)
val room = FakeMatrixRoom()
val presenter = createPresenter(client)
@@ -325,9 +325,9 @@ class InviteListPresenterTests {
@Test
fun `present - dismisses accepting error state`() = runTest {
- val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
+ val roomListService = FakeRoomListService().withRoomInvitation()
val client = FakeMatrixClient(
- roomSummaryDataSource = roomSummaryDataSource,
+ roomListService = roomListService,
)
val room = FakeMatrixRoom()
val presenter = createPresenter(client)
@@ -352,11 +352,11 @@ class InviteListPresenterTests {
@Test
fun `present - stores seen invites when received`() = runTest {
- val roomSummaryDataSource = FakeRoomSummaryDataSource()
+ val roomListService = FakeRoomListService()
val store = FakeSeenInvitesStore()
val presenter = InviteListPresenter(
FakeMatrixClient(
- roomSummaryDataSource = roomSummaryDataSource,
+ roomListService = roomListService,
),
store,
FakeAnalyticsService(),
@@ -368,19 +368,19 @@ class InviteListPresenterTests {
awaitItem()
// When one invite is received, that ID is saved
- roomSummaryDataSource.postInviteRooms(listOf(aRoomSummary()))
+ roomListService.postInviteRooms(listOf(aRoomSummary()))
awaitItem()
Truth.assertThat(store.getProvidedRoomIds()).isEqualTo(setOf(A_ROOM_ID))
// When a second is added, both are saved
- roomSummaryDataSource.postInviteRooms(listOf(aRoomSummary(), aRoomSummary(A_ROOM_ID_2)))
+ roomListService.postInviteRooms(listOf(aRoomSummary(), aRoomSummary(A_ROOM_ID_2)))
awaitItem()
Truth.assertThat(store.getProvidedRoomIds()).isEqualTo(setOf(A_ROOM_ID, A_ROOM_ID_2))
// When they're both dismissed, an empty set is saved
- roomSummaryDataSource.postInviteRooms(listOf())
+ roomListService.postInviteRooms(listOf())
awaitItem()
Truth.assertThat(store.getProvidedRoomIds()).isEmpty()
@@ -389,12 +389,12 @@ class InviteListPresenterTests {
@Test
fun `present - marks invite as new if they're unseen`() = runTest {
- val roomSummaryDataSource = FakeRoomSummaryDataSource()
+ val roomListService = FakeRoomListService()
val store = FakeSeenInvitesStore()
store.publishRoomIds(setOf(A_ROOM_ID))
val presenter = InviteListPresenter(
FakeMatrixClient(
- roomSummaryDataSource = roomSummaryDataSource,
+ roomListService = roomListService,
),
store,
FakeAnalyticsService(),
@@ -405,7 +405,7 @@ class InviteListPresenterTests {
}.test {
awaitItem()
- roomSummaryDataSource.postInviteRooms(listOf(aRoomSummary(), aRoomSummary(A_ROOM_ID_2)))
+ roomListService.postInviteRooms(listOf(aRoomSummary(), aRoomSummary(A_ROOM_ID_2)))
skipItems(1)
val withInviteState = awaitItem()
@@ -417,7 +417,7 @@ class InviteListPresenterTests {
}
}
- private suspend fun FakeRoomSummaryDataSource.withRoomInvitation(): FakeRoomSummaryDataSource {
+ private suspend fun FakeRoomListService.withRoomInvitation(): FakeRoomListService {
postInviteRooms(
listOf(
RoomSummary.Filled(
@@ -446,7 +446,7 @@ class InviteListPresenterTests {
return this
}
- private suspend fun FakeRoomSummaryDataSource.withDirectChatInvitation(): FakeRoomSummaryDataSource {
+ private suspend fun FakeRoomListService.withDirectChatInvitation(): FakeRoomListService {
postInviteRooms(
listOf(
RoomSummary.Filled(
diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt
index 39b3e8a9a1..50d400092a 100644
--- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt
+++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt
@@ -120,7 +120,7 @@ fun StaticMapView(
@DayNightPreviews
@Composable
-fun StaticMapViewPreview() = ElementPreview {
+internal fun StaticMapViewPreview() = ElementPreview {
StaticMapView(
lat = 0.0,
lon = 0.0,
diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapPlaceholder.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapPlaceholder.kt
index d36ead5b28..84349d97c9 100644
--- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapPlaceholder.kt
+++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapPlaceholder.kt
@@ -79,7 +79,7 @@ internal fun StaticMapPlaceholder(
@DayNightPreviews
@Composable
-fun StaticMapPlaceholderPreview(
+internal fun StaticMapPlaceholderPreview(
@PreviewParameter(BooleanParameterProvider::class) values: Boolean
) = ElementPreview {
StaticMapPlaceholder(
diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationView.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationView.kt
index cdaca94b04..cfb30a5523 100644
--- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationView.kt
+++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationView.kt
@@ -224,7 +224,7 @@ fun SendLocationView(
@DayNightPreviews
@Composable
-fun SendLocationViewPreview(
+internal fun SendLocationViewPreview(
@PreviewParameter(SendLocationStateProvider::class) state: SendLocationState
) = ElementPreview {
SendLocationView(
diff --git a/features/login/impl/build.gradle.kts b/features/login/impl/build.gradle.kts
index 4a4c6756aa..91b8a2f543 100644
--- a/features/login/impl/build.gradle.kts
+++ b/features/login/impl/build.gradle.kts
@@ -19,7 +19,7 @@ plugins {
alias(libs.plugins.anvil)
alias(libs.plugins.ksp)
id("kotlin-parcelize")
- kotlin("plugin.serialization") version "1.8.22"
+ kotlin("plugin.serialization") version "1.9.0"
}
android {
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt
index 378859225e..85332ec8b3 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt
@@ -38,7 +38,7 @@ import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtomSize
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
-import io.element.android.libraries.designsystem.theme.components.Divider
+import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.theme.ElementTheme
@@ -55,7 +55,7 @@ fun AccountProviderView(
Column(modifier = modifier
.fillMaxWidth()
.clickable { onClick() }) {
- Divider()
+ HorizontalDivider()
Column(
modifier = Modifier
.fillMaxWidth()
@@ -114,12 +114,12 @@ fun AccountProviderView(
@Preview
@Composable
-fun AccountProviderViewLightPreview(@PreviewParameter(AccountProviderProvider::class) item: AccountProvider) =
+internal fun AccountProviderViewLightPreview(@PreviewParameter(AccountProviderProvider::class) item: AccountProvider) =
ElementPreviewLight { ContentToPreview(item) }
@Preview
@Composable
-fun AccountProviderViewDarkPreview(@PreviewParameter(AccountProviderProvider::class) item: AccountProvider) =
+internal fun AccountProviderViewDarkPreview(@PreviewParameter(AccountProviderProvider::class) item: AccountProvider) =
ElementPreviewDark { ContentToPreview(item) }
@Composable
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt
index f9e9624503..ffa337fba7 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt
@@ -71,12 +71,12 @@ fun ChangeServerView(
@Preview
@Composable
-fun ChangeServerViewLightPreview(@PreviewParameter(ChangeServerStateProvider::class) state: ChangeServerState) =
+internal fun ChangeServerViewLightPreview(@PreviewParameter(ChangeServerStateProvider::class) state: ChangeServerState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
-fun ChangeServerViewDarkPreview(@PreviewParameter(ChangeServerStateProvider::class) state: ChangeServerState) =
+internal fun ChangeServerViewDarkPreview(@PreviewParameter(ChangeServerStateProvider::class) state: ChangeServerState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/dialogs/SlidingSyncNotSupportedDialog.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/dialogs/SlidingSyncNotSupportedDialog.kt
index 5beb14b0b4..6ef5be06ab 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/dialogs/SlidingSyncNotSupportedDialog.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/dialogs/SlidingSyncNotSupportedDialog.kt
@@ -35,7 +35,6 @@ internal fun SlidingSyncNotSupportedDialog(
submitText = stringResource(CommonStrings.action_learn_more),
onSubmitClicked = onLearnMoreClicked,
onCancelClicked = onDismiss,
- emphasizeSubmitButton = true,
title = stringResource(CommonStrings.dialog_title_error),
content = stringResource(R.string.screen_change_server_error_no_sliding_sync_message),
)
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/CustomTabHandler.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/CustomTabHandler.kt
index 48c674e0a0..b83c0eb1ac 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/CustomTabHandler.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/CustomTabHandler.kt
@@ -41,8 +41,7 @@ class CustomTabHandler @Inject constructor(
if (packageName != null) {
customTabsServiceConnection = object : CustomTabsServiceConnection() {
override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) {
- customTabsClient = client
- .also { it.warmup(0L) }
+ customTabsClient = client.apply { warmup(0L) }
prefetchUrl(url)
}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcView.kt
index c1235b76c5..1b7486e814 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcView.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcView.kt
@@ -100,12 +100,12 @@ fun OidcView(
@Preview
@Composable
-fun OidcViewLightPreview(@PreviewParameter(OidcStateProvider::class) state: OidcState) =
+internal fun OidcViewLightPreview(@PreviewParameter(OidcStateProvider::class) state: OidcState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
-fun OidcViewDarkPreview(@PreviewParameter(OidcStateProvider::class) state: OidcState) =
+internal fun OidcViewDarkPreview(@PreviewParameter(OidcStateProvider::class) state: OidcState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderView.kt
index 0f444350c9..0e35f9eda5 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderView.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderView.kt
@@ -127,12 +127,12 @@ fun ChangeAccountProviderView(
@Preview
@Composable
-fun ChangeAccountProviderViewLightPreview(@PreviewParameter(ChangeAccountProviderStateProvider::class) state: ChangeAccountProviderState) =
+internal fun ChangeAccountProviderViewLightPreview(@PreviewParameter(ChangeAccountProviderStateProvider::class) state: ChangeAccountProviderState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
-fun ChangeAccountProviderViewDarkPreview(@PreviewParameter(ChangeAccountProviderStateProvider::class) state: ChangeAccountProviderState) =
+internal fun ChangeAccountProviderViewDarkPreview(@PreviewParameter(ChangeAccountProviderStateProvider::class) state: ChangeAccountProviderState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt
index 123d3013c7..1a021ad605 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt
@@ -92,7 +92,7 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
} else if (matrixHomeServerDetails.supportsPasswordLogin) {
LoginFlow.PasswordLogin
} else {
- throw IllegalStateException("Unsupported login flow")
+ error("Unsupported login flow")
}
}.getOrThrow()
}.runCatchingUpdatingState(loginFlowAction, errorTransform = ChangeServerError::from)
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt
index 2bc002b3fc..239ee3c6ac 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt
@@ -36,11 +36,10 @@ import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
-import io.element.android.libraries.designsystem.components.button.ButtonWithProgress
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
-import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.matrix.api.auth.OidcDetails
import io.element.android.libraries.testtags.TestTags
@@ -87,7 +86,7 @@ fun ConfirmAccountProviderView(
},
footer = {
ButtonColumnMolecule {
- ButtonWithProgress(
+ Button(
text = stringResource(id = R.string.screen_account_provider_continue),
showProgress = isLoading,
onClick = { eventSink.invoke(ConfirmAccountProviderEvents.Continue) },
@@ -97,14 +96,13 @@ fun ConfirmAccountProviderView(
.testTag(TestTags.loginContinue)
)
TextButton(
+ text = stringResource(id = R.string.screen_account_provider_change),
onClick = onChange,
enabled = true,
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.loginChangeServer)
- ) {
- Text(text = stringResource(id = R.string.screen_account_provider_change))
- }
+ )
}
}
) {
@@ -143,12 +141,12 @@ fun ConfirmAccountProviderView(
@Preview
@Composable
-fun ConfirmAccountProviderViewLightPreview(@PreviewParameter(ConfirmAccountProviderStateProvider::class) state: ConfirmAccountProviderState) =
+internal fun ConfirmAccountProviderViewLightPreview(@PreviewParameter(ConfirmAccountProviderStateProvider::class) state: ConfirmAccountProviderState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
-fun ConfirmAccountProviderViewDarkPreview(@PreviewParameter(ConfirmAccountProviderStateProvider::class) state: ConfirmAccountProviderState) =
+internal fun ConfirmAccountProviderViewDarkPreview(@PreviewParameter(ConfirmAccountProviderStateProvider::class) state: ConfirmAccountProviderState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt
index d62506ff75..d06ab86b06 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt
@@ -60,11 +60,11 @@ import io.element.android.features.login.impl.error.loginError
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.components.button.BackButton
-import io.element.android.libraries.designsystem.components.button.ButtonWithProgress
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.components.form.textFieldState
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
+import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.OutlinedTextField
@@ -141,7 +141,7 @@ fun LoginPasswordView(
// Flexible spacing to keep the submit button at the bottom
Spacer(modifier = Modifier.weight(1f))
// Submit
- ButtonWithProgress(
+ Button(
text = stringResource(R.string.screen_login_submit),
showProgress = isLoading,
onClick = ::submit,
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt
index 84c5bf4fac..8cfaae2a9e 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt
@@ -211,12 +211,12 @@ private fun HomeserverData.toAccountProvider(): AccountProvider {
@Preview
@Composable
-fun SearchAccountProviderViewLightPreview(@PreviewParameter(SearchAccountProviderStateProvider::class) state: SearchAccountProviderState) =
+internal fun SearchAccountProviderViewLightPreview(@PreviewParameter(SearchAccountProviderStateProvider::class) state: SearchAccountProviderState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
-fun SearchAccountProviderViewDarkPreview(@PreviewParameter(SearchAccountProviderStateProvider::class) state: SearchAccountProviderState) =
+internal fun SearchAccountProviderViewDarkPreview(@PreviewParameter(SearchAccountProviderStateProvider::class) state: SearchAccountProviderState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListStateProvider.kt
index 5907ff1acf..94a38fa406 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListStateProvider.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListStateProvider.kt
@@ -25,7 +25,7 @@ open class WaitListStateProvider : PreviewParameterProvider {
get() = sequenceOf(
aWaitListState(loginAction = Async.Uninitialized),
aWaitListState(loginAction = Async.Loading()),
- aWaitListState(loginAction = Async.Failure(Throwable())),
+ aWaitListState(loginAction = Async.Failure(Throwable("error"))),
aWaitListState(loginAction = Async.Failure(Throwable(message = "IO_ELEMENT_X_WAIT_LIST"))),
aWaitListState(loginAction = Async.Success(SessionId("@alice:element.io"))),
// Add other state here
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListView.kt
index 73fbaf30f9..025a67516d 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListView.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListView.kt
@@ -29,7 +29,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.widthIn
-import androidx.compose.material3.ButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.BiasAbsoluteAlignment
@@ -52,7 +51,6 @@ import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
-import io.element.android.libraries.designsystem.theme.aliasButtonText
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.Text
@@ -141,18 +139,10 @@ private fun WaitListContent(
.padding(horizontal = 16.dp, vertical = 16.dp)
) {
if (state.loginAction !is Async.Success) {
- TextButton(
- onClick = onCancelClicked,
- colors = ButtonDefaults.buttonColors(
- containerColor = Color.White,
- contentColor = Color.Black,
- disabledContainerColor = Color.White,
- disabledContentColor = Color.Black,
- ),
- ) {
- Text(
+ ElementTheme(darkTheme = true) {
+ TextButton(
text = stringResource(CommonStrings.action_cancel),
- style = ElementTheme.typography.aliasButtonText,
+ onClick = onCancelClicked,
)
}
}
@@ -208,22 +198,14 @@ private fun WaitListContent(
}
}
if (state.loginAction is Async.Success) {
- Button(
- onClick = { state.eventSink.invoke(WaitListEvents.Continue) },
- colors = ButtonDefaults.buttonColors(
- containerColor = Color.White,
- contentColor = Color.Black,
- disabledContainerColor = Color.White,
- disabledContentColor = Color.Black,
- ),
- modifier = Modifier
- .fillMaxWidth()
- .align(Alignment.BottomCenter)
- .padding(bottom = 8.dp)
- ) {
- Text(
+ ElementTheme(darkTheme = true) {
+ Button(
text = stringResource(id = CommonStrings.action_continue),
- style = ElementTheme.typography.aliasButtonText,
+ onClick = { state.eventSink.invoke(WaitListEvents.Continue) },
+ modifier = Modifier
+ .fillMaxWidth()
+ .align(Alignment.BottomCenter)
+ .padding(bottom = 8.dp),
)
}
}
diff --git a/features/login/impl/src/main/res/drawable/ic_homeserver.xml b/features/login/impl/src/main/res/drawable/ic_homeserver.xml
deleted file mode 100644
index ee061f7007..0000000000
--- a/features/login/impl/src/main/res/drawable/ic_homeserver.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
diff --git a/features/login/impl/src/main/res/drawable/onboarding_icon_light.png b/features/login/impl/src/main/res/drawable/onboarding_icon_light.png
deleted file mode 100644
index ffd8631c47..0000000000
Binary files a/features/login/impl/src/main/res/drawable/onboarding_icon_light.png and /dev/null differ
diff --git a/features/login/impl/src/main/res/values-de/translations.xml b/features/login/impl/src/main/res/values-de/translations.xml
index efc7c0cf3c..965cc30c4e 100644
--- a/features/login/impl/src/main/res/values-de/translations.xml
+++ b/features/login/impl/src/main/res/values-de/translations.xml
@@ -3,13 +3,13 @@
"Kontoanbieter wechseln"
"Weiter"
"Adresse des Homeservers"
- "Geben Sie einen Suchbegriff oder eine Domainadresse ein."
+ "Gib einen Suchbegriff oder eine Domainadresse ein."
"Suche nach einem Unternehmen, einer Community oder einem privaten Server."
- "Finde einen Accountanbieter"
+ "Finde einen Kontoanbieter"
"Hier werden deine Konversationen stattfinden — genauso wie du einen E-Mail-Anbieter verwenden würdest, um deine E-Mails aufzubewahren."
"Du bist dabei dich bei %s anzumelden"
"Hier werden deine Konversationen stattfinden — genauso wie du einen E-Mail-Anbieter verwenden würdest, um deine E-Mails aufzubewahren."
- "Du bist dabei einen Account auf %s zu erstellen"
+ "Du bist dabei ein Konto auf %s zu erstellen"
"Matrix.org ist ein offenes Netzwerk für sichere, dezentralisierte Kommunikation."
"Andere"
"Verwende einen anderen Kontoanbieter, z. B. deinen eigenen privaten Server oder ein Arbeitskonto."
@@ -31,7 +31,7 @@
"Matrix ist ein offenes Netzwerk für sichere, dezentrale Kommunikation"
"Hier werden deine Konversationen stattfinden — genau so wie du einen E-Mail-Anbieter verwenden würdest, um deine E-Mails aufzubewahren."
"Du bist dabei dich bei %1$s anzumelden"
- "Du bist dabei einen Account auf %1$s zu erstellen"
+ "Du bist dabei ein Konto auf %1$s zu erstellen"
"Im Moment besteht eine hohe Nachfrage nach %1$s auf %2$s. Besuche die App in ein paar Tagen wieder und versuche es erneut.
Vielen Dank für deine Geduld!"
diff --git a/features/login/impl/src/main/res/values-ru/translations.xml b/features/login/impl/src/main/res/values-ru/translations.xml
new file mode 100644
index 0000000000..33514e9f09
--- /dev/null
+++ b/features/login/impl/src/main/res/values-ru/translations.xml
@@ -0,0 +1,47 @@
+
+
+ "Переключить аккаунт"
+ "Продолжить"
+ "Адрес домашнего сервера"
+ "Введите поисковый запрос или адрес домена."
+ "Поиск компании, сообщества или частного сервера."
+ "Поиск сервера учетной записи"
+ "Здесь будут храниться ваши разговоры - точно так же, как вы используете почтового провайдера для хранения своих писем."
+ "Вы собираетесь войти в %s"
+ "Здесь будут храниться ваши разговоры - точно так же, как вы используете почтового провайдера для хранения своих писем."
+ "Вы собираетесь создать учетную запись на %s"
+ "Matrix.org — это открытая сеть для безопасной децентрализованной связи."
+ "Другое"
+ "Используйте другого поставщика учетных записей, например, собственный частный сервер или рабочую учетную запись."
+ "Сменить поставщика учетной записи"
+ "Нам не удалось связаться с этим домашним сервером. Убедитесь, что вы правильно ввели URL-адрес домашнего сервера. Если URL-адрес указан правильно, обратитесь к администратору домашнего сервера за дополнительной помощью."
+ "В настоящее время этот сервер не поддерживает скользящую синхронизацию."
+ "URL-адрес домашнего сервера"
+ "Вы можете подключиться только к существующему серверу, поддерживающему скользящую синхронизацию. Администратору домашнего сервера потребуется настроить его. %1$s"
+ "Какой адрес у вашего сервера?"
+ "Данная учетная запись была деактивирована."
+ "Неверное имя пользователя и/или пароль"
+ "Это не корректный идентификатор пользователя. Ожидаемый формат: \'@user:homeserver.org\'"
+ "Выбранный домашний сервер не поддерживает пароль или логин OIDC. Пожалуйста, свяжитесь с администратором или выберите другой домашний сервер."
+ "Введите сведения о себе"
+ "Рады видеть вас снова!"
+ "Войти в %1$s"
+ "Сменить учетную запись"
+ "Частный сервер для сотрудников Element."
+ "Matrix — это открытая сеть для безопасной децентрализованной связи."
+ "Здесь будут храниться ваши разговоры - точно так же, как вы используете почтового провайдера для хранения своих писем."
+ "Вы собираетесь войти в %1$s"
+ "Вы собираетесь создать учетную запись на %1$s"
+ "В настоящее время существует высокий спрос на %1$s на %2$s. Вернитесь в приложение через несколько дней и попробуйте снова.
+
+Спасибо за терпение!"
+ "Добро пожаловать в %1$s!"
+ "Почти готово!"
+ "Вы зарегистрированы!"
+ "Продолжить"
+ "Выберите свой сервер"
+ "Пароль"
+ "Продолжить"
+ "Matrix — это открытая сеть для безопасной децентрализованной связи."
+ "Имя пользователя"
+
diff --git a/features/login/impl/src/main/res/values-zh-rTW/translations.xml b/features/login/impl/src/main/res/values-zh-rTW/translations.xml
new file mode 100644
index 0000000000..ae2ccae3f5
--- /dev/null
+++ b/features/login/impl/src/main/res/values-zh-rTW/translations.xml
@@ -0,0 +1,16 @@
+
+
+ "繼續"
+ "您即將登入%s"
+ "您即將在 %s 建立帳號"
+ "其他"
+ "歡迎回來!"
+ "您即將登入 %1$s"
+ "您即將在 %1$s 建立帳號"
+ "歡迎使用 %1$s!"
+ "繼續"
+ "選擇您的伺服器"
+ "密碼"
+ "繼續"
+ "使用者名稱"
+
diff --git a/features/logout/api/src/main/res/values-ru/translations.xml b/features/logout/api/src/main/res/values-ru/translations.xml
new file mode 100644
index 0000000000..3991e27251
--- /dev/null
+++ b/features/logout/api/src/main/res/values-ru/translations.xml
@@ -0,0 +1,8 @@
+
+
+ "Вы уверены, что вы хотите выйти?"
+ "Выйти"
+ "Выполняется выход…"
+ "Выйти"
+ "Выйти"
+
diff --git a/features/logout/api/src/main/res/values-zh-rTW/translations.xml b/features/logout/api/src/main/res/values-zh-rTW/translations.xml
new file mode 100644
index 0000000000..df722a9467
--- /dev/null
+++ b/features/logout/api/src/main/res/values-zh-rTW/translations.xml
@@ -0,0 +1,8 @@
+
+
+ "您確定要登出嗎?"
+ "登出"
+ "正在登出…"
+ "登出"
+ "登出"
+
diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts
index b5edaec78e..4746cff1de 100644
--- a/features/messages/impl/build.gradle.kts
+++ b/features/messages/impl/build.gradle.kts
@@ -34,6 +34,7 @@ dependencies {
anvil(projects.anvilcodegen)
api(projects.features.messages.api)
implementation(projects.features.location.api)
+ implementation(projects.features.poll.api)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
@@ -52,7 +53,6 @@ dependencies {
implementation(libs.coil.compose)
implementation(libs.datetime)
implementation(libs.accompanist.flowlayout)
- implementation(libs.androidx.recyclerview)
implementation(libs.jsoup)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.constraintlayout.compose)
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt
index a0a3e3a286..8a374471e3 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt
@@ -21,10 +21,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
@@ -41,6 +39,7 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer
import io.element.android.features.messages.impl.timeline.TimelineEvents
import io.element.android.features.messages.impl.timeline.TimelinePresenter
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter
+import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryPresenter
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
@@ -48,6 +47,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
+import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
@@ -72,10 +72,12 @@ import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
+import io.element.android.libraries.matrix.ui.room.canRedactAsState
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
import io.element.android.libraries.textcomposer.MessageComposerMode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import timber.log.Timber
class MessagesPresenter @AssistedInject constructor(
@@ -84,6 +86,7 @@ class MessagesPresenter @AssistedInject constructor(
private val timelinePresenter: TimelinePresenter,
private val actionListPresenter: ActionListPresenter,
private val customReactionPresenter: CustomReactionPresenter,
+ private val reactionSummaryPresenter: ReactionSummaryPresenter,
private val retrySendMenuPresenter: RetrySendMenuPresenter,
private val networkMonitor: NetworkMonitor,
private val snackbarDispatcher: SnackbarDispatcher,
@@ -105,32 +108,31 @@ class MessagesPresenter @AssistedInject constructor(
val timelineState = timelinePresenter.present()
val actionListState = actionListPresenter.present()
val customReactionState = customReactionPresenter.present()
+ val reactionSummaryState = reactionSummaryPresenter.present()
val retryState = retrySendMenuPresenter.present()
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value)
- val roomName by produceState(initialValue = room.displayName, key1 = syncUpdateFlow.value) {
- value = room.displayName
- }
- val roomAvatar by produceState(initialValue = room.avatarData(), key1 = syncUpdateFlow.value) {
- value = room.avatarData()
+ val userHasPermissionToRedact by room.canRedactAsState(updateKey = syncUpdateFlow.value)
+ var roomName: Async by remember { mutableStateOf(Async.Uninitialized) }
+ var roomAvatar: Async by remember { mutableStateOf(Async.Uninitialized) }
+ LaunchedEffect(syncUpdateFlow.value) {
+ withContext(dispatchers.io) {
+ roomName = Async.Success(room.displayName)
+ roomAvatar = Async.Success(room.avatarData())
+ }
}
var hasDismissedInviteDialog by rememberSaveable {
mutableStateOf(false)
}
val inviteProgress = remember { mutableStateOf>(Async.Uninitialized) }
-
- val showReinvitePrompt by remember(
- hasDismissedInviteDialog,
- composerState.hasFocus,
- syncUpdateFlow,
- ) {
- derivedStateOf {
- !hasDismissedInviteDialog && composerState.hasFocus && room.isDirect && room.activeMemberCount == 1L
+ var showReinvitePrompt by remember { mutableStateOf(false) }
+ LaunchedEffect(hasDismissedInviteDialog, composerState.hasFocus, syncUpdateFlow) {
+ withContext(dispatchers.io) {
+ showReinvitePrompt = !hasDismissedInviteDialog && composerState.hasFocus && room.isDirect && room.activeMemberCount == 1L
}
}
-
val networkConnectionStatus by networkMonitor.connectivity.collectAsState()
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
@@ -163,10 +165,12 @@ class MessagesPresenter @AssistedInject constructor(
roomName = roomName,
roomAvatar = roomAvatar,
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
+ userHasPermissionToRedact = userHasPermissionToRedact,
composerState = composerState,
timelineState = timelineState,
actionListState = actionListState,
customReactionState = customReactionState,
+ reactionSummaryState = reactionSummaryState,
retrySendMenuState = retryState,
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
snackbarMessage = snackbarMessage,
@@ -274,6 +278,7 @@ class MessagesPresenter @AssistedInject constructor(
is TimelineItemLocationContent -> AttachmentThumbnailInfo(
type = AttachmentThumbnailType.Location,
)
+ is TimelineItemPollContent, // TODO Polls: handle reply to
is TimelineItemTextBasedContent,
is TimelineItemRedactedContent,
is TimelineItemStateContent,
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt
index 8a067a3a26..d22d54e7f3 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt
@@ -21,6 +21,7 @@ import io.element.android.features.messages.impl.actionlist.ActionListState
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
import io.element.android.features.messages.impl.timeline.TimelineState
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
+import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.components.avatar.AvatarData
@@ -30,13 +31,15 @@ import io.element.android.libraries.matrix.api.core.RoomId
@Immutable
data class MessagesState(
val roomId: RoomId,
- val roomName: String,
- val roomAvatar: AvatarData,
+ val roomName: Async,
+ val roomAvatar: Async,
val userHasPermissionToSendMessage: Boolean,
+ val userHasPermissionToRedact: Boolean,
val composerState: MessageComposerState,
val timelineState: TimelineState,
val actionListState: ActionListState,
val customReactionState: CustomReactionState,
+ val reactionSummaryState: ReactionSummaryState,
val retrySendMenuState: RetrySendMenuState,
val hasNetworkConnection: Boolean,
val snackbarMessage: SnackbarMessage?,
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt
index d0ddcf68f4..9b3f5073a1 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt
@@ -22,6 +22,7 @@ import io.element.android.features.messages.impl.messagecomposer.aMessageCompose
import io.element.android.features.messages.impl.timeline.aTimelineItemList
import io.element.android.features.messages.impl.timeline.aTimelineState
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
+import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
import io.element.android.libraries.architecture.Async
@@ -29,6 +30,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.textcomposer.MessageComposerMode
+import kotlinx.collections.immutable.persistentSetOf
open class MessagesStateProvider : PreviewParameterProvider {
override val values: Sequence
@@ -38,14 +40,19 @@ open class MessagesStateProvider : PreviewParameterProvider {
aMessagesState().copy(composerState = aMessageComposerState().copy(showAttachmentSourcePicker = true)),
aMessagesState().copy(userHasPermissionToSendMessage = false),
aMessagesState().copy(showReinvitePrompt = true),
+ aMessagesState().copy(
+ roomName = Async.Uninitialized,
+ roomAvatar = Async.Uninitialized,
+ ),
)
}
fun aMessagesState() = MessagesState(
roomId = RoomId("!id:domain"),
- roomName = "Room name",
- roomAvatar = AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom),
+ roomName = Async.Success("Room name"),
+ roomAvatar = Async.Success(AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom)),
userHasPermissionToSendMessage = true,
+ userHasPermissionToRedact = false,
composerState = aMessageComposerState().copy(
text = "Hello",
isFullScreen = false,
@@ -62,6 +69,11 @@ fun aMessagesState() = MessagesState(
customReactionState = CustomReactionState(
selectedEventId = null,
eventSink = {},
+ selectedEmoji = persistentSetOf(),
+ ),
+ reactionSummaryState = ReactionSummaryState(
+ target = null,
+ eventSink = {},
),
hasNetworkConnection = true,
snackbarMessage = null,
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt
index b68b0eea7a..145813fe54 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt
@@ -35,7 +35,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.SnackbarHost
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
@@ -53,19 +52,24 @@ import io.element.android.features.messages.impl.actionlist.ActionListView
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
+import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
import io.element.android.features.messages.impl.messagecomposer.MessageComposerView
import io.element.android.features.messages.impl.timeline.TimelineView
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvents
+import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvents
+import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryView
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuEvents
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMessageMenu
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
import io.element.android.libraries.androidutils.ui.hideKeyboard
+import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule
import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.ProgressDialogType
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.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
@@ -74,6 +78,7 @@ 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 io.element.android.libraries.designsystem.utils.SnackbarHost
import io.element.android.libraries.designsystem.utils.rememberSnackbarHostState
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
@@ -96,7 +101,11 @@ fun MessagesView(
) {
LogCompositions(tag = "MessagesScreen", msg = "Root")
- AttachmentStateView(state.composerState.attachmentsState, onPreviewAttachments)
+ AttachmentStateView(
+ state = state.composerState.attachmentsState,
+ onPreviewAttachments = onPreviewAttachments,
+ onCancel = { state.composerState.eventSink(MessageComposerEvents.CancelSendAttachment) },
+ )
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
@@ -113,7 +122,7 @@ fun MessagesView(
fun onMessageLongClicked(event: TimelineItem.Event) {
Timber.v("OnMessageLongClicked= ${event.id}")
localView.hideKeyboard()
- state.actionListState.eventSink(ActionListEvents.ComputeForMessage(event))
+ state.actionListState.eventSink(ActionListEvents.ComputeForMessage(event, state.userHasPermissionToRedact))
}
fun onActionSelected(action: TimelineItemAction, event: TimelineItem.Event) {
@@ -125,8 +134,14 @@ fun MessagesView(
state.eventSink(MessagesEvents.ToggleReaction(emoji, event.eventId))
}
- fun onMoreReactionsClicked(event: TimelineItem.Event): Unit =
- state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(event.eventId))
+ fun onEmojiReactionLongClicked(emoji: String, event: TimelineItem.Event) {
+ if (event.eventId == null) return
+ state.reactionSummaryState.eventSink(ReactionSummaryEvents.ShowReactionSummary(event.eventId, event.reactionsState.reactions, emoji))
+ }
+
+ fun onMoreReactionsClicked(event: TimelineItem.Event) {
+ state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(event))
+ }
Scaffold(
modifier = modifier,
@@ -135,8 +150,8 @@ fun MessagesView(
Column {
ConnectivityIndicatorView(isOnline = state.hasNetworkConnection)
MessagesViewTopBar(
- roomTitle = state.roomName,
- roomAvatar = state.roomAvatar,
+ roomName = state.roomName.dataOrNull(),
+ roomAvatar = state.roomAvatar.dataOrNull(),
onBackPressed = onBackPressed,
onRoomDetailsClicked = onRoomDetailsClicked,
)
@@ -157,6 +172,7 @@ fun MessagesView(
}
},
onReactionClicked = ::onEmojiReactionClicked,
+ onReactionLongClicked = ::onEmojiReactionLongClicked,
onMoreReactionsClicked = ::onMoreReactionsClicked,
onSendLocationClicked = onSendLocationClicked,
onSwipeToReply = { targetEvent ->
@@ -176,7 +192,7 @@ fun MessagesView(
state = state.actionListState,
onActionSelected = ::onActionSelected,
onCustomReactionClicked = { event ->
- state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(event.eventId))
+ state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(event))
},
onEmojiReactionClicked = ::onEmojiReactionClicked,
)
@@ -191,6 +207,7 @@ fun MessagesView(
}
)
+ ReactionSummaryView(state = state.reactionSummaryState)
RetrySendMessageMenu(
state = state.retrySendMenuState
)
@@ -201,14 +218,13 @@ fun MessagesView(
}
@Composable
-fun ReinviteDialog(state: MessagesState) {
+private fun ReinviteDialog(state: MessagesState) {
if (state.showReinvitePrompt) {
ConfirmationDialog(
title = stringResource(id = R.string.screen_room_invite_again_alert_title),
content = stringResource(id = R.string.screen_room_invite_again_alert_message),
cancelText = stringResource(id = CommonStrings.action_cancel),
submitText = stringResource(id = CommonStrings.action_invite),
- emphasizeSubmitButton = true,
onSubmitClicked = { state.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite)) },
onDismiss = { state.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Cancel)) }
)
@@ -218,7 +234,8 @@ fun ReinviteDialog(state: MessagesState) {
@Composable
private fun AttachmentStateView(
state: AttachmentsState,
- onPreviewAttachments: (ImmutableList) -> Unit
+ onPreviewAttachments: (ImmutableList) -> Unit,
+ onCancel: () -> Unit,
) {
when (state) {
AttachmentsState.None -> Unit
@@ -231,18 +248,21 @@ private fun AttachmentStateView(
is AttachmentsState.Sending.Uploading -> ProgressDialogType.Determinate(state.progress)
is AttachmentsState.Sending.Processing -> ProgressDialogType.Indeterminate
},
- text = stringResource(id = CommonStrings.common_sending)
+ text = stringResource(id = CommonStrings.common_sending),
+ isCancellable = true,
+ onDismissRequest = onCancel,
)
}
}
}
@Composable
-fun MessagesViewContent(
+private fun MessagesViewContent(
state: MessagesState,
onMessageClicked: (TimelineItem.Event) -> Unit,
onUserDataClicked: (UserId) -> Unit,
onReactionClicked: (key: String, TimelineItem.Event) -> Unit,
+ onReactionLongClicked: (key: String, TimelineItem.Event) -> Unit,
onMoreReactionsClicked: (TimelineItem.Event) -> Unit,
onMessageLongClicked: (TimelineItem.Event) -> Unit,
onTimestampClicked: (TimelineItem.Event) -> Unit,
@@ -266,6 +286,7 @@ fun MessagesViewContent(
onUserDataClicked = onUserDataClicked,
onTimestampClicked = onTimestampClicked,
onReactionClicked = onReactionClicked,
+ onReactionLongClicked = onReactionLongClicked,
onMoreReactionsClicked = onMoreReactionsClicked,
onSwipeToReply = onSwipeToReply,
)
@@ -286,9 +307,9 @@ fun MessagesViewContent(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun MessagesViewTopBar(
- roomTitle: String,
- roomAvatar: AvatarData,
+private fun MessagesViewTopBar(
+ roomName: String?,
+ roomAvatar: AvatarData?,
modifier: Modifier = Modifier,
onRoomDetailsClicked: () -> Unit = {},
onBackPressed: () -> Unit = {},
@@ -299,17 +320,17 @@ fun MessagesViewTopBar(
BackButton(onClick = onBackPressed)
},
title = {
- Row(
- modifier = Modifier.clickable { onRoomDetailsClicked() },
- verticalAlignment = Alignment.CenterVertically
- ) {
- Avatar(roomAvatar)
- Spacer(modifier = Modifier.width(8.dp))
- Text(
- text = roomTitle,
- style = ElementTheme.typography.fontBodyLgMedium,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
+ val titleModifier = Modifier.clickable { onRoomDetailsClicked() }
+ if (roomName != null && roomAvatar != null) {
+ RoomAvatarAndNameRow(
+ roomName = roomName,
+ roomAvatar = roomAvatar,
+ modifier = titleModifier
+ )
+ } else {
+ IconTitlePlaceholdersRowMolecule(
+ iconSize = AvatarSize.TimelineRoom.dp,
+ modifier = titleModifier
)
}
},
@@ -318,7 +339,28 @@ fun MessagesViewTopBar(
}
@Composable
-fun CantSendMessageBanner(
+private fun RoomAvatarAndNameRow(
+ roomName: String,
+ roomAvatar: AvatarData,
+ modifier: Modifier = Modifier
+) {
+ Row(
+ modifier = modifier,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Avatar(roomAvatar)
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ text = roomName,
+ style = ElementTheme.typography.fontBodyLgMedium,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+}
+
+@Composable
+private fun CantSendMessageBanner(
modifier: Modifier = Modifier,
) {
Row(
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListEvents.kt
index a6244a72e3..3c796036e7 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListEvents.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListEvents.kt
@@ -20,5 +20,5 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
sealed interface ActionListEvents {
object Clear : ActionListEvents
- data class ComputeForMessage(val event: TimelineItem.Event) : ActionListEvents
+ data class ComputeForMessage(val event: TimelineItem.Event, val canRedact: Boolean) : ActionListEvents
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt
index 2d18018746..f71c750c22 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt
@@ -28,6 +28,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
import io.element.android.features.messages.impl.timeline.model.event.canBeCopied
+import io.element.android.features.messages.impl.timeline.model.event.canReact
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta
import kotlinx.collections.immutable.toImmutableList
@@ -48,13 +49,20 @@ class ActionListPresenter @Inject constructor(
}
val displayEmojiReactions by remember {
- derivedStateOf { (target.value as? ActionListState.Target.Success)?.event?.isRemote == true }
+ derivedStateOf {
+ val event = (target.value as? ActionListState.Target.Success)?.event
+ event?.isRemote == true && event.content.canReact()
+ }
}
fun handleEvents(event: ActionListEvents) {
when (event) {
ActionListEvents.Clear -> target.value = ActionListState.Target.None
- is ActionListEvents.ComputeForMessage -> localCoroutineScope.computeForMessage(event.event, target)
+ is ActionListEvents.ComputeForMessage -> localCoroutineScope.computeForMessage(
+ timelineItem = event.event,
+ userCanRedact = event.canRedact,
+ target = target,
+ )
}
}
@@ -65,7 +73,11 @@ class ActionListPresenter @Inject constructor(
)
}
- private fun CoroutineScope.computeForMessage(timelineItem: TimelineItem.Event, target: MutableState) = launch {
+ private fun CoroutineScope.computeForMessage(
+ timelineItem: TimelineItem.Event,
+ userCanRedact: Boolean,
+ target: MutableState
+ ) = launch {
target.value = ActionListState.Target.Loading(timelineItem)
val actions =
when (timelineItem.content) {
@@ -102,7 +114,7 @@ class ActionListPresenter @Inject constructor(
if (!timelineItem.isMine) {
add(TimelineItemAction.ReportContent)
}
- if (timelineItem.isMine) {
+ if (timelineItem.isMine || userCanRedact) {
add(TimelineItemAction.Redact)
}
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt
index fd2ad94345..c1561d5458 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt
@@ -31,6 +31,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ListItem
@@ -61,6 +62,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
+import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
@@ -72,7 +74,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.text.toSp
-import io.element.android.libraries.designsystem.theme.components.Divider
+import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
import io.element.android.libraries.designsystem.theme.components.hide
@@ -175,7 +177,7 @@ private fun SheetContent(
.padding(horizontal = 16.dp)
)
Spacer(modifier = Modifier.height(14.dp))
- Divider()
+ HorizontalDivider()
}
}
if (state.displayEmojiReactions) {
@@ -186,7 +188,7 @@ private fun SheetContent(
onCustomReactionClicked = onCustomReactionClicked,
modifier = Modifier.fillMaxWidth(),
)
- Divider()
+ HorizontalDivider()
}
}
items(
@@ -235,6 +237,7 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif
val textContent = remember(event.content) { formatter.format(event) }
when (event.content) {
+ is TimelineItemPollContent, // TODO Polls: handle summary
is TimelineItemTextBasedContent,
is TimelineItemStateContent,
is TimelineItemEncryptedContent,
@@ -342,7 +345,7 @@ internal fun EmojiReactionsRow(
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
- modifier = modifier.padding(horizontal = 28.dp, vertical = 16.dp)
+ modifier = modifier.padding(horizontal = 24.dp, vertical = 16.dp)
) {
// TODO use most recently used emojis here when available from the Rust SDK
val defaultEmojis = sequenceOf(
@@ -352,21 +355,25 @@ internal fun EmojiReactionsRow(
val isHighlighted = highlightedEmojis.contains(emoji)
EmojiButton(emoji, isHighlighted, onEmojiReactionClicked)
}
-
- Icon(
- imageVector = Icons.Outlined.AddReaction,
- contentDescription = "Emojis",
- tint = MaterialTheme.colorScheme.secondary,
+ Box(
modifier = Modifier
- .size(24.dp)
- .align(Alignment.CenterVertically)
- .clickable(
- enabled = true,
- onClick = onCustomReactionClicked,
- indication = rememberRipple(bounded = false, radius = emojiRippleRadius),
- interactionSource = remember { MutableInteractionSource() }
- )
- )
+ .size(48.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.AddReaction,
+ contentDescription = "Emojis",
+ tint = MaterialTheme.colorScheme.secondary,
+ modifier = Modifier
+ .size(24.dp)
+ .clickable(
+ enabled = true,
+ onClick = onCustomReactionClicked,
+ indication = rememberRipple(bounded = false, radius = emojiRippleRadius),
+ interactionSource = remember { MutableInteractionSource() }
+ )
+ )
+ }
}
}
@@ -385,12 +392,13 @@ private fun EmojiButton(
Box(
modifier = modifier
.size(48.dp)
- .background(backgroundColor, RoundedCornerShape(24.dp)),
+ .background(backgroundColor, CircleShape),
+
contentAlignment = Alignment.Center
) {
Text(
emoji,
- fontSize = 28.dp.toSp(),
+ fontSize = 24.dp.toSp(),
color = Color.White,
modifier = Modifier
.clickable(
@@ -405,7 +413,7 @@ private fun EmojiButton(
@DayNightPreviews
@Composable
-fun SheetContentPreview(
+internal fun SheetContentPreview(
@PreviewParameter(ActionListStateProvider::class) state: ActionListState
) = ElementPreview {
SheetContent(
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt
index 3ee87c0bc8..983d016854 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt
@@ -28,8 +28,12 @@ import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.mediaupload.api.MediaSender
+import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
+import kotlin.coroutines.coroutineContext
class AttachmentsPreviewPresenter @AssistedInject constructor(
@Assisted private val attachment: Attachment,
@@ -50,10 +54,18 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
mutableStateOf(SendActionState.Idle)
}
+ val ongoingSendAttachmentJob = remember { mutableStateOf(null) }
+
fun handleEvents(attachmentsPreviewEvents: AttachmentsPreviewEvents) {
when (attachmentsPreviewEvents) {
- AttachmentsPreviewEvents.SendAttachment -> coroutineScope.sendAttachment(attachment, sendActionState)
- AttachmentsPreviewEvents.ClearSendState -> sendActionState.value = SendActionState.Idle
+ AttachmentsPreviewEvents.SendAttachment -> ongoingSendAttachmentJob.value = coroutineScope.sendAttachment(attachment, sendActionState)
+ AttachmentsPreviewEvents.ClearSendState -> {
+ ongoingSendAttachmentJob.value?.let {
+ it.cancel()
+ ongoingSendAttachmentJob.value = null
+ }
+ sendActionState.value = SendActionState.Idle
+ }
}
}
@@ -72,7 +84,7 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
is Attachment.Media -> {
sendMedia(
mediaAttachment = attachment,
- sendActionState = sendActionState
+ sendActionState = sendActionState,
)
}
}
@@ -81,10 +93,13 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
private suspend fun sendMedia(
mediaAttachment: Attachment.Media,
sendActionState: MutableState,
- ) {
+ ) = runCatching {
+ val context = coroutineContext
val progressCallback = object : ProgressCallback {
override fun onProgress(current: Long, total: Long) {
- sendActionState.value = SendActionState.Sending.Uploading(current.toFloat() / total.toFloat())
+ if (context.isActive) {
+ sendActionState.value = SendActionState.Sending.Uploading(current.toFloat() / total.toFloat())
+ }
}
}
sendActionState.value = SendActionState.Sending.Processing
@@ -93,13 +108,17 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
mimeType = mediaAttachment.localMedia.info.mimeType,
compressIfPossible = mediaAttachment.compressIfPossible,
progressCallback = progressCallback
- ).fold(
- onSuccess = {
- sendActionState.value = SendActionState.Done
- },
- onFailure = {
- sendActionState.value = SendActionState.Failure(it)
+ ).getOrThrow()
+ }.fold(
+ onSuccess = {
+ sendActionState.value = SendActionState.Done
+ },
+ onFailure = { error ->
+ if (error is CancellationException) {
+ throw error
+ } else {
+ sendActionState.value = SendActionState.Failure(error)
}
- )
- }
+ }
+ )
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt
index ee41ace4b0..de7f5cd47b 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt
@@ -30,7 +30,7 @@ open class AttachmentsPreviewStateProvider : PreviewParameterProvider ProgressDialogType.Determinate(sendActionState.progress)
SendActionState.Sending.Processing -> ProgressDialogType.Indeterminate
},
- text = stringResource(id = CommonStrings.common_sending)
+ text = stringResource(id = CommonStrings.common_sending),
+ isCancellable = true,
+ onDismissRequest = onDismissClicked,
)
}
is SendActionState.Failure -> {
@@ -155,18 +156,14 @@ private fun AttachmentsPreviewBottomActions(
ButtonRowMolecule(
modifier = modifier,
) {
- TextButton(onClick = onCancelClicked) {
- Text(stringResource(id = CommonStrings.action_cancel))
- }
- TextButton(onClick = onSendClicked) {
- Text(stringResource(id = CommonStrings.action_send))
- }
+ TextButton(stringResource(id = CommonStrings.action_cancel), onClick = onCancelClicked)
+ TextButton(stringResource(id = CommonStrings.action_send), onClick = onSendClicked)
}
}
@Preview
@Composable
-fun AttachmentsPreviewViewDarkPreview(@PreviewParameter(AttachmentsPreviewStateProvider::class) state: AttachmentsPreviewState) =
+internal fun AttachmentsPreviewViewDarkPreview(@PreviewParameter(AttachmentsPreviewStateProvider::class) state: AttachmentsPreviewState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesEvents.kt
index 6b74918d71..0ae406efff 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesEvents.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesEvents.kt
@@ -16,7 +16,7 @@
package io.element.android.features.messages.impl.forward
-import io.element.android.libraries.matrix.api.room.RoomSummaryDetails
+import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
sealed interface ForwardMessagesEvents {
data class SetSelectedRoom(val room: RoomSummaryDetails) : ForwardMessagesEvents
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt
index e1d7ed3e7e..273ed5906d 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt
@@ -35,8 +35,8 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.MatrixRoom
-import io.element.android.libraries.matrix.api.room.RoomSummary
-import io.element.android.libraries.matrix.api.room.RoomSummaryDetails
+import io.element.android.libraries.matrix.api.roomlist.RoomSummary
+import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
@@ -65,7 +65,7 @@ class ForwardMessagesPresenter @AssistedInject constructor(
var results: SearchBarResultState> by remember { mutableStateOf(SearchBarResultState.NotSearching()) }
val forwardingActionState: MutableState>> = remember { mutableStateOf(Async.Uninitialized) }
- val summaries by client.roomSummaryDataSource.allRooms().collectAsState()
+ val summaries by client.roomListService.allRooms().summaries.collectAsState()
LaunchedEffect(query, summaries) {
val filteredSummaries = summaries.filterIsInstance()
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesState.kt
index 7540766097..953a7897f6 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesState.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesState.kt
@@ -18,7 +18,7 @@ package io.element.android.features.messages.impl.forward
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.core.RoomId
-import io.element.android.libraries.matrix.api.room.RoomSummaryDetails
+import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
import kotlinx.collections.immutable.ImmutableList
data class ForwardMessagesState(
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesStateProvider.kt
index 75aacea616..56d7f63eb1 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesStateProvider.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesStateProvider.kt
@@ -20,7 +20,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomMember
-import io.element.android.libraries.matrix.api.room.RoomSummaryDetails
+import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
import io.element.android.libraries.matrix.api.room.message.RoomMessage
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesView.kt
index 467a963088..bbcb05f406 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesView.kt
@@ -52,7 +52,7 @@ import io.element.android.libraries.designsystem.components.dialogs.ErrorDialogD
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
-import io.element.android.libraries.designsystem.theme.components.Divider
+import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.designsystem.theme.components.RadioButton
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.SearchBar
@@ -63,7 +63,7 @@ import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.theme.roomListRoomMessage
import io.element.android.libraries.designsystem.theme.roomListRoomName
import io.element.android.libraries.matrix.api.core.RoomId
-import io.element.android.libraries.matrix.api.room.RoomSummaryDetails
+import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
import io.element.android.libraries.matrix.ui.components.SelectedRoom
import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings
@@ -123,11 +123,10 @@ fun ForwardMessagesView(
},
actions = {
TextButton(
+ text = stringResource(CommonStrings.action_send),
enabled = state.selectedRooms.isNotEmpty(),
onClick = { state.eventSink(ForwardMessagesEvents.ForwardEvent) }
- ) {
- Text(text = stringResource(CommonStrings.action_send))
- }
+ )
}
)
}
@@ -162,7 +161,7 @@ fun ForwardMessagesView(
state.eventSink(ForwardMessagesEvents.SetSelectedRoom(roomSummary))
}
)
- Divider(modifier = Modifier.fillMaxWidth())
+ HorizontalDivider(modifier = Modifier.fillMaxWidth())
}
}
}
@@ -187,7 +186,7 @@ fun ForwardMessagesView(
state.eventSink(ForwardMessagesEvents.SetSelectedRoom(roomSummary))
}
)
- Divider(modifier = Modifier.fillMaxWidth())
+ HorizontalDivider(modifier = Modifier.fillMaxWidth())
}
}
}
@@ -283,12 +282,12 @@ private fun ForwardingErrorDialog(onDismiss: () -> Unit, modifier: Modifier = Mo
@Preview
@Composable
-fun ForwardMessagesViewLightPreview(@PreviewParameter(ForwardMessagesStateProvider::class) state: ForwardMessagesState) =
+internal fun ForwardMessagesViewLightPreview(@PreviewParameter(ForwardMessagesStateProvider::class) state: ForwardMessagesState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
-fun ForwardMessagesViewDarkPreview(@PreviewParameter(ForwardMessagesStateProvider::class) state: ForwardMessagesState) =
+internal fun ForwardMessagesViewDarkPreview(@PreviewParameter(ForwardMessagesStateProvider::class) state: ForwardMessagesState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt
index 7cc73ef32e..13a9ff3bee 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt
@@ -103,14 +103,17 @@ class MediaViewerPresenter @AssistedInject constructor(
)
.onSuccess {
mediaFile.value = it
- }.mapCatching { mediaFile ->
+ }
+ .mapCatching { mediaFile ->
localMediaFactory.createFromMediaFile(
mediaFile = mediaFile,
mediaInfo = inputs.mediaInfo
)
- }.onSuccess {
+ }
+ .onSuccess {
localMedia.value = Async.Success(it)
- }.onFailure {
+ }
+ .onFailure {
localMedia.value = Async.Failure(it)
}
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt
index 820a34d8d4..1042261be8 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt
@@ -32,7 +32,7 @@ open class MediaViewerStateProvider : PreviewParameterProvider
get() = sequenceOf(
aMediaViewerState(),
aMediaViewerState(Async.Loading()),
- aMediaViewerState(Async.Failure(IllegalStateException())),
+ aMediaViewerState(Async.Failure(IllegalStateException("error"))),
aMediaViewerState(
Async.Success(
LocalMedia(Uri.EMPTY, anImageInfo())
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt
index 5fb14a6ca0..66f15225f7 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt
@@ -34,9 +34,6 @@ import androidx.compose.material.icons.filled.OpenInNew
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LinearProgressIndicator
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Snackbar
-import androidx.compose.material3.SnackbarHost
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -64,6 +61,7 @@ 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.TopAppBar
+import io.element.android.libraries.designsystem.utils.SnackbarHost
import io.element.android.libraries.designsystem.utils.rememberSnackbarHostState
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.ui.media.MediaRequestData
@@ -99,15 +97,7 @@ fun MediaViewerView(
eventSink = state.eventSink
)
},
- snackbarHost = {
- SnackbarHost(snackbarHostState) { data ->
- Snackbar(
- snackbarData = data,
- containerColor = MaterialTheme.colorScheme.surfaceVariant,
- contentColor = MaterialTheme.colorScheme.primary
- )
- }
- },
+ snackbarHost = { SnackbarHost(snackbarHostState) },
) {
Column(
modifier = Modifier
@@ -255,7 +245,7 @@ private fun ErrorView(
@Preview
@Composable
-fun MediaViewerViewDarkPreview(@PreviewParameter(MediaViewerStateProvider::class) state: MediaViewerState) =
+internal fun MediaViewerViewDarkPreview(@PreviewParameter(MediaViewerStateProvider::class) state: MediaViewerState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt
index 46e57e92de..d040c503b1 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt
@@ -36,4 +36,5 @@ sealed interface MessageComposerEvents {
object VideoFromCamera : PickAttachmentSource
object Location : PickAttachmentSource
}
+ object CancelSendAttachment : MessageComposerEvents
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt
index ec16d3342d..5477b10c63 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt
@@ -47,9 +47,13 @@ import io.element.android.libraries.mediaupload.api.MediaSender
import io.element.android.libraries.textcomposer.MessageComposerMode
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.collections.immutable.persistentListOf
+import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import javax.inject.Inject
+import kotlin.coroutines.coroutineContext
import io.element.android.libraries.core.mimetype.MimeTypes.Any as AnyMimeTypes
@SingleIn(RoomScope::class)
@@ -100,6 +104,7 @@ class MessageComposerPresenter @Inject constructor(
val text: MutableState = rememberSaveable {
mutableStateOf("")
}
+ val ongoingSendAttachmentJob = remember { mutableStateOf(null) }
var showAttachmentSourcePicker: Boolean by remember { mutableStateOf(false) }
@@ -112,7 +117,12 @@ class MessageComposerPresenter @Inject constructor(
LaunchedEffect(attachmentsState.value) {
when (val attachmentStateValue = attachmentsState.value) {
- is AttachmentsState.Sending.Processing -> localCoroutineScope.sendAttachment(attachmentStateValue.attachments.first(), attachmentsState)
+ is AttachmentsState.Sending.Processing -> {
+ ongoingSendAttachmentJob.value = localCoroutineScope.sendAttachment(
+ attachmentStateValue.attachments.first(),
+ attachmentsState,
+ )
+ }
else -> Unit
}
}
@@ -169,6 +179,12 @@ class MessageComposerPresenter @Inject constructor(
showAttachmentSourcePicker = false
// Navigation to the location picker screen is done at the view layer
}
+ is MessageComposerEvents.CancelSendAttachment -> {
+ ongoingSendAttachmentJob.value?.let {
+ it.cancel()
+ ongoingSendAttachmentJob.value == null
+ }
+ }
}
}
@@ -212,13 +228,13 @@ class MessageComposerPresenter @Inject constructor(
private fun CoroutineScope.sendAttachment(
attachment: Attachment,
attachmentState: MutableState,
- ) = launch {
- when (attachment) {
- is Attachment.Media -> {
+ ) = when (attachment) {
+ is Attachment.Media -> {
+ launch {
sendMedia(
uri = attachment.localMedia.uri,
mimeType = attachment.localMedia.info.mimeType,
- attachmentState = attachmentState
+ attachmentState = attachmentState,
)
}
}
@@ -259,19 +275,27 @@ class MessageComposerPresenter @Inject constructor(
uri: Uri,
mimeType: String,
attachmentState: MutableState,
- ) {
+ ) = runCatching {
+ val context = coroutineContext
val progressCallback = object : ProgressCallback {
override fun onProgress(current: Long, total: Long) {
- attachmentState.value = AttachmentsState.Sending.Uploading(current.toFloat() / total.toFloat())
+ if (context.isActive) {
+ attachmentState.value = AttachmentsState.Sending.Uploading(current.toFloat() / total.toFloat())
+ }
}
}
- mediaSender.sendMedia(uri, mimeType, compressIfPossible = false, progressCallback)
- .onSuccess {
- attachmentState.value = AttachmentsState.None
- }.onFailure {
- val snackbarMessage = SnackbarMessage(sendAttachmentError(it))
- snackbarDispatcher.post(snackbarMessage)
- attachmentState.value = AttachmentsState.None
- }
+ mediaSender.sendMedia(uri, mimeType, compressIfPossible = false, progressCallback).getOrThrow()
+ }
+ .onSuccess {
+ attachmentState.value = AttachmentsState.None
+ }
+ .onFailure { cause ->
+ attachmentState.value = AttachmentsState.None
+ if (cause is CancellationException) {
+ throw cause
+ } else {
+ val snackbarMessage = SnackbarMessage(sendAttachmentError(cause))
+ snackbarDispatcher.post(snackbarMessage)
+ }
}
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageStateProvider.kt
index 89e6d7a220..de5e787a3d 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageStateProvider.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageStateProvider.kt
@@ -26,7 +26,7 @@ open class ReportMessageStateProvider : PreviewParameterProvider
val key = emojis[index % emojis.size]
- add(AggregatedReaction(key = key, count = 1 + index, isHighlighted = isHighlighted))
+ add(anAggregatedReaction(
+ key = key,
+ count = index + 1,
+ isHighlighted = isHighlighted
+ ))
}
}.toPersistentList()
)
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt
index d7c259eba2..6e16a3b92d 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt
@@ -81,6 +81,7 @@ fun TimelineView(
onTimestampClicked: (TimelineItem.Event) -> Unit,
onSwipeToReply: (TimelineItem.Event) -> Unit,
onReactionClicked: (emoji: String, TimelineItem.Event) -> Unit,
+ onReactionLongClicked: (emoji: String, TimelineItem.Event) -> Unit,
onMoreReactionsClicked: (TimelineItem.Event) -> Unit,
modifier: Modifier = Modifier,
) {
@@ -121,6 +122,7 @@ fun TimelineView(
onUserDataClick = onUserDataClicked,
inReplyToClick = ::inReplyToClicked,
onReactionClick = onReactionClicked,
+ onReactionLongClick = onReactionLongClicked,
onMoreReactionsClick = onMoreReactionsClicked,
onTimestampClicked = onTimestampClicked,
onSwipeToReply = onSwipeToReply,
@@ -138,6 +140,7 @@ fun TimelineView(
}
TimelineScrollHelper(
+ isTimelineEmpty = state.timelineItems.isEmpty(),
lazyListState = lazyListState,
hasNewItems = state.hasNewItems,
onScrollFinishedAt = ::onScrollFinishedAt
@@ -155,6 +158,7 @@ fun TimelineItemRow(
onLongClick: (TimelineItem.Event) -> Unit,
inReplyToClick: (EventId) -> Unit,
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
+ onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
onTimestampClicked: (TimelineItem.Event) -> Unit,
onSwipeToReply: (TimelineItem.Event) -> Unit,
@@ -186,6 +190,7 @@ fun TimelineItemRow(
onUserDataClick = onUserDataClick,
inReplyToClick = inReplyToClick,
onReactionClick = onReactionClick,
+ onReactionLongClick = onReactionLongClick,
onMoreReactionsClick = onMoreReactionsClick,
onTimestampClicked = onTimestampClicked,
onSwipeToReply = { onSwipeToReply(timelineItem) },
@@ -224,6 +229,7 @@ fun TimelineItemRow(
onUserDataClick = onUserDataClick,
onTimestampClicked = onTimestampClicked,
onReactionClick = onReactionClick,
+ onReactionLongClick = onReactionLongClick,
onMoreReactionsClick = onMoreReactionsClick,
onSwipeToReply = {},
)
@@ -237,6 +243,7 @@ fun TimelineItemRow(
@Composable
private fun BoxScope.TimelineScrollHelper(
+ isTimelineEmpty: Boolean,
lazyListState: LazyListState,
hasNewItems: Boolean,
onScrollFinishedAt: (Int) -> Unit,
@@ -254,8 +261,8 @@ private fun BoxScope.TimelineScrollHelper(
}
}
- LaunchedEffect(isScrollFinished) {
- if (isScrollFinished) {
+ LaunchedEffect(isScrollFinished, isTimelineEmpty) {
+ if (isScrollFinished && !isTimelineEmpty) {
// Notify the parent composable about the first visible item index when scrolling finishes
onScrollFinishedAt(lazyListState.firstVisibleItemIndex)
}
@@ -310,7 +317,7 @@ private fun JumpToBottomButton(
@DayNightPreviews
@Composable
-fun TimelineViewPreview(
+internal fun TimelineViewPreview(
@PreviewParameter(TimelineItemEventContentProvider::class) content: TimelineItemEventContent
) = ElementPreview {
val timelineItems = aTimelineItemList(content)
@@ -321,6 +328,7 @@ fun TimelineViewPreview(
onUserDataClicked = {},
onMessageLongClicked = {},
onReactionClicked = { _, _ -> },
+ onReactionLongClicked = { _, _ -> },
onMoreReactionsClicked = {},
onSwipeToReply = {},
)
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/EmojiPicker.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/EmojiPicker.kt
index 6e121685f2..45fd5bf186 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/EmojiPicker.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/EmojiPicker.kt
@@ -17,6 +17,7 @@
package io.element.android.features.messages.impl.timeline.components
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
@@ -31,6 +32,7 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
@@ -39,6 +41,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
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 com.vanniktech.emoji.Emoji
@@ -48,19 +51,22 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.theme.ElementTheme
+import kotlinx.collections.immutable.ImmutableSet
+import kotlinx.collections.immutable.persistentSetOf
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun EmojiPicker(
onEmojiSelected: (Emoji) -> Unit,
+ selectedEmojis: ImmutableSet,
modifier: Modifier = Modifier,
) {
val coroutineScope = rememberCoroutineScope()
val emojiProvider = remember { GoogleEmojiProvider() }
val categories = remember { emojiProvider.categories }
- val pagerState = rememberPagerState()
+ val pagerState = rememberPagerState(pageCount = { emojiProvider.categories.size })
Column(modifier) {
TabRow(
selectedTabIndex = pagerState.currentPage,
@@ -82,7 +88,6 @@ fun EmojiPicker(
}
HorizontalPager(
- pageCount = categories.size,
state = pagerState,
modifier = Modifier.fillMaxWidth(),
) { index ->
@@ -91,12 +96,19 @@ fun EmojiPicker(
modifier = Modifier.fillMaxSize(),
columns = GridCells.Adaptive(minSize = 40.dp),
contentPadding = PaddingValues(vertical = 10.dp, horizontal = 16.dp),
- horizontalArrangement = Arrangement.SpaceEvenly,
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
items(category.emojis, key = { it.unicode }) { item ->
+ val backgroundColor = if (selectedEmojis.contains(item.unicode)) {
+ ElementTheme.colors.bgActionPrimaryRest
+ } else {
+ Color.Transparent
+ }
+
Box(
modifier = Modifier
.size(40.dp)
+ .background(backgroundColor, CircleShape)
.clickable(
enabled = true,
onClick = { onEmojiSelected(item) },
@@ -132,6 +144,7 @@ internal fun EmojiPickerDarkPreview() {
private fun ContentToPreview() {
EmojiPicker(
onEmojiSelected = {},
- modifier = Modifier.fillMaxWidth()
+ modifier = Modifier.fillMaxWidth(),
+ selectedEmojis = persistentSetOf("😀", "😄", "😃")
)
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt
index 446846db83..de7c050bd4 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt
@@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -152,7 +153,7 @@ private fun ContentToPreview(state: BubbleState) {
) {
MessageEventBubble(
state = state,
- interactionSource = MutableInteractionSource(),
+ interactionSource = remember { MutableInteractionSource() },
) {
// Render the state as a text to better understand the previews
Box(
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageStateEventContainer.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageStateEventContainer.kt
index 69c73a68e1..8a7ad0a61c 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageStateEventContainer.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageStateEventContainer.kt
@@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
@@ -81,13 +82,13 @@ private fun ContentToPreview() {
Column {
MessageStateEventContainer(
isHighlighted = false,
- interactionSource = MutableInteractionSource(),
+ interactionSource = remember { MutableInteractionSource() },
) {
Spacer(modifier = Modifier.size(width = 120.dp, height = 32.dp))
}
MessageStateEventContainer(
isHighlighted = true,
- interactionSource = MutableInteractionSource(),
+ interactionSource = remember { MutableInteractionSource() },
) {
Spacer(modifier = Modifier.size(width = 120.dp, height = 32.dp))
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt
index 20658c798e..930adf36cd 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt
@@ -17,9 +17,10 @@
package io.element.android.features.messages.impl.timeline.components
import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.border
-import androidx.compose.foundation.clickable
+import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
@@ -54,8 +55,10 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.theme.ElementTheme
@Composable
+@OptIn(ExperimentalFoundationApi::class)
fun MessagesReactionButton(
onClick: () -> Unit,
+ onLongClick: () -> Unit,
content: MessagesReactionsButtonContent,
modifier: Modifier = Modifier,
) {
@@ -82,7 +85,10 @@ fun MessagesReactionButton(
.padding(vertical = 2.dp, horizontal = 2.dp)
// Clip click indicator inside the outer border
.clip(RoundedCornerShape(corner = CornerSize(12.dp)))
- .clickable(onClick = onClick)
+ .combinedClickable(
+ onClick = onClick,
+ onLongClick = onLongClick
+ )
// Inner border, to highlight when selected
.border(BorderStroke(1.dp, borderColor), RoundedCornerShape(corner = CornerSize(12.dp)))
.background(buttonColor, RoundedCornerShape(corner = CornerSize(12.dp)))
@@ -107,6 +113,7 @@ sealed class MessagesReactionsButtonContent {
}
private val reactionEmojiLineHeight = 20.sp
+private val addEmojiSize = 16.dp
@Composable
private fun TextContent(
@@ -129,7 +136,8 @@ private fun IconContent(
contentDescription = stringResource(id = R.string.screen_room_timeline_add_reaction),
tint = ElementTheme.materialColors.secondary,
modifier = modifier
- .size(reactionEmojiLineHeight.toDp())
+ .size(addEmojiSize)
+
)
@Composable
@@ -162,7 +170,18 @@ private fun ReactionContent(
internal fun MessagesReactionButtonPreview(@PreviewParameter(AggregatedReactionProvider::class) reaction: AggregatedReaction) = ElementPreview {
MessagesReactionButton(
content = MessagesReactionsButtonContent.Reaction(reaction),
- onClick = {}
+ onClick = {},
+ onLongClick = {}
+ )
+}
+
+@DayNightPreviews
+@Composable
+internal fun MessagesAddReactionButtonPreview() = ElementPreview {
+ MessagesReactionButton(
+ content = MessagesReactionsButtonContent.Icon(Icons.Outlined.AddReaction),
+ onClick = {},
+ onLongClick = {}
)
}
@@ -170,13 +189,10 @@ internal fun MessagesReactionButtonPreview(@PreviewParameter(AggregatedReactionP
@Composable
internal fun MessagesReactionExtraButtonsPreview() = ElementPreview {
Row {
- MessagesReactionButton(
- content = MessagesReactionsButtonContent.Icon(Icons.Outlined.AddReaction),
- onClick = {}
- )
MessagesReactionButton(
content = MessagesReactionsButtonContent.Text("12 more"),
- onClick = {}
+ onClick = {},
+ onLongClick = {}
)
MessagesReactionButton(
content = MessagesReactionsButtonContent.Reaction(
@@ -184,7 +200,8 @@ internal fun MessagesReactionExtraButtonsPreview() = ElementPreview {
key = "A very long reaction with many characters that should be truncated"
)
),
- onClick = {}
+ onClick = {},
+ onLongClick = {}
)
}
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt
index f0a2c3473b..bc33aab2b7 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt
@@ -29,6 +29,7 @@ import androidx.compose.material.icons.filled.Error
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
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@@ -63,7 +64,7 @@ fun TimelineEventTimestampView(
onClick = onClick,
onLongClick = onLongClick,
indication = rememberRipple(bounded = false),
- interactionSource = MutableInteractionSource()
+ interactionSource = remember { MutableInteractionSource() }
)
} else {
Modifier
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt
index a74b37efc5..90d3e6cd8c 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt
@@ -112,6 +112,7 @@ fun TimelineItemEventRow(
inReplyToClick: (EventId) -> Unit,
onTimestampClicked: (TimelineItem.Event) -> Unit,
onReactionClick: (emoji: String, eventId: TimelineItem.Event) -> Unit,
+ onReactionLongClick: (emoji: String, eventId: TimelineItem.Event) -> Unit,
onMoreReactionsClick: (eventId: TimelineItem.Event) -> Unit,
onSwipeToReply: () -> Unit,
modifier: Modifier = Modifier
@@ -169,6 +170,7 @@ fun TimelineItemEventRow(
inReplyToClicked = ::inReplyToClicked,
onUserDataClicked = ::onUserDataClicked,
onReactionClicked = { emoji -> onReactionClick(emoji, event) },
+ onReactionLongClicked = { emoji -> onReactionLongClick(emoji, event) },
onMoreReactionsClicked = { onMoreReactionsClick(event) },
)
}
@@ -184,6 +186,7 @@ fun TimelineItemEventRow(
inReplyToClicked = ::inReplyToClicked,
onUserDataClicked = ::onUserDataClicked,
onReactionClicked = { emoji -> onReactionClick(emoji, event) },
+ onReactionLongClicked = { emoji -> onReactionLongClick(emoji, event) },
onMoreReactionsClicked = { onMoreReactionsClick(event) },
)
}
@@ -224,6 +227,7 @@ private fun TimelineItemEventRowContent(
inReplyToClicked: () -> Unit,
onUserDataClicked: () -> Unit,
onReactionClicked: (emoji: String) -> Unit,
+ onReactionLongClicked: (emoji: String) -> Unit,
onMoreReactionsClicked: (event: TimelineItem.Event) -> Unit,
modifier: Modifier = Modifier,
) {
@@ -292,6 +296,7 @@ private fun TimelineItemEventRowContent(
reactionsState = event.reactionsState,
isOutgoing = event.isMine,
onReactionClicked = onReactionClicked,
+ onReactionLongClicked = onReactionLongClicked,
onMoreReactionsClicked = { onMoreReactionsClicked(event) },
modifier = Modifier
.constrainAs(reactions) {
@@ -479,7 +484,7 @@ private fun ReplyToContent(
val paddings = if (attachmentThumbnailInfo != null) {
PaddingValues(start = 4.dp, end = 12.dp, top = 4.dp, bottom = 4.dp)
} else {
- PaddingValues(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 4.dp)
+ PaddingValues(horizontal = 12.dp, vertical = 4.dp)
}
Row(
modifier
@@ -517,42 +522,46 @@ private fun ReplyToContent(
}
}
-private fun attachmentThumbnailInfoForInReplyTo(inReplyTo: InReplyTo.Ready) =
- when (val type = inReplyTo.content.type) {
+private fun attachmentThumbnailInfoForInReplyTo(inReplyTo: InReplyTo.Ready): AttachmentThumbnailInfo? {
+ val messageContent = inReplyTo.content as? MessageContent ?: return null
+ return when (val type = messageContent.type) {
is ImageMessageType -> AttachmentThumbnailInfo(
thumbnailSource = type.info?.thumbnailSource,
- textContent = inReplyTo.content.body,
+ textContent = messageContent.body,
type = AttachmentThumbnailType.Image,
blurHash = type.info?.blurhash,
)
is VideoMessageType -> AttachmentThumbnailInfo(
thumbnailSource = type.info?.thumbnailSource,
- textContent = inReplyTo.content.body,
+ textContent = messageContent.body,
type = AttachmentThumbnailType.Video,
blurHash = type.info?.blurhash,
)
is FileMessageType -> AttachmentThumbnailInfo(
thumbnailSource = type.info?.thumbnailSource,
- textContent = inReplyTo.content.body,
+ textContent = messageContent.body,
type = AttachmentThumbnailType.File,
)
is LocationMessageType -> AttachmentThumbnailInfo(
- textContent = inReplyTo.content.body,
+ textContent = messageContent.body,
type = AttachmentThumbnailType.Location,
)
is AudioMessageType -> AttachmentThumbnailInfo(
- textContent = inReplyTo.content.body,
+ textContent = messageContent.body,
type = AttachmentThumbnailType.Audio,
)
else -> null
}
+}
@Composable
-private fun textForInReplyTo(inReplyTo: InReplyTo.Ready) =
- when (inReplyTo.content.type) {
+private fun textForInReplyTo(inReplyTo: InReplyTo.Ready): String {
+ val messageContent = inReplyTo.content as? MessageContent ?: return ""
+ return when (messageContent.type) {
is LocationMessageType -> stringResource(CommonStrings.common_shared_location)
- else -> inReplyTo.content.body
+ else -> messageContent.body
}
+}
@Preview
@Composable
@@ -584,6 +593,7 @@ private fun ContentToPreview() {
onUserDataClick = {},
inReplyToClick = {},
onReactionClick = { _, _ -> },
+ onReactionLongClick = { _, _ -> },
onMoreReactionsClick = {},
onTimestampClicked = {},
onSwipeToReply = {},
@@ -603,6 +613,7 @@ private fun ContentToPreview() {
onUserDataClick = {},
inReplyToClick = {},
onReactionClick = { _, _ -> },
+ onReactionLongClick = { _, _ -> },
onMoreReactionsClick = {},
onTimestampClicked = {},
onSwipeToReply = {},
@@ -649,6 +660,7 @@ private fun ContentToPreviewWithReply() {
onUserDataClick = {},
inReplyToClick = {},
onReactionClick = { _, _ -> },
+ onReactionLongClick = { _, _ -> },
onMoreReactionsClick = {},
onTimestampClicked = {},
onSwipeToReply = {},
@@ -669,6 +681,7 @@ private fun ContentToPreviewWithReply() {
onUserDataClick = {},
inReplyToClick = {},
onReactionClick = { _, _ -> },
+ onReactionLongClick = { _, _ -> },
onMoreReactionsClick = {},
onTimestampClicked = {},
onSwipeToReply = {},
@@ -725,6 +738,7 @@ private fun ContentTimestampToPreview(event: TimelineItem.Event) {
onUserDataClick = {},
inReplyToClick = {},
onReactionClick = { _, _ -> },
+ onReactionLongClick = { _, _ -> },
onMoreReactionsClick = {},
onTimestampClicked = {},
onSwipeToReply = {},
@@ -764,6 +778,7 @@ private fun ContentWithManyReactionsToPreview() {
onUserDataClick = {},
inReplyToClick = {},
onReactionClick = { _, _ -> },
+ onReactionLongClick = { _, _ -> },
onMoreReactionsClick = {},
onSwipeToReply = {},
onTimestampClicked = {},
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemReactionsLayout.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemReactionsLayout.kt
index 851389c6bd..01800f6348 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemReactionsLayout.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemReactionsLayout.kt
@@ -58,12 +58,12 @@ fun TimelineItemReactionsLayout(
SubcomposeLayout(modifier) { constraints ->
// Given the placeables and returns a structure representing
// how they should wrap on to multiple rows given the constraints max width.
- fun calculateRows(measurables: List): List> {
+ fun calculateRows(placeables: List): List> {
val rows = mutableListOf>()
var currentRow = mutableListOf()
var rowX = 0
- measurables.forEach { placeable ->
+ placeables.forEach { placeable ->
val horizontalSpacing = if (currentRow.isEmpty()) 0 else itemSpacing.toPx().toInt()
// If the current view does not fit on this row bump to the next
if (rowX + placeable.width > constraints.maxWidth) {
@@ -146,12 +146,18 @@ fun TimelineItemReactionsLayout(
}
}
- val reactionsPlaceables = subcompose(0, reactions).map { it.measure(constraints) }
+ var reactionsPlaceables = subcompose(0, reactions).map { it.measure(constraints) }
if (reactionsPlaceables.isEmpty()) {
return@SubcomposeLayout layoutRows(listOf())
}
- val addMorePlaceable = subcompose(1, addMoreButton).first().measure(constraints)
- val expandPlaceable = subcompose(2, expandButton).first().measure(constraints)
+ var expandPlaceable = subcompose(1, expandButton).first().measure(constraints)
+ // Enforce all reaction buttons have the same height
+ val maxHeight = (reactionsPlaceables + listOf(expandPlaceable)).maxOf { it.height }
+ val newConstrains = constraints.copy(minHeight = maxHeight)
+ reactionsPlaceables = subcompose(2, reactions).map { it.measure(newConstrains) }
+ expandPlaceable = subcompose(3, expandButton).first().measure(newConstrains)
+ val addMorePlaceable = subcompose(4, addMoreButton).first().measure(newConstrains)
+
// Calculate the layout of the rows with the reactions button and add more button
val reactionsAndAddMore = calculateRows(reactionsPlaceables + listOf(addMorePlaceable))
@@ -185,13 +191,15 @@ internal fun TimelineItemReactionsLayoutPreview() = ElementPreview {
content = MessagesReactionsButtonContent.Text(
text = stringResource(id = R.string.screen_room_timeline_less_reactions)
),
- onClick = { },
+ onClick = {},
+ onLongClick = {}
)
},
addMoreButton = {
MessagesReactionButton(
content = MessagesReactionsButtonContent.Icon(Icons.Outlined.AddReaction),
- onClick = {}
+ onClick = {},
+ onLongClick = {}
)
},
reactions = {
@@ -200,7 +208,8 @@ internal fun TimelineItemReactionsLayoutPreview() = ElementPreview {
content = MessagesReactionsButtonContent.Reaction(
it
),
- onClick = {}
+ onClick = {},
+ onLongClick = {}
)
}
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemReactionsView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemReactionsView.kt
index 962d3a2b2b..a5cdf4b6cc 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemReactionsView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemReactionsView.kt
@@ -42,71 +42,83 @@ fun TimelineItemReactions(
reactionsState: TimelineItemReactions,
isOutgoing: Boolean,
onReactionClicked: (emoji: String) -> Unit,
+ onReactionLongClicked: (emoji: String) -> Unit,
onMoreReactionsClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
var expanded: Boolean by rememberSaveable { mutableStateOf(false) }
-
- // In LTR languages we want an incoming message's reactions to be LRT and outgoing to be RTL.
- // For RTL languages it should be the opposite.
- val reactionsLayoutDirection = if (!isOutgoing) LocalLayoutDirection.current
- else if (LocalLayoutDirection.current == LayoutDirection.Ltr)
- LayoutDirection.Rtl
- else
- LayoutDirection.Ltr
-
- CompositionLocalProvider(LocalLayoutDirection provides reactionsLayoutDirection) {
TimelineItemReactionsView(
modifier = modifier,
reactions = reactionsState.reactions,
expanded = expanded,
+ isOutgoing = isOutgoing,
onReactionClick = onReactionClicked,
+ onReactionLongClick = onReactionLongClicked,
onMoreReactionsClick = onMoreReactionsClicked,
onToggleExpandClick = { expanded = !expanded },
)
- }
}
@Composable
private fun TimelineItemReactionsView(
reactions: ImmutableList,
+ isOutgoing: Boolean,
expanded: Boolean,
onReactionClick: (emoji: String) -> Unit,
+ onReactionLongClick: (emoji: String) -> Unit,
onMoreReactionsClick: () -> Unit,
onToggleExpandClick: () -> Unit,
modifier: Modifier = Modifier
-) = TimelineItemReactionsLayout(
- modifier = modifier,
- itemSpacing = 4.dp,
- rowSpacing = 4.dp,
- expanded = expanded,
- expandButton = {
- MessagesReactionButton(
- content = MessagesReactionsButtonContent.Text(
- text = stringResource(id = if (expanded) R.string.screen_room_reactions_show_less else R.string.screen_room_reactions_show_more)
- ),
- onClick = onToggleExpandClick,
+) {
+ // In LTR languages we want an incoming message's reactions to be LRT and outgoing to be RTL.
+ // For RTL languages it should be the opposite.
+ val currentLayout = LocalLayoutDirection.current
+ val reactionsLayoutDirection = if (!isOutgoing) currentLayout
+ else if (currentLayout == LayoutDirection.Ltr)
+ LayoutDirection.Rtl
+ else
+ LayoutDirection.Ltr
+
+ return CompositionLocalProvider(LocalLayoutDirection provides reactionsLayoutDirection) {
+ TimelineItemReactionsLayout(
+ modifier = modifier,
+ itemSpacing = 4.dp,
+ rowSpacing = 4.dp,
+ expanded = expanded,
+ expandButton = {
+ MessagesReactionButton(
+ content = MessagesReactionsButtonContent.Text(
+ text = stringResource(id = if (expanded) R.string.screen_room_reactions_show_less else R.string.screen_room_reactions_show_more)
+ ),
+ onClick = onToggleExpandClick,
+ onLongClick = {}
+ )
+ },
+ addMoreButton = {
+ MessagesReactionButton(
+ content = MessagesReactionsButtonContent.Icon(Icons.Outlined.AddReaction),
+ onClick = onMoreReactionsClick,
+ onLongClick = {}
+ )
+ },
+ reactions = {
+ reactions.forEach { reaction ->
+ CompositionLocalProvider(LocalLayoutDirection provides currentLayout) {
+ MessagesReactionButton(
+ content = MessagesReactionsButtonContent.Reaction(reaction = reaction),
+ onClick = { onReactionClick(reaction.key) },
+ onLongClick = { onReactionLongClick(reaction.key) }
+ )
+ }
+ }
+ }
)
- },
- addMoreButton = {
- MessagesReactionButton(
- content = MessagesReactionsButtonContent.Icon(Icons.Outlined.AddReaction),
- onClick = onMoreReactionsClick
- )
- },
- reactions = {
- reactions.forEach { reaction ->
- MessagesReactionButton(
- content = MessagesReactionsButtonContent.Reaction(reaction = reaction),
- onClick = { onReactionClick(reaction.key) }
- )
- }
}
-)
+}
@DayNightPreviews
@Composable
-fun TimelineItemReactionsViewPreview() = ElementPreview {
+internal fun TimelineItemReactionsViewPreview() = ElementPreview {
ContentToPreview(
reactions = aTimelineItemReactions(count = 1).reactions
)
@@ -114,7 +126,7 @@ fun TimelineItemReactionsViewPreview() = ElementPreview {
@DayNightPreviews
@Composable
-fun TimelineItemReactionsViewFewPreview() = ElementPreview {
+internal fun TimelineItemReactionsViewFewPreview() = ElementPreview {
ContentToPreview(
reactions = aTimelineItemReactions(count = 3).reactions
)
@@ -122,7 +134,7 @@ fun TimelineItemReactionsViewFewPreview() = ElementPreview {
@DayNightPreviews
@Composable
-fun TimelineItemReactionsViewIncomingPreview() = ElementPreview {
+internal fun TimelineItemReactionsViewIncomingPreview() = ElementPreview {
ContentToPreview(
reactions = aTimelineItemReactions(count = 18).reactions
)
@@ -130,7 +142,7 @@ fun TimelineItemReactionsViewIncomingPreview() = ElementPreview {
@DayNightPreviews
@Composable
-fun TimelineItemReactionsViewOutgoingPreview() = ElementPreview {
+internal fun TimelineItemReactionsViewOutgoingPreview() = ElementPreview {
ContentToPreview(
reactions = aTimelineItemReactions(count = 18).reactions,
isOutgoing = true
@@ -148,6 +160,7 @@ private fun ContentToPreview(
),
isOutgoing = isOutgoing,
onReactionClicked = {},
+ onReactionLongClicked = {},
onMoreReactionsClicked = {},
)
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt
index 70c2a7dc10..d817ec0cd4 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt
@@ -57,7 +57,8 @@ fun CustomReactionBottomSheet(
) {
EmojiPicker(
onEmojiSelected = ::onEmojiSelectedDismiss,
- modifier = Modifier.fillMaxSize()
+ modifier = Modifier.fillMaxSize(),
+ selectedEmojis = state.selectedEmoji,
)
}
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionEvents.kt
index b7c210553e..a0d69df372 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionEvents.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionEvents.kt
@@ -16,8 +16,8 @@
package io.element.android.features.messages.impl.timeline.components.customreaction
-import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.features.messages.impl.timeline.model.TimelineItem
sealed interface CustomReactionEvents {
- data class UpdateSelectedEvent(val eventId: EventId?) : CustomReactionEvents
+ data class UpdateSelectedEvent(val event: TimelineItem.Event?) : CustomReactionEvents
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt
index 0a23d42085..f094f2dbc6 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt
@@ -21,22 +21,24 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.libraries.architecture.Presenter
-import io.element.android.libraries.matrix.api.core.EventId
+import kotlinx.collections.immutable.toImmutableSet
import javax.inject.Inject
class CustomReactionPresenter @Inject constructor() : Presenter {
@Composable
override fun present(): CustomReactionState {
- var selectedEventId by remember { mutableStateOf(null) }
+ var selectedEvent by remember { mutableStateOf(null) }
fun handleEvents(event: CustomReactionEvents) {
when (event) {
- is CustomReactionEvents.UpdateSelectedEvent -> selectedEventId = event.eventId
+ is CustomReactionEvents.UpdateSelectedEvent -> selectedEvent = event.event
}
}
- return CustomReactionState(selectedEventId = selectedEventId, eventSink = ::handleEvents)
+ val selectedEmoji = selectedEvent?.reactionsState?.reactions?.mapNotNull { if(it.isHighlighted) it.key else null }.orEmpty().toImmutableSet()
+ return CustomReactionState(selectedEventId = selectedEvent?.eventId, selectedEmoji = selectedEmoji, eventSink = ::handleEvents)
}
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionState.kt
index 6c0c7f3599..9de1642dff 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionState.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionState.kt
@@ -17,8 +17,10 @@
package io.element.android.features.messages.impl.timeline.components.customreaction
import io.element.android.libraries.matrix.api.core.EventId
+import kotlinx.collections.immutable.ImmutableSet
data class CustomReactionState(
val selectedEventId: EventId?,
+ val selectedEmoji: ImmutableSet,
val eventSink: (CustomReactionEvents) -> Unit,
)
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt
index 3df45eb760..d53e3f1e5b 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt
@@ -25,6 +25,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
+import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
@@ -90,5 +91,10 @@ fun TimelineItemEventContentView(
content = content,
modifier = modifier
)
+ is TimelineItemPollContent -> TimelineItemPollView(
+ content = content,
+ onAnswerSelected = {},
+ modifier = modifier,
+ )
}
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt
new file mode 100644
index 0000000000..db3503be37
--- /dev/null
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.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.features.messages.impl.timeline.components.event
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
+import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContentProvider
+import io.element.android.features.poll.api.ActivePollContentView
+import io.element.android.libraries.designsystem.preview.DayNightPreviews
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.matrix.api.poll.PollAnswer
+import kotlinx.collections.immutable.toImmutableList
+
+@Composable
+fun TimelineItemPollView(
+ content: TimelineItemPollContent,
+ onAnswerSelected: (PollAnswer) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ ActivePollContentView(
+ question = content.question,
+ answerItems = content.answerItems.toImmutableList(),
+ pollKind = content.pollKind,
+ onAnswerSelected = onAnswerSelected,
+ modifier = modifier,
+ )
+}
+
+@DayNightPreviews
+@Composable
+internal fun TimelineItemPollViewPreview(@PreviewParameter(TimelineItemPollContentProvider::class) content: TimelineItemPollContent) =
+ ElementPreview {
+ TimelineItemPollView(
+ content = content,
+ onAnswerSelected = {},
+ )
+ }
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt
index 65be8f44e0..9066c88182 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt
@@ -125,7 +125,7 @@ internal fun TimelineItemTextViewDarkPreview(@PreviewParameter(TimelineItemTextB
fun ContentToPreview(content: TimelineItemTextBasedContent) {
TimelineItemTextView(
content = content,
- interactionSource = MutableInteractionSource(),
+ interactionSource = remember { MutableInteractionSource() },
extraPadding = ExtraPadding(nbChars = 8),
)
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/group/GroupHeaderView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/group/GroupHeaderView.kt
index a631e34266..ce853b84b5 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/group/GroupHeaderView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/group/GroupHeaderView.kt
@@ -91,12 +91,12 @@ fun GroupHeaderView(
@Preview
@Composable
-fun GroupHeaderViewLightPreview() =
+internal fun GroupHeaderViewLightPreview() =
ElementPreviewLight { ContentToPreview() }
@Preview
@Composable
-fun GroupHeaderViewDarkPreview() =
+internal fun GroupHeaderViewDarkPreview() =
ElementPreviewDark { ContentToPreview() }
@Composable
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/html/HtmlDocument.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/html/HtmlDocument.kt
index ecb81a4367..5d07db73a5 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/html/HtmlDocument.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/html/HtmlDocument.kt
@@ -29,6 +29,7 @@ import androidx.compose.material3.ColorScheme
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
@@ -599,5 +600,5 @@ internal fun HtmlDocumentDarkPreview(@PreviewParameter(DocumentProvider::class)
@Composable
private fun ContentToPreview(document: Document) {
- HtmlDocument(document, MutableInteractionSource())
+ HtmlDocument(document, remember { MutableInteractionSource() })
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryEvents.kt
new file mode 100644
index 0000000000..fdf94f52ce
--- /dev/null
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryEvents.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.messages.impl.timeline.components.reactionsummary
+
+import io.element.android.features.messages.impl.timeline.model.AggregatedReaction
+import io.element.android.libraries.matrix.api.core.EventId
+
+sealed interface ReactionSummaryEvents {
+ object Clear : ReactionSummaryEvents
+ data class ShowReactionSummary(val eventId: EventId, val reactions: List, val selectedKey: String) : ReactionSummaryEvents
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryPresenter.kt
new file mode 100644
index 0000000000..456ac5f548
--- /dev/null
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryPresenter.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.features.messages.impl.timeline.components.reactionsummary
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.matrix.api.room.MatrixRoom
+import io.element.android.libraries.matrix.api.room.RoomMember
+import io.element.android.libraries.matrix.api.room.roomMembers
+import io.element.android.libraries.matrix.api.user.MatrixUser
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.toImmutableList
+import javax.inject.Inject
+
+class ReactionSummaryPresenter @Inject constructor(
+ private val room: MatrixRoom,
+) : Presenter {
+ @Composable
+ override fun present(): ReactionSummaryState {
+ LaunchedEffect(Unit) {
+ room.updateMembers()
+ }
+
+ val membersState by room.membersStateFlow.collectAsState()
+
+ val target: MutableState = remember {
+ mutableStateOf(null)
+ }
+ val targetWithAvatars = populateSenderAvatars(members = membersState.roomMembers().orEmpty().toImmutableList(), summary = target.value)
+
+ fun handleEvents(event: ReactionSummaryEvents) {
+ when (event) {
+ is ReactionSummaryEvents.ShowReactionSummary -> target.value = ReactionSummaryState.Summary(
+ reactions = event.reactions,
+ selectedKey = event.selectedKey,
+ selectedEventId = event.eventId
+ )
+ ReactionSummaryEvents.Clear -> target.value = null
+ }
+ }
+ return ReactionSummaryState(
+ target = targetWithAvatars.value,
+ eventSink = ::handleEvents
+ )
+ }
+
+ @Composable
+ private fun populateSenderAvatars(members: ImmutableList, summary: ReactionSummaryState.Summary?) = remember(summary) {
+ derivedStateOf {
+ summary?.let { summary ->
+ summary.copy(reactions = summary.reactions.map { reaction ->
+ reaction.copy(senders = reaction.senders.map { sender ->
+ val member = members.firstOrNull { it.userId == sender.senderId }
+ val user = MatrixUser(
+ userId = sender.senderId,
+ displayName = member?.displayName,
+ avatarUrl = member?.avatarUrl
+ )
+ sender.copy(user = user)
+ })
+ })
+ }
+ }
+ }
+
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryState.kt
new file mode 100644
index 0000000000..37e150320b
--- /dev/null
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryState.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.messages.impl.timeline.components.reactionsummary
+
+import io.element.android.features.messages.impl.timeline.model.AggregatedReaction
+import io.element.android.libraries.matrix.api.core.EventId
+
+data class ReactionSummaryState(
+ val target: Summary?,
+ val eventSink: (ReactionSummaryEvents) -> Unit
+){
+ data class Summary(
+ val reactions: List,
+ val selectedKey: String,
+ val selectedEventId: EventId
+ )
+}
+
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryStateProvider.kt
new file mode 100644
index 0000000000..d6642922bb
--- /dev/null
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryStateProvider.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.messages.impl.timeline.components.reactionsummary
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
+import io.element.android.libraries.matrix.api.core.EventId
+
+open class ReactionSummaryStateProvider : PreviewParameterProvider {
+ override val values = sequenceOf(anActionListState())
+}
+
+fun anActionListState(): ReactionSummaryState {
+ val reactions = aTimelineItemReactions(8, true).reactions
+ return ReactionSummaryState(
+ target = ReactionSummaryState.Summary(
+ reactions = reactions,
+ selectedKey = reactions[0].key,
+ selectedEventId = EventId("$1234"),
+ ),
+ eventSink = {}
+ )
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryView.kt
new file mode 100644
index 0000000000..a775fa1856
--- /dev/null
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryView.kt
@@ -0,0 +1,275 @@
+/*
+ * 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.messages.impl.timeline.components.reactionsummary
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.foundation.shape.CornerSize
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import io.element.android.features.messages.impl.timeline.model.AggregatedReaction
+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.DayNightPreviews
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
+import io.element.android.libraries.designsystem.theme.components.Surface
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.matrix.api.user.MatrixUser
+import io.element.android.libraries.matrix.ui.model.getAvatarData
+import io.element.android.libraries.theme.ElementTheme
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ReactionSummaryView(
+ state: ReactionSummaryState,
+ modifier: Modifier = Modifier,
+) {
+ val sheetState = rememberModalBottomSheetState()
+
+ fun onDismiss() {
+ state.eventSink(ReactionSummaryEvents.Clear)
+ }
+
+ if (state.target != null) {
+ ModalBottomSheet(
+ onDismissRequest = ::onDismiss,
+ sheetState = sheetState,
+ modifier = modifier
+ ) {
+ SheetContent(summary = state.target)
+ }
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun SheetContent(
+ summary: ReactionSummaryState.Summary,
+ modifier: Modifier = Modifier,
+) {
+ val animationScope = rememberCoroutineScope()
+ var selectedReactionKey: String by rememberSaveable { mutableStateOf(summary.selectedKey) }
+ val selectedReactionIndex: Int by remember {
+ derivedStateOf {
+ summary.reactions.indexOfFirst { it.key == selectedReactionKey }
+ }
+ }
+ val pagerState = rememberPagerState(initialPage = selectedReactionIndex, pageCount = { summary.reactions.size })
+ val reactionListState = rememberLazyListState()
+
+ LaunchedEffect(pagerState.currentPage) {
+ selectedReactionKey = summary.reactions[pagerState.currentPage].key
+ val visibleInfo = reactionListState.layoutInfo.visibleItemsInfo
+ if (selectedReactionIndex <= visibleInfo.first().index || selectedReactionIndex >= visibleInfo.last().index) {
+ reactionListState.animateScrollToItem(selectedReactionIndex)
+ }
+ }
+
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .fillMaxHeight()
+ ) {
+ LazyRow(
+ state = reactionListState,
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ contentPadding = PaddingValues(start = 12.dp, end = 12.dp, bottom = 12.dp)
+ ) {
+ items(summary.reactions) { reaction ->
+ AggregatedReactionButton(
+ reaction = reaction,
+ isHighlighted = selectedReactionKey == reaction.key,
+ onClick = {
+ selectedReactionKey = reaction.key
+ animationScope.launch {
+ pagerState.animateScrollToPage(selectedReactionIndex)
+ }
+ }
+ )
+ }
+ }
+ HorizontalPager(state = pagerState) { page ->
+ LazyColumn(modifier = Modifier.fillMaxHeight()) {
+ items(summary.reactions[page].senders) { sender ->
+
+ val user = sender.user ?: MatrixUser(userId = sender.senderId)
+
+ SenderRow(
+ avatarData = user.getAvatarData(AvatarSize.UserListItem),
+ name = user.displayName ?: user.userId.value,
+ userId = user.userId.value,
+ sentTime = sender.sentTime
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun AggregatedReactionButton(
+ reaction: AggregatedReaction,
+ isHighlighted: Boolean,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+
+ val buttonColor = if (isHighlighted) {
+ ElementTheme.colors.bgActionPrimaryRest
+ } else {
+ Color.Transparent
+ }
+
+ val textColor = if (isHighlighted) {
+ MaterialTheme.colorScheme.inversePrimary
+ } else {
+ MaterialTheme.colorScheme.primary
+ }
+
+ val roundedCornerShape = RoundedCornerShape(corner = CornerSize(percent = 50))
+ Surface(
+ modifier = modifier
+ .background(buttonColor, roundedCornerShape)
+ .clip(roundedCornerShape)
+ .clickable(onClick = onClick)
+ .padding(vertical = 8.dp, horizontal = 12.dp),
+ color = buttonColor
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier,
+ ) {
+ Text(
+ text = reaction.displayKey,
+ style = ElementTheme.typography.fontBodyMdRegular.copy(
+ fontSize = 20.sp,
+ lineHeight = 25.sp
+ ),
+ )
+ if (reaction.count > 1) {
+ Spacer(modifier = Modifier.width(4.dp))
+ Text(
+ text = reaction.count.toString(),
+ color = textColor,
+ style = ElementTheme.typography.fontBodyMdRegular.copy(
+ fontSize = 20.sp,
+ lineHeight = 25.sp
+ )
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun SenderRow(
+ avatarData: AvatarData,
+ name: String,
+ userId: String,
+ sentTime: String,
+ modifier: Modifier = Modifier,
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .heightIn(min = 56.dp)
+ .padding(start = 16.dp, top = 4.dp, end = 16.dp, bottom = 4.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Avatar(avatarData)
+ Column(
+ modifier = Modifier.padding(start = 12.dp),
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.Bottom
+ ) {
+ Text(
+ modifier = Modifier
+ .padding(end = 4.dp)
+ .weight(1f),
+ text = name,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ color = MaterialTheme.colorScheme.primary,
+ style = ElementTheme.typography.fontBodyMdRegular,
+ )
+ Text(
+ text = sentTime,
+ color = MaterialTheme.colorScheme.secondary,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = ElementTheme.typography.fontBodySmRegular,
+ )
+ }
+ Text(
+ text = userId,
+ color = MaterialTheme.colorScheme.secondary,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = ElementTheme.typography.fontBodySmRegular,
+ )
+ }
+ }
+}
+
+@DayNightPreviews
+@Composable
+internal fun SheetContentPreview(
+ @PreviewParameter(ReactionSummaryStateProvider::class) state: ReactionSummaryState
+) = ElementPreview {
+ SheetContent(summary = state.target as ReactionSummaryState.Summary)
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/diff/CacheInvalidator.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/diff/CacheInvalidator.kt
deleted file mode 100644
index 9aa3ab5e02..0000000000
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/diff/CacheInvalidator.kt
+++ /dev/null
@@ -1,56 +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.features.messages.impl.timeline.diff
-
-import androidx.recyclerview.widget.ListUpdateCallback
-import io.element.android.features.messages.impl.timeline.model.TimelineItem
-import io.element.android.features.messages.impl.timeline.util.invalidateLast
-import timber.log.Timber
-
-internal class CacheInvalidator(private val itemStatesCache: MutableList) :
- ListUpdateCallback {
-
- override fun onChanged(position: Int, count: Int, payload: Any?) {
- Timber.d("onChanged(position= $position, count= $count)")
- (position until position + count).forEach {
- // Invalidate cache
- itemStatesCache[it] = null
- }
- }
-
- override fun onMoved(fromPosition: Int, toPosition: Int) {
- Timber.d("onMoved(fromPosition= $fromPosition, toPosition= $toPosition)")
- val model = itemStatesCache.removeAt(fromPosition)
- itemStatesCache.add(toPosition, model)
- }
-
- override fun onInserted(position: Int, count: Int) {
- Timber.d("onInserted(position= $position, count= $count)")
- itemStatesCache.invalidateLast()
- repeat(count) {
- itemStatesCache.add(position, null)
- }
- }
-
- override fun onRemoved(position: Int, count: Int) {
- Timber.d("onRemoved(position= $position, count= $count)")
- itemStatesCache.invalidateLast()
- repeat(count) {
- itemStatesCache.removeAt(position)
- }
- }
-}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/diff/TimelineItemsCacheInvalidator.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/diff/TimelineItemsCacheInvalidator.kt
new file mode 100644
index 0000000000..a7a3bea00e
--- /dev/null
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/diff/TimelineItemsCacheInvalidator.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.features.messages.impl.timeline.diff
+
+import io.element.android.features.messages.impl.timeline.model.TimelineItem
+import io.element.android.libraries.androidutils.diff.DefaultDiffCacheInvalidator
+import io.element.android.libraries.androidutils.diff.DiffCacheInvalidator
+import io.element.android.libraries.androidutils.diff.MutableDiffCache
+
+/**
+ * [DiffCacheInvalidator] implementation for [TimelineItem].
+ * It uses [DefaultDiffCacheInvalidator] and invalidate the cache around the updated item so that those items are computed again.
+ * This is needed because a timeline item is computed based on the previous and next items.
+ */
+internal class TimelineItemsCacheInvalidator : DiffCacheInvalidator {
+
+ private val delegate = DefaultDiffCacheInvalidator()
+
+ override fun onChanged(position: Int, count: Int, cache: MutableDiffCache) {
+ delegate.onChanged(position, count, cache)
+ }
+
+ override fun onMoved(fromPosition: Int, toPosition: Int, cache: MutableDiffCache) {
+ delegate.onMoved(fromPosition, toPosition, cache)
+ }
+
+ override fun onInserted(position: Int, count: Int, cache: MutableDiffCache) {
+ cache.invalidateAround(position)
+ delegate.onInserted(position, count, cache)
+ }
+
+ override fun onRemoved(position: Int, count: Int, cache: MutableDiffCache) {
+ cache.invalidateAround(position)
+ delegate.onRemoved(position, count, cache)
+ }
+}
+
+/**
+ * Invalidate the cache around the given position.
+ * It invalidates the previous and next items.
+ */
+private fun MutableDiffCache<*>.invalidateAround(position: Int) {
+ if (position > 0) {
+ set(position - 1, null)
+ }
+ if (position < indices().last) {
+ set(position + 1, null)
+ }
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt
index aa9786c945..8c894bc99a 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt
@@ -19,13 +19,13 @@ package io.element.android.features.messages.impl.timeline.factories
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
-import androidx.recyclerview.widget.DiffUtil
-import io.element.android.features.messages.impl.timeline.diff.CacheInvalidator
-import io.element.android.features.messages.impl.timeline.diff.MatrixTimelineItemsDiffCallback
+import io.element.android.features.messages.impl.timeline.diff.TimelineItemsCacheInvalidator
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemEventFactory
import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory
import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper
import io.element.android.features.messages.impl.timeline.model.TimelineItem
+import io.element.android.libraries.androidutils.diff.DiffCacheUpdater
+import io.element.android.libraries.androidutils.diff.MutableListDiffCache
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import kotlinx.collections.immutable.ImmutableList
@@ -35,9 +35,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
-import timber.log.Timber
import javax.inject.Inject
-import kotlin.system.measureTimeMillis
class TimelineItemsFactory @Inject constructor(
private val dispatchers: CoroutineDispatchers,
@@ -46,13 +44,20 @@ class TimelineItemsFactory @Inject constructor(
private val timelineItemGrouper: TimelineItemGrouper,
) {
private val timelineItems = MutableStateFlow(persistentListOf())
- private val timelineItemsCache = arrayListOf()
-
- // Items from rust sdk, used for diffing
- private var matrixTimelineItems: List = emptyList()
private val lock = Mutex()
- private val cacheInvalidator = CacheInvalidator(timelineItemsCache)
+ private val diffCache = MutableListDiffCache()
+ private val diffCacheUpdater = DiffCacheUpdater(
+ diffCache = diffCache,
+ detectMoves = false,
+ cacheInvalidator = TimelineItemsCacheInvalidator()
+ ) { old, new ->
+ if (old is MatrixTimelineItem.Event && new is MatrixTimelineItem.Event) {
+ old.uniqueId == new.uniqueId
+ } else {
+ false
+ }
+ }
@Composable
fun collectItemsAsState(): State> {
@@ -63,15 +68,15 @@ class TimelineItemsFactory @Inject constructor(
timelineItems: List,
) = withContext(dispatchers.computation) {
lock.withLock {
- calculateAndApplyDiff(timelineItems)
+ diffCacheUpdater.updateWith(timelineItems)
buildAndEmitTimelineItemStates(timelineItems)
}
}
private suspend fun buildAndEmitTimelineItemStates(timelineItems: List) {
val newTimelineItemStates = ArrayList()
- for (index in timelineItemsCache.indices.reversed()) {
- val cacheItem = timelineItemsCache[index]
+ for (index in diffCache.indices().reversed()) {
+ val cacheItem = diffCache.get(index)
if (cacheItem == null) {
buildAndCacheItem(timelineItems, index)?.also { timelineItemState ->
newTimelineItemStates.add(timelineItemState)
@@ -84,21 +89,7 @@ class TimelineItemsFactory @Inject constructor(
this.timelineItems.emit(result)
}
- private fun calculateAndApplyDiff(newTimelineItems: List) {
- val timeToDiff = measureTimeMillis {
- val diffCallback =
- MatrixTimelineItemsDiffCallback(
- oldList = matrixTimelineItems,
- newList = newTimelineItems
- )
- val diffResult = DiffUtil.calculateDiff(diffCallback, false)
- matrixTimelineItems = newTimelineItems
- diffResult.dispatchUpdatesTo(cacheInvalidator)
- }
- Timber.v("Time to apply diff on new list of ${newTimelineItems.size} items: $timeToDiff ms")
- }
-
- private fun buildAndCacheItem(
+ private suspend fun buildAndCacheItem(
timelineItems: List,
index: Int
): TimelineItem? {
@@ -108,7 +99,7 @@ class TimelineItemsFactory @Inject constructor(
is MatrixTimelineItem.Virtual -> virtualItemFactory.create(currentTimelineItem)
MatrixTimelineItem.Other -> null
}
- timelineItemsCache[index] = timelineItemState
+ diffCache[index] = timelineItemState
return timelineItemState
}
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt
index eb6d0e45c0..b3b2c896c3 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt
@@ -22,6 +22,8 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventTimeline
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
+import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
+import io.element.android.libraries.matrix.api.timeline.item.event.PollEndContent
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
@@ -35,6 +37,8 @@ class TimelineItemContentFactory @Inject constructor(
private val messageFactory: TimelineItemContentMessageFactory,
private val redactedMessageFactory: TimelineItemContentRedactedFactory,
private val stickerFactory: TimelineItemContentStickerFactory,
+ private val pollFactory: TimelineItemContentPollFactory,
+ private val pollEndFactory: TimelineItemContentPollEndFactory,
private val utdFactory: TimelineItemContentUTDFactory,
private val roomMembershipFactory: TimelineItemContentRoomMembershipFactory,
private val profileChangeFactory: TimelineItemContentProfileChangeFactory,
@@ -43,7 +47,7 @@ class TimelineItemContentFactory @Inject constructor(
private val failedToParseStateFactory: TimelineItemContentFailedToParseStateFactory
) {
- fun create(eventTimelineItem: EventTimelineItem): TimelineItemEventContent {
+ suspend fun create(eventTimelineItem: EventTimelineItem): TimelineItemEventContent {
return when (val itemContent = eventTimelineItem.content) {
is FailedToParseMessageLikeContent -> failedToParseMessageFactory.create(itemContent)
is FailedToParseStateContent -> failedToParseStateFactory.create(itemContent)
@@ -53,6 +57,8 @@ class TimelineItemContentFactory @Inject constructor(
is RoomMembershipContent -> roomMembershipFactory.create(eventTimelineItem)
is StateContent -> stateFactory.create(eventTimelineItem)
is StickerContent -> stickerFactory.create(itemContent)
+ is PollContent -> pollFactory.create(itemContent)
+ is PollEndContent -> pollEndFactory.create(itemContent)
is UnableToDecryptContent -> utdFactory.create(itemContent)
is UnknownContent -> TimelineItemUnknownContent
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollEndFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollEndFactory.kt
new file mode 100644
index 0000000000..ff9eb837b6
--- /dev/null
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollEndFactory.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.features.messages.impl.timeline.factories.event
+
+import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
+import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
+import io.element.android.libraries.matrix.api.timeline.item.event.PollEndContent
+import javax.inject.Inject
+
+class TimelineItemContentPollEndFactory @Inject constructor() {
+
+ fun create(@Suppress("UNUSED_PARAMETER") content: PollEndContent): TimelineItemEventContent {
+ return TimelineItemUnknownContent
+ }
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt
new file mode 100644
index 0000000000..7c61466337
--- /dev/null
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.messages.impl.timeline.factories.event
+
+import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
+import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
+import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
+import io.element.android.features.poll.api.PollAnswerItem
+import io.element.android.libraries.featureflag.api.FeatureFlagService
+import io.element.android.libraries.featureflag.api.FeatureFlags
+import io.element.android.libraries.matrix.api.MatrixClient
+import io.element.android.libraries.matrix.api.poll.PollKind
+import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
+import javax.inject.Inject
+
+class TimelineItemContentPollFactory @Inject constructor(
+ private val matrixClient: MatrixClient,
+ private val featureFlagService: FeatureFlagService,
+) {
+
+ suspend fun create(content: PollContent): TimelineItemEventContent {
+ if (!featureFlagService.isFeatureEnabled(FeatureFlags.Polls)) return TimelineItemUnknownContent
+
+ // Todo Move this computation to the matrix rust sdk
+ val showResults = content.kind == PollKind.Disclosed && matrixClient.sessionId in content.votes.flatMap { it.value }
+ val pollVotesCount = content.votes.flatMap { it.value }.size
+ val userVotes = content.votes.filter { matrixClient.sessionId in it.value }.keys
+ val answerItems = content.answers.map { answer ->
+ val votesCount = content.votes[answer.id]?.size ?: 0
+ val progress = if (pollVotesCount > 0) votesCount.toFloat() / pollVotesCount.toFloat() else 0f
+ PollAnswerItem(
+ answer = answer,
+ isSelected = answer.id in userVotes,
+ isDisclosed = showResults,
+ votesCount = votesCount,
+ progress = progress,
+ )
+ }
+
+ return TimelineItemPollContent(
+ question = content.question,
+ answerItems = answerItems,
+ votes = content.votes,
+ pollKind = content.kind,
+ isDisclosed = showResults
+ )
+ }
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt
index 6bc5df1e79..4cb249af72 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt
@@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline.factories.event
import io.element.android.features.messages.impl.timeline.groups.canBeDisplayedInBubbleBlock
import io.element.android.features.messages.impl.timeline.model.AggregatedReaction
+import io.element.android.features.messages.impl.timeline.model.AggregatedReactionSender
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions
@@ -37,7 +38,7 @@ class TimelineItemEventFactory @Inject constructor(
private val matrixClient: MatrixClient,
) {
- fun create(
+ suspend fun create(
currentTimelineItem: MatrixTimelineItem.Event,
index: Int,
timelineItems: List,
@@ -90,14 +91,34 @@ class TimelineItemEventFactory @Inject constructor(
}
private fun MatrixTimelineItem.Event.computeReactionsState(): TimelineItemReactions {
- val aggregatedReactions = event.reactions.map {
+ val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
+ var aggregatedReactions = event.reactions.map { reaction ->
+ // Sort reactions within an aggregation by timestamp descending.
+ // This puts the most recent at the top, useful in cases like the
+ // reaction summary view or getting the most recent reaction.
AggregatedReaction(
- key = it.key,
- count = it.count.toInt(),
- isHighlighted = it.senderIds.contains(matrixClient.sessionId),
+ key = reaction.key,
+ currentUserId = matrixClient.sessionId,
+ senders = reaction.senders
+ .sortedByDescending{ it.timestamp }
+ .map {
+ val date = Date(it.timestamp)
+ AggregatedReactionSender(
+ senderId = it.senderId,
+ timestamp = date,
+ sentTime = timeFormatter.format(date),
+ )
+ }
)
}
- aggregatedReactions.sortedByDescending { it.count }
+ // Sort aggregated reactions by count and then timestamp ascending, using
+ // the most recent reaction in the aggregation(hence index 0).
+ // This appends new aggregations on the end of the reaction layout.
+ aggregatedReactions = aggregatedReactions
+ .sortedWith(
+ compareByDescending { it.count }
+ .thenBy { it.senders[0].timestamp }
+ )
return TimelineItemReactions(aggregatedReactions.toImmutableList())
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/Groupability.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/Groupability.kt
index 0b8baf692a..1d2dec09b7 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/Groupability.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/Groupability.kt
@@ -22,6 +22,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
+import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemProfileChangeContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRoomMembershipContent
@@ -33,6 +34,8 @@ import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
+import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
+import io.element.android.libraries.matrix.api.timeline.item.event.PollEndContent
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
@@ -55,6 +58,7 @@ internal fun TimelineItem.Event.canBeGrouped(): Boolean {
is TimelineItemVideoContent,
is TimelineItemAudioContent,
is TimelineItemLocationContent,
+ is TimelineItemPollContent,
TimelineItemRedactedContent,
TimelineItemUnknownContent -> false
is TimelineItemProfileChangeContent,
@@ -74,6 +78,8 @@ internal fun MatrixTimelineItem.Event.canBeDisplayedInBubbleBlock(): Boolean {
is MessageContent,
RedactedContent,
is StickerContent,
+ is PollContent,
+ is PollEndContent,
is UnableToDecryptContent -> true
is FailedToParseStateContent,
is ProfileChangeContent,
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/AggregatedReaction.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/AggregatedReaction.kt
index ba13896c06..59c52ed8cf 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/AggregatedReaction.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/AggregatedReaction.kt
@@ -17,6 +17,7 @@
package io.element.android.features.messages.impl.timeline.model
import io.element.android.libraries.core.extensions.ellipsize
+import io.element.android.libraries.matrix.api.core.UserId
/**
* Length at which we ellipsize a reaction key for display
@@ -27,16 +28,15 @@ import io.element.android.libraries.core.extensions.ellipsize
private const val MAX_DISPLAY_CHARS = 16
/**
+ * @property currentUserId the ID of the currently logged in user
* @property key the full reaction key (e.g. "👍", "YES!")
- * @property count the number of users who reacted with this key
- * @property isHighlighted true if the reaction has (also) been sent by the current user.
+ * @property senders the list of users who sent the reactions
*/
data class AggregatedReaction(
+ val currentUserId: UserId,
val key: String,
- val count: Int,
- val isHighlighted: Boolean = false
+ val senders: List
) {
-
/**
* The key to be displayed on screen.
*
@@ -45,4 +45,18 @@ data class AggregatedReaction(
val displayKey: String by lazy {
key.ellipsize(MAX_DISPLAY_CHARS)
}
+
+ /**
+ * The number of users who reacted with this key.
+ */
+ val count: Int by lazy {
+ senders.count()
+ }
+
+ /**
+ * True if the reaction has (also) been sent by the current user.
+ */
+ val isHighlighted: Boolean by lazy {
+ senders.any { it.senderId.value == currentUserId.value }
+ }
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/AggregatedReactionProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/AggregatedReactionProvider.kt
index 148f565911..dcd6bb105c 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/AggregatedReactionProvider.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/AggregatedReactionProvider.kt
@@ -17,6 +17,9 @@
package io.element.android.features.messages.impl.timeline.model
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.libraries.matrix.api.core.UserId
+import java.text.DateFormat
+import java.util.Date
open class AggregatedReactionProvider : PreviewParameterProvider {
override val values: Sequence
@@ -29,11 +32,27 @@ open class AggregatedReactionProvider : PreviewParameterProvider
+ val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
+ val date = Date(1_689_061_264L)
+ add(
+ AggregatedReactionSender(
+ senderId = if (isHighlighted && index == 0) userId else UserId("@user$index:server.org"),
+ timestamp = date,
+ sentTime = timeFormatter.format(date),
+ )
+ )
+ }
+ }
+ return AggregatedReaction(
+ currentUserId = userId,
+ key = key,
+ senders = senders
+ )
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/AggregatedReactionSender.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/AggregatedReactionSender.kt
new file mode 100644
index 0000000000..276ee0b266
--- /dev/null
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/AggregatedReactionSender.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.messages.impl.timeline.model
+
+import io.element.android.libraries.matrix.api.core.UserId
+import io.element.android.libraries.matrix.api.user.MatrixUser
+import java.util.Date
+
+data class AggregatedReactionSender(
+ val senderId: UserId,
+ val timestamp: Date,
+ val sentTime: String,
+ val user: MatrixUser? = null
+)
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt
index 0ff67e481f..02837bd6b4 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt
@@ -33,3 +33,21 @@ fun TimelineItemEventContent.canBeCopied(): Boolean =
is TimelineItemRedactedContent -> true
else -> false
}
+
+/**
+ * Return true if user can react (i.e. send a reaction) on the event content.
+ */
+fun TimelineItemEventContent.canReact(): Boolean =
+ when (this) {
+ is TimelineItemTextBasedContent,
+ is TimelineItemAudioContent,
+ is TimelineItemEncryptedContent,
+ is TimelineItemFileContent,
+ is TimelineItemImageContent,
+ is TimelineItemLocationContent,
+ is TimelineItemPollContent,
+ is TimelineItemVideoContent -> true
+ is TimelineItemStateContent,
+ is TimelineItemRedactedContent,
+ TimelineItemUnknownContent -> false
+ }
diff --git a/app/src/main/kotlin/io/element/android/x/initializer/TimberInitializer.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContent.kt
similarity index 52%
rename from app/src/main/kotlin/io/element/android/x/initializer/TimberInitializer.kt
rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContent.kt
index 5a641d75c6..b8a2fa8bca 100644
--- a/app/src/main/kotlin/io/element/android/x/initializer/TimberInitializer.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContent.kt
@@ -14,22 +14,18 @@
* limitations under the License.
*/
-package io.element.android.x.initializer
+package io.element.android.features.messages.impl.timeline.model.event
-import android.content.Context
-import androidx.startup.Initializer
-import io.element.android.features.rageshake.impl.logs.VectorFileLogger
-import io.element.android.x.BuildConfig
-import timber.log.Timber
+import io.element.android.features.poll.api.PollAnswerItem
+import io.element.android.libraries.matrix.api.core.UserId
+import io.element.android.libraries.matrix.api.poll.PollKind
-class TimberInitializer : Initializer {
-
- override fun create(context: Context) {
- if (BuildConfig.DEBUG) {
- Timber.plant(Timber.DebugTree())
- }
- Timber.plant(VectorFileLogger(context))
- }
-
- override fun dependencies(): List>> = emptyList()
+data class TimelineItemPollContent(
+ val question: String,
+ val answerItems: List,
+ val votes: Map>,
+ val pollKind: PollKind,
+ val isDisclosed: Boolean,
+) : TimelineItemEventContent {
+ override val type: String = "TimelineItemPollContent"
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt
new file mode 100644
index 0000000000..665d507ead
--- /dev/null
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.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.features.messages.impl.timeline.model.event
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.features.poll.api.aPollAnswerItemList
+import io.element.android.libraries.matrix.api.poll.PollKind
+
+open class TimelineItemPollContentProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ aTimelineItemPollContent(),
+ aTimelineItemPollContent().copy(isDisclosed = true),
+ )
+}
+
+fun aTimelineItemPollContent(): TimelineItemPollContent {
+ return TimelineItemPollContent(
+ pollKind = PollKind.Disclosed,
+ isDisclosed = false,
+ question = "What type of food should we have at the party?",
+ answerItems = aPollAnswerItemList(),
+ votes = emptyMap(),
+ )
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/MessageSummaryFormatterImpl.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/MessageSummaryFormatterImpl.kt
index 42c50bbd9d..2b35eeda37 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/MessageSummaryFormatterImpl.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/MessageSummaryFormatterImpl.kt
@@ -24,6 +24,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
+import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemProfileChangeContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
@@ -47,6 +48,7 @@ class MessageSummaryFormatterImpl @Inject constructor(
is TimelineItemLocationContent -> context.getString(CommonStrings.common_shared_location)
is TimelineItemEncryptedContent -> context.getString(CommonStrings.common_unable_to_decrypt)
is TimelineItemRedactedContent -> context.getString(CommonStrings.common_message_removed)
+ is TimelineItemPollContent, // Todo Polls: handle summary
is TimelineItemUnknownContent -> context.getString(CommonStrings.common_unsupported_event)
is TimelineItemImageContent -> context.getString(CommonStrings.common_image)
is TimelineItemVideoContent -> context.getString(CommonStrings.common_video)
diff --git a/features/messages/impl/src/main/res/values-de/translations.xml b/features/messages/impl/src/main/res/values-de/translations.xml
index 1486e35726..84c903844e 100644
--- a/features/messages/impl/src/main/res/values-de/translations.xml
+++ b/features/messages/impl/src/main/res/values-de/translations.xml
@@ -20,6 +20,6 @@
"Mehr anzeigen"
"Erneut senden"
"Ihre Nachricht konnte nicht gesendet werden"
- "Fehler bei der Verarbeitung von Medien zum Hochladen, bitte versuchen Sie es erneut."
+ "Fehler bei der Verarbeitung von Medien zum Hochladen, bitte versuche es erneut."
"Entfernen"
diff --git a/features/messages/impl/src/main/res/values-fr/translations.xml b/features/messages/impl/src/main/res/values-fr/translations.xml
index 276fc30c94..5f9f0223aa 100644
--- a/features/messages/impl/src/main/res/values-fr/translations.xml
+++ b/features/messages/impl/src/main/res/values-fr/translations.xml
@@ -5,7 +5,7 @@
- "%1$d changements dans la conversation"
- - "1 de plus"
+ - "%1$d de plus"
- "%1$d de plus"
"Appareil photo"
@@ -19,6 +19,16 @@
"Vous êtes seul dans ce chat"
"Message copié"
"Vous n‘avez pas le droit de poster dans ce salon"
+ "Autoriser les paramètres personnalisés"
+ "Activer cette option remplacera votre paramètre par défaut"
+ "Me notifier dans ce chat pour"
+ "paramètres généraux"
+ "Paramètre par défaut"
+ "Une erreur s’est produite lors du chargement des paramètres de notification."
+ "Impossible de restaurer le mode par défaut, veuillez réessayer."
+ "Impossible de régler le mode, veuillez réessayer."
+ "Tous les messages"
+ "Mentions et mots-clés uniquement"
"Afficher moins"
"Afficher plus"
"Renvoyer"
diff --git a/features/messages/impl/src/main/res/values-ru/translations.xml b/features/messages/impl/src/main/res/values-ru/translations.xml
new file mode 100644
index 0000000000..7870cbc331
--- /dev/null
+++ b/features/messages/impl/src/main/res/values-ru/translations.xml
@@ -0,0 +1,39 @@
+
+
+
+ - "%1$d изменение в комнате"
+ - "%1$d изменения в комнате"
+ - "%1$d изменений в комнате"
+
+ "Камера"
+ "Сделать фото"
+ "Записать видео"
+ "Вложение"
+ "Фото и Видео библиотека"
+ "Местоположение"
+ "В настоящее время история сообщений недоступна в этой комнате"
+ "Не удалось получить данные о пользователе"
+ "Хотите пригласить их снова?"
+ "Вы одни в этой комнате"
+ "Сообщение скопировано"
+ "У вас нет разрешения публиковать сообщения в этой комнате"
+ "Разрешить пользовательские настройки"
+ "Включение этого параметра отменяет настройки по умолчанию"
+ "Уведомить меня в этом чате"
+ "Вы можете изменить его в своем %1$s."
+ "Основные Настройки"
+ "Настройка по умолчанию"
+ "Произошла ошибка при загрузке настроек уведомлений."
+ "Не удалось восстановить режим по умолчанию, попробуйте еще раз."
+ "Не удалось настроить режим, попробуйте еще раз."
+ "Все сообщения"
+ "Только упоминания и ключевые слова"
+ "Показать меньше"
+ "Показать больше"
+ "Отправить снова"
+ "Не удалось отправить ваше сообщение"
+ "Добавить эмодзи"
+ "Показать меньше"
+ "Не удалось обработать медиафайл для загрузки, попробуйте еще раз."
+ "Удалить"
+
diff --git a/features/messages/impl/src/main/res/values-zh-rTW/translations.xml b/features/messages/impl/src/main/res/values-zh-rTW/translations.xml
new file mode 100644
index 0000000000..d7344e54ca
--- /dev/null
+++ b/features/messages/impl/src/main/res/values-zh-rTW/translations.xml
@@ -0,0 +1,26 @@
+
+
+
+ - "%1$d 個聊天室變更"
+
+
+ - "還有 %1$d 個"
+
+ "照相機"
+ "拍照"
+ "錄影"
+ "附件"
+ "位置"
+ "此聊天室只有您一個人"
+ "訊息已複製"
+ "您沒有權限在此聊天室傳送訊息"
+ "全域設定"
+ "預設"
+ "無法重設為預設模式,請再試一次。"
+ "無法設定模式,請再試一次。"
+ "所有訊息"
+ "只限提及與關鍵字"
+ "重傳"
+ "無法傳送您的訊息"
+ "移除"
+
diff --git a/features/messages/impl/src/main/res/values/localazy.xml b/features/messages/impl/src/main/res/values/localazy.xml
index 6303589aa2..c88beff81a 100644
--- a/features/messages/impl/src/main/res/values/localazy.xml
+++ b/features/messages/impl/src/main/res/values/localazy.xml
@@ -13,6 +13,7 @@
"Attachment"
"Photo & Video Library"
"Location"
+ "Poll"
"Message history is currently unavailable in this room"
"Could not retrieve user details"
"Would you like to invite them back?"
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt
index 2ff244621d..5fcd06a980 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt
@@ -35,6 +35,7 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
import io.element.android.features.messages.impl.timeline.TimelinePresenter
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter
+import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryPresenter
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
@@ -44,8 +45,11 @@ import io.element.android.features.messages.media.FakeLocalMediaFactory
import io.element.android.features.messages.utils.messagesummary.FakeMessageSummaryFormatter
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
+import io.element.android.libraries.architecture.Async
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.mimetype.MimeTypes
+import io.element.android.libraries.designsystem.components.avatar.AvatarData
+import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.media.MediaSource
@@ -65,6 +69,8 @@ import io.element.android.libraries.mediapickers.test.FakePickerProvider
import io.element.android.libraries.mediaupload.api.MediaSender
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
import io.element.android.libraries.textcomposer.MessageComposerMode
+import io.element.android.tests.testutils.consumeItemsUntilPredicate
+import io.element.android.tests.testutils.consumeItemsUntilTimeout
import io.element.android.tests.testutils.testCoroutineDispatchers
import io.mockk.mockk
import kotlinx.coroutines.test.TestScope
@@ -81,9 +87,16 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
- skipItems(1)
- val initialState = awaitItem()
+ val initialState = consumeItemsUntilTimeout().last()
assertThat(initialState.roomId).isEqualTo(A_ROOM_ID)
+ assertThat(initialState.roomName).isEqualTo(Async.Success(""))
+ assertThat(initialState.roomAvatar).isEqualTo(Async.Success(AvatarData(id = A_ROOM_ID.value, name = "", size = AvatarSize.TimelineRoom)))
+ assertThat(initialState.userHasPermissionToSendMessage).isTrue()
+ assertThat(initialState.userHasPermissionToRedact).isFalse()
+ assertThat(initialState.hasNetworkConnection).isTrue()
+ assertThat(initialState.snackbarMessage).isNull()
+ assertThat(initialState.inviteProgress).isEqualTo(Async.Uninitialized)
+ assertThat(initialState.showReinvitePrompt).isFalse()
}
}
@@ -132,7 +145,6 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
- skipItems(1)
val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Forward, aMessageEvent()))
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
@@ -177,7 +189,6 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
- skipItems(1)
val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent(eventId = null)))
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
@@ -314,7 +325,6 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
- skipItems(1)
val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.ReportContent, aMessageEvent()))
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
@@ -328,10 +338,10 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
- skipItems(1)
val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.Dismiss)
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
+
}
}
@@ -342,7 +352,6 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
- skipItems(1)
val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Developer, aMessageEvent()))
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
@@ -363,11 +372,15 @@ class MessagesPresenterTest {
assertThat(initialState.showReinvitePrompt).isFalse()
// When the input field is focused we show the alert
initialState.composerState.eventSink(MessageComposerEvents.FocusChanged(true))
- val focusedState = awaitItem()
+ val focusedState = consumeItemsUntilPredicate { state ->
+ state.showReinvitePrompt
+ }.last()
assertThat(focusedState.showReinvitePrompt).isTrue()
// If it's dismissed then we stop showing the alert
initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Cancel))
- val dismissedState = awaitItem()
+ val dismissedState = consumeItemsUntilPredicate { state ->
+ !state.showReinvitePrompt
+ }.last()
assertThat(dismissedState.showReinvitePrompt).isFalse()
}
}
@@ -419,9 +432,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
- skipItems(1)
- val initialState = awaitItem()
- skipItems(1)
+ val initialState = consumeItemsUntilTimeout().last()
initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite))
skipItems(1)
val loadingState = awaitItem()
@@ -448,9 +459,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
- skipItems(1)
- val initialState = awaitItem()
- skipItems(1)
+ val initialState = consumeItemsUntilTimeout().last()
initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite))
skipItems(1)
val loadingState = awaitItem()
@@ -469,9 +478,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
- skipItems(1)
- val initialState = awaitItem()
- skipItems(1)
+ val initialState = consumeItemsUntilTimeout().last()
initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite))
skipItems(1)
val loadingState = awaitItem()
@@ -497,15 +504,16 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
- skipItems(1)
- val initialState = awaitItem()
- skipItems(1)
+ val initialState = consumeItemsUntilTimeout().last()
initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite))
- skipItems(1)
- val loadingState = awaitItem()
+ val loadingState = consumeItemsUntilPredicate { state ->
+ state.inviteProgress.isLoading()
+ }.last()
assertThat(loadingState.inviteProgress.isLoading()).isTrue()
- val newState = awaitItem()
- assertThat(newState.inviteProgress.isFailure()).isTrue()
+ val failureState = consumeItemsUntilPredicate { state ->
+ state.inviteProgress.isFailure()
+ }.last()
+ assertThat(failureState.inviteProgress.isFailure()).isTrue()
}
}
@@ -532,8 +540,22 @@ class MessagesPresenterTest {
}.test {
// Default value
assertThat(awaitItem().userHasPermissionToSendMessage).isTrue()
- skipItems(2)
+ skipItems(1)
assertThat(awaitItem().userHasPermissionToSendMessage).isFalse()
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
+ @Test
+ fun `present - permission to redact`() = runTest {
+ val matrixRoom = FakeMatrixRoom(canRedact = true)
+ val presenter = createMessagePresenter(matrixRoom = matrixRoom)
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = consumeItemsUntilPredicate { it.userHasPermissionToRedact }.last()
+ assertThat(initialState.userHasPermissionToRedact).isTrue()
+ cancelAndIgnoreRemainingEvents()
}
}
@@ -563,6 +585,7 @@ class MessagesPresenterTest {
val buildMeta = aBuildMeta()
val actionListPresenter = ActionListPresenter(buildMeta = buildMeta)
val customReactionPresenter = CustomReactionPresenter()
+ val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom)
val retrySendMenuPresenter = RetrySendMenuPresenter(room = matrixRoom)
return MessagesPresenter(
room = matrixRoom,
@@ -570,6 +593,7 @@ class MessagesPresenterTest {
timelinePresenter = timelinePresenter,
actionListPresenter = actionListPresenter,
customReactionPresenter = customReactionPresenter,
+ reactionSummaryPresenter = reactionSummaryPresenter,
retrySendMenuPresenter = retrySendMenuPresenter,
networkMonitor = FakeNetworkMonitor(),
snackbarDispatcher = SnackbarDispatcher(),
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt
index a2baa9dff7..afef9f6730 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt
@@ -56,7 +56,7 @@ class ActionListPresenterTest {
}.test {
val initialState = awaitItem()
val messageEvent = aMessageEvent(isMine = true, content = TimelineItemRedactedContent)
- initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent))
+ initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
@@ -81,7 +81,7 @@ class ActionListPresenterTest {
}.test {
val initialState = awaitItem()
val messageEvent = aMessageEvent(isMine = false, content = TimelineItemRedactedContent)
- initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent))
+ initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
@@ -109,7 +109,7 @@ class ActionListPresenterTest {
isMine = false,
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
)
- initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent))
+ initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
@@ -130,6 +130,37 @@ class ActionListPresenterTest {
}
}
+ @Test
+ fun `present - compute for others message and can redact`() = runTest {
+ val presenter = anActionListPresenter(isBuildDebuggable = true)
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ val messageEvent = aMessageEvent(
+ isMine = false,
+ content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
+ )
+ initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, true))
+ val successState = awaitItem()
+ assertThat(successState.target).isEqualTo(
+ ActionListState.Target.Success(
+ messageEvent,
+ persistentListOf(
+ TimelineItemAction.Reply,
+ TimelineItemAction.Forward,
+ TimelineItemAction.Copy,
+ TimelineItemAction.Developer,
+ TimelineItemAction.ReportContent,
+ TimelineItemAction.Redact,
+ )
+ )
+ )
+ initialState.eventSink.invoke(ActionListEvents.Clear)
+ assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None)
+ }
+ }
+
@Test
fun `present - compute for my message`() = runTest {
val presenter = anActionListPresenter(isBuildDebuggable = true)
@@ -141,7 +172,7 @@ class ActionListPresenterTest {
isMine = true,
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
)
- initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent))
+ initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
@@ -174,7 +205,7 @@ class ActionListPresenterTest {
isMine = true,
content = aTimelineItemImageContent(),
)
- initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent))
+ initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
@@ -205,7 +236,7 @@ class ActionListPresenterTest {
isMine = true,
content = aTimelineItemStateEventContent(),
)
- initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent))
+ initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, false))
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
@@ -234,7 +265,7 @@ class ActionListPresenterTest {
isMine = true,
content = aTimelineItemStateEventContent(),
)
- initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent))
+ initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, false))
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
@@ -262,7 +293,7 @@ class ActionListPresenterTest {
isMine = true,
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
)
- initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent))
+ initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
@@ -299,10 +330,10 @@ class ActionListPresenterTest {
content = TimelineItemRedactedContent,
)
- initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent))
+ initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
assertThat(awaitItem().target).isInstanceOf(ActionListState.Target.Success::class.java)
- initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(redactedEvent))
+ initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(redactedEvent, false))
awaitItem().run {
assertThat(target).isEqualTo(ActionListState.Target.None)
assertThat(displayEmojiReactions).isFalse()
@@ -323,7 +354,7 @@ class ActionListPresenterTest {
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false),
)
- initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent))
+ initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
val successState = awaitItem()
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt
index fb8b3b1948..3608d9e80e 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt
@@ -94,6 +94,21 @@ class AttachmentsPreviewPresenterTest {
}
}
+ @Test
+ fun `present - dismissing the progress dialog stops media upload`() = runTest {
+ val presenter = anAttachmentsPreviewPresenter()
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
+ initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
+ assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
+ initialState.eventSink(AttachmentsPreviewEvents.ClearSendState)
+ assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
+ }
+ }
+
private fun anAttachmentsPreviewPresenter(
localMedia: LocalMedia = aLocalMedia(
uri = mockMediaUrl,
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt
index 638c5e0556..8fea1ae155 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt
@@ -21,6 +21,8 @@ import io.element.android.features.messages.impl.timeline.factories.event.Timeli
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentFailedToParseMessageFactory
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentFailedToParseStateFactory
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentMessageFactory
+import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentPollEndFactory
+import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentPollFactory
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentProfileChangeFactory
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentRedactedFactory
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentRoomMembershipFactory
@@ -35,6 +37,7 @@ import io.element.android.features.messages.impl.timeline.util.FileExtensionExtr
import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
+import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.tests.testutils.testCoroutineDispatchers
@@ -42,6 +45,7 @@ import kotlinx.coroutines.test.TestScope
internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory {
val timelineEventFormatter = aTimelineEventFormatter()
+ val matrixClient = FakeMatrixClient()
return TimelineItemsFactory(
dispatchers = testCoroutineDispatchers(),
eventItemFactory = TimelineItemEventFactory(
@@ -49,14 +53,16 @@ internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory {
messageFactory = TimelineItemContentMessageFactory(FakeFileSizeFormatter(), FileExtensionExtractorWithoutValidation()),
redactedMessageFactory = TimelineItemContentRedactedFactory(),
stickerFactory = TimelineItemContentStickerFactory(),
+ pollFactory = TimelineItemContentPollFactory(matrixClient, FakeFeatureFlagService()),
+ pollEndFactory = TimelineItemContentPollEndFactory(),
utdFactory = TimelineItemContentUTDFactory(),
roomMembershipFactory = TimelineItemContentRoomMembershipFactory(timelineEventFormatter),
profileChangeFactory = TimelineItemContentProfileChangeFactory(timelineEventFormatter),
stateFactory = TimelineItemContentStateFactory(timelineEventFormatter),
failedToParseMessageFactory = TimelineItemContentFailedToParseMessageFactory(),
- failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory()
+ failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory(),
),
- matrixClient = FakeMatrixClient(),
+ matrixClient = matrixClient,
),
virtualItemFactory = TimelineItemVirtualFactory(
daySeparatorFactory = TimelineItemDaySeparatorFactory(
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/forward/ForwardMessagesPresenterTests.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/forward/ForwardMessagesPresenterTests.kt
index 9d19932716..983b251df7 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/forward/ForwardMessagesPresenterTests.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/forward/ForwardMessagesPresenterTests.kt
@@ -24,12 +24,12 @@ import io.element.android.features.messages.impl.forward.ForwardMessagesEvents
import io.element.android.features.messages.impl.forward.ForwardMessagesPresenter
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.core.EventId
-import io.element.android.libraries.matrix.api.room.RoomSummary
+import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
-import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
import io.element.android.libraries.matrix.test.room.aRoomSummaryDetail
+import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.runTest
@@ -76,10 +76,10 @@ class ForwardMessagesPresenterTests {
@Test
fun `present - update query`() = runTest {
- val roomSummaryDataSource = FakeRoomSummaryDataSource().apply {
+ val roomListService = FakeRoomListService().apply {
postAllRooms(listOf(RoomSummary.Filled(aRoomSummaryDetail())))
}
- val client = FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
+ val client = FakeMatrixClient(roomListService = roomListService)
val presenter = aPresenter(client = client)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -166,11 +166,10 @@ class ForwardMessagesPresenterTests {
}
}
- private fun CoroutineScope.aPresenter(
+ private fun CoroutineScope.aPresenter(
eventId: EventId = AN_EVENT_ID,
fakeMatrixRoom: FakeMatrixRoom = FakeMatrixRoom(),
coroutineScope: CoroutineScope = this,
client: FakeMatrixClient = FakeMatrixClient(),
) = ForwardMessagesPresenter(eventId.value, fakeMatrixRoom, coroutineScope, client)
-
}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt
index 5c75c963e5..25b506eb7f 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt
@@ -500,6 +500,23 @@ class MessageComposerPresenterTest {
}
}
+ @Test
+ fun `present - CancelSendAttachment stops media upload`() = runTest {
+ val presenter = createPresenter(this)
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ skipItems(1)
+ val initialState = awaitItem()
+ initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles)
+ val sendingState = awaitItem()
+ assertThat(sendingState.showAttachmentSourcePicker).isFalse()
+ assertThat(sendingState.attachmentsState).isInstanceOf(AttachmentsState.Sending.Processing::class.java)
+ sendingState.eventSink(MessageComposerEvents.CancelSendAttachment)
+ assertThat(awaitItem().attachmentsState).isEqualTo(AttachmentsState.None)
+ }
+ }
+
private suspend fun ReceiveTurbine.backToNormalMode(state: MessageComposerState, skipCount: Int = 0) {
state.eventSink.invoke(MessageComposerEvents.CloseSpecialMode)
skipItems(skipCount)
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt
index 0dfe6fd53c..b4bb14f672 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt
@@ -24,8 +24,12 @@ import io.element.android.features.messages.fixtures.aTimelineItemsFactory
import io.element.android.features.messages.impl.timeline.TimelineEvents
import io.element.android.features.messages.impl.timeline.TimelinePresenter
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
+import io.element.android.features.messages.impl.timeline.model.TimelineItem
+import io.element.android.libraries.matrix.ui.components.aMatrixUserList
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
+import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction
+import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
@@ -37,6 +41,7 @@ import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
+import java.util.Date
class TimelinePresenterTest {
@Test
@@ -114,7 +119,7 @@ class TimelinePresenterTest {
}
@Test
- fun `present - on scroll finished will not send read receipt no event is before the index`() = runTest {
+ fun `present - on scroll finished will not send read receipt if no event is before the index`() = runTest {
val timeline = FakeMatrixTimeline(
initialTimelineItems = listOf(
MatrixTimelineItem.Event(0, anEventTimelineItem())
@@ -188,6 +193,61 @@ class TimelinePresenterTest {
}
}
+ @Test
+ fun `present - reaction ordering`() = runTest {
+ val timeline = FakeMatrixTimeline()
+ val presenter = createTimelinePresenter(timeline)
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ assertThat(initialState.hasNewItems).isFalse()
+ assertThat(initialState.timelineItems.size).isEqualTo(0)
+ val now = Date().time
+ val minuteInMilis = 60 * 1000
+ // Use index as a convenient value for timestamp
+ val (alice, bob, charlie) = aMatrixUserList().take(3).mapIndexed { i, user ->
+ ReactionSender(senderId = user.userId, timestamp = now + i * minuteInMilis)
+ }
+ val oneReaction = listOf(
+ EventReaction(
+ key = "❤️",
+ senders = listOf(alice, charlie)
+ ),
+ EventReaction(
+ key = "👍",
+ senders = listOf(alice, bob)
+ ),
+ EventReaction(
+ key = "🐶",
+ senders = listOf(charlie)
+ ),
+ )
+ timeline.updateTimelineItems {
+ listOf(MatrixTimelineItem.Event(0, anEventTimelineItem(reactions = oneReaction)))
+ }
+ skipItems(1)
+ val item = awaitItem().timelineItems.first()
+ assertThat(item).isInstanceOf(TimelineItem.Event::class.java)
+ val event = item as TimelineItem.Event
+ val reactions = event.reactionsState.reactions
+ assertThat(reactions.size).isEqualTo(3)
+
+ // Aggregated reactions are sorted by count first and then timestamp ascending(new ones tagged on the end)
+ assertThat(reactions[0].count).isEqualTo(2)
+ assertThat(reactions[0].key).isEqualTo("👍")
+ assertThat(reactions[0].senders[0].senderId).isEqualTo(bob.senderId)
+
+ assertThat(reactions[1].count).isEqualTo(2)
+ assertThat(reactions[1].key).isEqualTo("❤️")
+ assertThat(reactions[1].senders[0].senderId).isEqualTo(charlie.senderId)
+
+ assertThat(reactions[2].count).isEqualTo(1)
+ assertThat(reactions[2].key).isEqualTo("🐶")
+ assertThat(reactions[2].senders[0].senderId).isEqualTo(charlie.senderId)
+ }
+ }
+
private fun TestScope.createTimelinePresenter(
timeline: MatrixTimeline = FakeMatrixTimeline(),
timelineItemsFactory: TimelineItemsFactory = aTimelineItemsFactory()
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/customreaction/CustomReactionPresenterTests.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/customreaction/CustomReactionPresenterTests.kt
index 1c40483ffe..84628cedae 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/customreaction/CustomReactionPresenterTests.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/customreaction/CustomReactionPresenterTests.kt
@@ -20,6 +20,8 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
+import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
+import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvents
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter
import io.element.android.libraries.matrix.test.AN_EVENT_ID
@@ -38,11 +40,27 @@ class CustomReactionPresenterTests {
val initialState = awaitItem()
assertThat(initialState.selectedEventId).isNull()
- initialState.eventSink(CustomReactionEvents.UpdateSelectedEvent(AN_EVENT_ID))
+ initialState.eventSink(CustomReactionEvents.UpdateSelectedEvent(aTimelineItemEvent(eventId = AN_EVENT_ID)))
assertThat(awaitItem().selectedEventId).isEqualTo(AN_EVENT_ID)
initialState.eventSink(CustomReactionEvents.UpdateSelectedEvent(null))
assertThat(awaitItem().selectedEventId).isNull()
}
}
+
+ @Test
+ fun `present - handle selected emojis`() = runTest {
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ assertThat(initialState.selectedEventId).isNull()
+ val reactions = aTimelineItemReactions(count = 1, isHighlighted = true)
+ val key = reactions.reactions.first().key
+ initialState.eventSink(CustomReactionEvents.UpdateSelectedEvent(aTimelineItemEvent(eventId = AN_EVENT_ID, timelineItemReactions = reactions)))
+ val stateWithSelectedEmojis = awaitItem()
+ assertThat(stateWithSelectedEmojis.selectedEventId).isEqualTo(AN_EVENT_ID)
+ assertThat(stateWithSelectedEmojis.selectedEmoji).contains(key)
+ }
+ }
}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/reactionsummary/ReactionSummaryPresenterTests.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/reactionsummary/ReactionSummaryPresenterTests.kt
new file mode 100644
index 0000000000..0170878cb5
--- /dev/null
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/reactionsummary/ReactionSummaryPresenterTests.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.messages.timeline.components.reactionsummary
+
+import app.cash.molecule.RecompositionMode
+import app.cash.molecule.moleculeFlow
+import app.cash.turbine.test
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvents
+import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryPresenter
+import io.element.android.features.messages.impl.timeline.model.anAggregatedReaction
+import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
+import io.element.android.libraries.matrix.test.AN_AVATAR_URL
+import io.element.android.libraries.matrix.test.AN_EVENT_ID
+import io.element.android.libraries.matrix.test.A_USER_ID
+import io.element.android.libraries.matrix.test.A_USER_NAME
+import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
+import io.element.android.libraries.matrix.test.room.aRoomMember
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class ReactionSummaryPresenterTests {
+ private val aggregatedReaction = anAggregatedReaction(userId = A_USER_ID, key = "👍", isHighlighted = true)
+ private val roomMember = aRoomMember(userId = A_USER_ID, avatarUrl = AN_AVATAR_URL, displayName = A_USER_NAME)
+ private val summaryEvent = ReactionSummaryEvents.ShowReactionSummary(AN_EVENT_ID, listOf(aggregatedReaction), aggregatedReaction.key)
+ private val room = FakeMatrixRoom().apply {
+ givenRoomMembersState(MatrixRoomMembersState.Ready(listOf(roomMember)))
+ }
+ private val presenter = ReactionSummaryPresenter(room)
+
+ @Test
+ fun `present - handle showing and hiding the reaction summary`() = runTest {
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ assertThat(initialState.target).isEqualTo(null)
+
+ initialState.eventSink(summaryEvent)
+ assertThat(awaitItem().target).isNotNull()
+
+ initialState.eventSink(ReactionSummaryEvents.Clear)
+ assertThat(awaitItem().target).isNull()
+ }
+ }
+
+ @Test
+ fun `present - handle reaction summary content and avatars populated`() = runTest {
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ assertThat(initialState.target).isEqualTo(null)
+
+ initialState.eventSink(summaryEvent)
+ val reactions = awaitItem().target?.reactions
+ assertThat(reactions?.count()).isEqualTo(1)
+ assertThat(reactions?.first()?.key).isEqualTo("👍")
+ assertThat(reactions?.first()?.senders?.first()?.senderId).isEqualTo(A_USER_ID)
+ assertThat(reactions?.first()?.senders?.first()?.user?.userId).isEqualTo(A_USER_ID)
+ assertThat(reactions?.first()?.senders?.first()?.user?.avatarUrl).isEqualTo(AN_AVATAR_URL)
+ assertThat(reactions?.first()?.senders?.first()?.user?.displayName).isEqualTo(A_USER_NAME)
+ }
+ }
+
+}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/model/AggregatedReactionTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/model/AggregatedReactionTest.kt
index 0e1ccbd003..ce107f76aa 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/model/AggregatedReactionTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/model/AggregatedReactionTest.kt
@@ -16,19 +16,30 @@
package io.element.android.features.messages.timeline.model
-import io.element.android.features.messages.impl.timeline.model.AggregatedReaction
+import io.element.android.features.messages.impl.timeline.model.anAggregatedReaction
import org.junit.Assert.assertEquals
import org.junit.Test
class AggregatedReactionTest {
@Test
fun `reaction display key is shortened`() {
- val reaction = AggregatedReaction(
- key = "1234567890123456790",
- count = 1,
- isHighlighted = false
+ val reaction = anAggregatedReaction(
+ key = "1234567890123456790",
+ count = 1
)
assertEquals("1234567890123456…", reaction.displayKey)
}
+
+ @Test
+ fun `reaction count and isHighlighted are computed correctly`() {
+ val reaction = anAggregatedReaction(
+ key = "1234567890123456790",
+ count = 3,
+ isHighlighted = true
+ )
+
+ assertEquals(3, reaction.count)
+ assertEquals(true, reaction.isHighlighted)
+ }
}
diff --git a/features/networkmonitor/api/src/main/kotlin/io/element/android/features/networkmonitor/api/ui/ConnectivityIndicatorView.kt b/features/networkmonitor/api/src/main/kotlin/io/element/android/features/networkmonitor/api/ui/ConnectivityIndicatorView.kt
index bf05dbc5f5..855bf067dd 100644
--- a/features/networkmonitor/api/src/main/kotlin/io/element/android/features/networkmonitor/api/ui/ConnectivityIndicatorView.kt
+++ b/features/networkmonitor/api/src/main/kotlin/io/element/android/features/networkmonitor/api/ui/ConnectivityIndicatorView.kt
@@ -44,8 +44,10 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.stringResource
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.text.toDp
import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings
@@ -85,14 +87,14 @@ private fun Indicator(modifier: Modifier = Modifier) {
.statusBarsPadding()
.padding(vertical = 6.dp),
horizontalArrangement = Arrangement.Center,
- verticalAlignment = Alignment.Bottom,
+ verticalAlignment = Alignment.CenterVertically,
) {
val tint = MaterialTheme.colorScheme.primary
Image(
imageVector = Icons.Outlined.WifiOff,
contentDescription = null,
colorFilter = ColorFilter.tint(tint),
- modifier = Modifier.size(16.dp),
+ modifier = Modifier.size(16.sp.toDp()),
)
Spacer(modifier = Modifier.width(8.dp))
Text(
diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt
index 0651d9cc2e..1adfe6bd93 100644
--- a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt
+++ b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt
@@ -23,10 +23,8 @@ import androidx.compose.foundation.layout.fillMaxSize
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.material.icons.Icons
import androidx.compose.material.icons.filled.QrCode
-import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.BiasAlignment
@@ -42,9 +40,8 @@ import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMo
import io.element.android.libraries.designsystem.atomic.pages.OnBoardingPage
import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.preview.ElementPreview
-import io.element.android.libraries.designsystem.theme.aliasButtonText
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.IconSource
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.testtags.TestTags
@@ -144,46 +141,27 @@ private fun OnBoardingButtons(
}
if (state.canLoginWithQrCode) {
Button(
+ text = stringResource(id = R.string.screen_onboarding_sign_in_with_qr_code),
+ leadingIcon = IconSource.Vector(Icons.Default.QrCode),
onClick = onSignInWithQrCode,
- enabled = true,
- modifier = Modifier
- .fillMaxWidth()
- ) {
- Icon(
- imageVector = Icons.Default.QrCode, contentDescription = null,
- tint = MaterialTheme.colorScheme.onPrimary
- )
- Spacer(Modifier.width(14.dp))
- Text(
- text = stringResource(id = R.string.screen_onboarding_sign_in_with_qr_code),
- style = ElementTheme.typography.aliasButtonText,
- )
- }
+ modifier = Modifier.fillMaxWidth()
+ )
}
Button(
+ text = stringResource(id = signInButtonStringRes),
onClick = onSignIn,
- enabled = true,
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.onBoardingSignIn)
- ) {
- Text(
- text = stringResource(id = signInButtonStringRes),
- style = ElementTheme.typography.aliasButtonText,
- )
- }
+ )
if (state.canCreateAccount) {
OutlinedButton(
+ text = stringResource(id = R.string.screen_onboarding_sign_up),
onClick = onCreateAccount,
enabled = true,
modifier = Modifier
.fillMaxWidth()
- ) {
- Text(
- text = stringResource(id = R.string.screen_onboarding_sign_up),
- style = ElementTheme.typography.aliasButtonText,
- )
- }
+ )
}
Spacer(modifier = Modifier.height(16.dp))
}
diff --git a/features/onboarding/impl/src/main/res/values-de/translations.xml b/features/onboarding/impl/src/main/res/values-de/translations.xml
index e36fa31a2b..82e20c3509 100644
--- a/features/onboarding/impl/src/main/res/values-de/translations.xml
+++ b/features/onboarding/impl/src/main/res/values-de/translations.xml
@@ -4,7 +4,7 @@
"Mit QR-Code anmelden"
"Konto erstellen"
"Sicher kommunizieren und zusammenarbeiten"
- "Willkommen beim schnellsten Element jemals. Optimiert für Geschwindigkeit und Einfachheit."
+ "Willkommen beim schnellsten Element aller Zeiten. Optimiert für Geschwindigkeit und Einfachheit."
"Willkommen zur %1$s. Verbessert, für Geschwindigkeit und Einfachheit."
"Sei in deinem Element"
diff --git a/features/onboarding/impl/src/main/res/values-ru/translations.xml b/features/onboarding/impl/src/main/res/values-ru/translations.xml
new file mode 100644
index 0000000000..5c8c12c2b0
--- /dev/null
+++ b/features/onboarding/impl/src/main/res/values-ru/translations.xml
@@ -0,0 +1,10 @@
+
+
+ "Вход в систему вручную"
+ "Войти с помощью QR-кода"
+ "Создать учетную запись"
+ "Безопасное общение и совместная работа"
+ "Добро пожаловать в самый быстрый Element. Преимущество в скорости и простоте."
+ "Добро пожаловать в %1$s. Supercharged — это скорость и простота."
+ "Будь в своей стихии"
+
diff --git a/features/onboarding/impl/src/main/res/values-zh-rTW/translations.xml b/features/onboarding/impl/src/main/res/values-zh-rTW/translations.xml
new file mode 100644
index 0000000000..5150b50c9a
--- /dev/null
+++ b/features/onboarding/impl/src/main/res/values-zh-rTW/translations.xml
@@ -0,0 +1,8 @@
+
+
+ "手動登入"
+ "使用 QR code 登入"
+ "建立帳號"
+ "歡迎使用有史以來最快的 Element。速度超快,操作簡便。"
+ "得心應手"
+
diff --git a/features/poll/api/build.gradle.kts b/features/poll/api/build.gradle.kts
new file mode 100644
index 0000000000..be198ba740
--- /dev/null
+++ b/features/poll/api/build.gradle.kts
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+
+plugins {
+ id("io.element.android-compose-library")
+ alias(libs.plugins.ksp)
+}
+
+android {
+ namespace = "io.element.android.features.poll.api"
+}
+
+dependencies {
+ implementation(projects.libraries.architecture)
+ implementation(projects.libraries.designsystem)
+ implementation(projects.libraries.uiStrings)
+ implementation(libs.androidx.constraintlayout)
+ implementation(libs.androidx.constraintlayout.compose)
+ implementation(projects.libraries.matrix.api)
+
+ ksp(libs.showkase.processor)
+}
diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/ActivePollContentView.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/ActivePollContentView.kt
new file mode 100644
index 0000000000..587c3306b1
--- /dev/null
+++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/ActivePollContentView.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.features.poll.api
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.selection.selectableGroup
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.BarChart
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import io.element.android.libraries.designsystem.preview.DayNightPreviews
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.theme.components.Icon
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.matrix.api.poll.PollAnswer
+import io.element.android.libraries.matrix.api.poll.PollKind
+import io.element.android.libraries.theme.ElementTheme
+import io.element.android.libraries.ui.strings.CommonStrings
+import kotlinx.collections.immutable.ImmutableList
+
+@Composable
+fun ActivePollContentView(
+ question: String,
+ answerItems: ImmutableList,
+ pollKind: PollKind,
+ onAnswerSelected: (PollAnswer) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val showResults = answerItems.any { it.isSelected }
+ Column(
+ modifier = modifier
+ .selectableGroup()
+ .fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ ) {
+ Icon(imageVector = Icons.Default.BarChart, contentDescription = null)
+ Text(
+ text = question,
+ style = ElementTheme.typography.fontBodyLgMedium
+ )
+ }
+
+ answerItems.forEach { answerItem ->
+ PollAnswerView(
+ answerItem = answerItem,
+ onClick = { onAnswerSelected(answerItem.answer) }
+ )
+ }
+
+ val votesCount = answerItems.sumOf { it.votesCount }
+ when {
+ pollKind == PollKind.Undisclosed -> {
+ Text(
+ modifier = Modifier
+ .align(Alignment.Start)
+ .padding(start = 32.dp),
+ style = ElementTheme.typography.fontBodyXsRegular,
+ color = ElementTheme.colors.textSecondary,
+ text = stringResource(CommonStrings.common_poll_undisclosed_text),
+ )
+ }
+ showResults -> {
+ Text(
+ modifier = Modifier.align(Alignment.End),
+ style = ElementTheme.typography.fontBodyXsRegular,
+ color = ElementTheme.colors.textSecondary,
+ text = stringResource(CommonStrings.common_poll_total_votes, votesCount),
+ )
+ }
+ }
+ }
+}
+
+@DayNightPreviews
+@Composable
+internal fun ActivePollContentNoResultsPreview() = ElementPreview {
+ ActivePollContentView(
+ question = "What type of food should we have at the party?",
+ answerItems = aPollAnswerItemList(isDisclosed = false),
+ pollKind = PollKind.Undisclosed,
+ onAnswerSelected = { },
+ )
+}
+
+@DayNightPreviews
+@Composable
+internal fun ActivePollContentWithResultsPreview() = ElementPreview {
+ ActivePollContentView(
+ question = "What type of food should we have at the party?",
+ answerItems = aPollAnswerItemList(),
+ pollKind = PollKind.Disclosed,
+ onAnswerSelected = { },
+ )
+}
diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerItem.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerItem.kt
new file mode 100644
index 0000000000..24db33ad1f
--- /dev/null
+++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerItem.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.features.poll.api
+
+import io.element.android.libraries.matrix.api.poll.PollAnswer
+
+/**
+ * UI model for a [PollAnswer].
+ *
+ * @property answer the poll answer.
+ * @property isSelected whether the user has selected this answer.
+ * @property isDisclosed whether the votes for this answer should be disclosed.
+ * @property votesCount the number of votes for this answer.
+ * @property progress the percentage of votes for this answer.
+ */
+data class PollAnswerItem(
+ val answer: PollAnswer,
+ val isSelected: Boolean,
+ val isDisclosed: Boolean,
+ val votesCount: Int,
+ val progress: Float,
+)
diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerView.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerView.kt
new file mode 100644
index 0000000000..26fa6fbb71
--- /dev/null
+++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerView.kt
@@ -0,0 +1,125 @@
+/*
+ * 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.poll.api
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.selection.selectable
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.res.pluralStringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.unit.dp
+import androidx.constraintlayout.compose.ConstraintLayout
+import androidx.constraintlayout.compose.Dimension
+import androidx.constraintlayout.compose.Visibility
+import io.element.android.libraries.designsystem.preview.DayNightPreviews
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator
+import io.element.android.libraries.designsystem.theme.components.RadioButton
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.theme.ElementTheme
+import io.element.android.libraries.ui.strings.CommonPlurals
+
+@Suppress("DestructuringDeclarationWithTooManyEntries") // This is necessary to declare the constraints ids
+@Composable
+fun PollAnswerView(
+ answerItem: PollAnswerItem,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ ConstraintLayout(
+ modifier
+ .wrapContentHeight()
+ .fillMaxWidth()
+ .selectable(
+ selected = answerItem.isSelected,
+ onClick = onClick,
+ role = Role.RadioButton,
+ )
+ ) {
+ val (radioButton, answerText, votesText, progressBar) = createRefs()
+ RadioButton(
+ modifier = Modifier.constrainAs(radioButton) {
+ top.linkTo(answerText.top)
+ bottom.linkTo(answerText.bottom)
+ start.linkTo(parent.start)
+ end.linkTo(answerText.start)
+ },
+ selected = answerItem.isSelected,
+ onClick = null // null recommended for accessibility with screenreaders
+ )
+ Text(
+ modifier = Modifier.constrainAs(answerText) {
+ width = Dimension.fillToConstraints
+ top.linkTo(parent.top)
+ start.linkTo(radioButton.end, margin = 8.dp)
+ end.linkTo(votesText.start)
+ bottom.linkTo(progressBar.top)
+ },
+ text = answerItem.answer.text,
+ )
+ Text(
+ modifier = Modifier.constrainAs(votesText) {
+ start.linkTo(answerText.end)
+ end.linkTo(parent.end)
+ bottom.linkTo(answerText.bottom)
+ visibility = if (answerItem.isDisclosed) Visibility.Visible else Visibility.Gone
+ },
+ text = pluralStringResource(
+ id = CommonPlurals.common_poll_votes_count,
+ count = answerItem.votesCount,
+ answerItem.votesCount
+ ),
+ style = ElementTheme.typography.fontBodySmRegular,
+ color = ElementTheme.colors.textSecondary,
+ )
+ LinearProgressIndicator(
+ progress = answerItem.progress,
+ modifier = Modifier
+ .constrainAs(progressBar) {
+ start.linkTo(answerText.start)
+ end.linkTo(votesText.end)
+ top.linkTo(answerText.bottom, margin = 10.dp)
+ bottom.linkTo(parent.bottom)
+ width = Dimension.fillToConstraints
+ visibility = if (answerItem.isDisclosed) Visibility.Visible else Visibility.Gone
+
+ },
+ strokeCap = StrokeCap.Round,
+ )
+ }
+}
+
+@DayNightPreviews
+@Composable
+internal fun PollAnswerViewNoResultsPreview() = ElementPreview {
+ PollAnswerView(
+ answerItem = aPollAnswerItem(),
+ onClick = { },
+ )
+}
+
+@DayNightPreviews
+@Composable
+internal fun PollAnswerViewWithResultPreview() = ElementPreview {
+ PollAnswerView(
+ answerItem = aPollAnswerItem(isDisclosed = true),
+ onClick = { }
+ )
+}
diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerViewProvider.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerViewProvider.kt
new file mode 100644
index 0000000000..062d09fd88
--- /dev/null
+++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerViewProvider.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.poll.api
+
+import io.element.android.libraries.matrix.api.poll.PollAnswer
+import kotlinx.collections.immutable.persistentListOf
+
+fun aPollAnswerItemList(isDisclosed: Boolean = true) = persistentListOf(
+ aPollAnswerItem(
+ answer = PollAnswer("option_1", "Italian \uD83C\uDDEE\uD83C\uDDF9"),
+ isDisclosed = isDisclosed,
+ votesCount = 5,
+ progress = 0.5f
+ ),
+ aPollAnswerItem(
+ answer = PollAnswer("option_2", "Chinese \uD83C\uDDE8\uD83C\uDDF3"),
+ isDisclosed = isDisclosed,
+ votesCount = 0,
+ progress = 0f
+ ),
+ aPollAnswerItem(
+ answer = PollAnswer("option_3", "Brazilian \uD83C\uDDE7\uD83C\uDDF7"),
+ isDisclosed = isDisclosed,
+ isSelected = true,
+ votesCount = 1,
+ progress = 0.1f
+ ),
+ aPollAnswerItem(isDisclosed = isDisclosed),
+)
+
+fun aPollAnswerItem(
+ answer: PollAnswer = PollAnswer(
+ "option_4",
+ "French \uD83C\uDDEB\uD83C\uDDF7 But make it a very very very long option then this should just keep expanding"
+ ),
+ isSelected: Boolean = false,
+ isDisclosed: Boolean = true,
+ votesCount: Int = 4,
+ progress: Float = 0.4f,
+) = PollAnswerItem(
+ answer = answer,
+ isSelected = isSelected,
+ isDisclosed = isDisclosed,
+ votesCount = votesCount,
+ progress = progress
+)
diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollEntryPoint.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollEntryPoint.kt
new file mode 100644
index 0000000000..d8f2aed846
--- /dev/null
+++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollEntryPoint.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.poll.api
+
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.core.plugin.Plugin
+import io.element.android.libraries.architecture.FeatureEntryPoint
+
+interface PollEntryPoint : FeatureEntryPoint {
+
+ fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
+
+ interface NodeBuilder {
+ fun callback(callback: Callback): NodeBuilder
+ fun build(): Node
+ }
+
+ interface Callback : Plugin {
+ // Add your callbacks
+ }
+}
+
diff --git a/features/poll/impl/build.gradle.kts b/features/poll/impl/build.gradle.kts
new file mode 100644
index 0000000000..626a7d0f2c
--- /dev/null
+++ b/features/poll/impl/build.gradle.kts
@@ -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.
+ */
+
+// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed
+@Suppress("DSL_SCOPE_VIOLATION")
+plugins {
+ id("io.element.android-compose-library")
+ alias(libs.plugins.anvil)
+ alias(libs.plugins.ksp)
+ id("kotlin-parcelize")
+}
+
+android {
+ namespace = "io.element.android.features.poll.impl"
+}
+
+anvil {
+ generateDaggerFactories.set(true)
+}
+
+dependencies {
+ implementation(projects.anvilannotations)
+ anvil(projects.anvilcodegen)
+ api(projects.features.poll.api)
+ implementation(projects.libraries.core)
+ implementation(projects.libraries.architecture)
+ implementation(projects.libraries.matrix.api)
+ implementation(projects.libraries.matrixui)
+ implementation(projects.libraries.designsystem)
+
+ testImplementation(libs.test.junit)
+ testImplementation(libs.coroutines.test)
+ testImplementation(libs.molecule.runtime)
+ testImplementation(libs.test.truth)
+ testImplementation(libs.test.turbine)
+ testImplementation(projects.libraries.matrix.test)
+
+ ksp(libs.showkase.processor)
+}
diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/DefaultPollEntryPoint.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/DefaultPollEntryPoint.kt
new file mode 100644
index 0000000000..052c1bcd5f
--- /dev/null
+++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/DefaultPollEntryPoint.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.poll.impl
+
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.core.plugin.Plugin
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.features.poll.api.PollEntryPoint
+import io.element.android.libraries.architecture.createNode
+import io.element.android.libraries.di.AppScope
+import javax.inject.Inject
+
+@ContributesBinding(AppScope::class)
+class DefaultPollEntryPoint @Inject constructor() : PollEntryPoint {
+
+ override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): PollEntryPoint.NodeBuilder {
+ val plugins = ArrayList()
+
+ return object : PollEntryPoint.NodeBuilder {
+
+ override fun callback(callback: PollEntryPoint.Callback): PollEntryPoint.NodeBuilder {
+ plugins += callback
+ return this
+ }
+
+ override fun build(): Node {
+ return parentNode.createNode(buildContext, plugins)
+ }
+ }
+ }
+}
diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/PollFlowNode.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/PollFlowNode.kt
new file mode 100644
index 0000000000..9dfeebc692
--- /dev/null
+++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/PollFlowNode.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.poll.impl
+
+import android.os.Parcelable
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.bumble.appyx.core.composable.Children
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.core.plugin.Plugin
+import com.bumble.appyx.navmodel.backstack.BackStack
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import io.element.android.anvilannotations.ContributesNode
+import io.element.android.libraries.architecture.BackstackNode
+import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
+import io.element.android.libraries.architecture.createNode
+import io.element.android.libraries.di.SessionScope
+import kotlinx.parcelize.Parcelize
+
+@ContributesNode(SessionScope::class)
+class PollFlowNode @AssistedInject constructor(
+ @Assisted buildContext: BuildContext,
+ @Assisted plugins: List,
+) : BackstackNode(
+ backstack = BackStack(
+ initialElement = NavTarget.Root,
+ savedStateMap = buildContext.savedStateMap,
+ ),
+ buildContext = buildContext,
+ plugins = plugins,
+) {
+
+ sealed interface NavTarget : Parcelable {
+ @Parcelize
+ object Root : NavTarget
+ }
+
+ override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
+ return when (navTarget) {
+ NavTarget.Root -> {
+ createNode(buildContext)
+ }
+ }
+ }
+
+ @Composable
+ override fun View(modifier: Modifier) {
+ Children(
+ navModel = backstack,
+ modifier = modifier,
+ transitionHandler = rememberDefaultTransitionHandler(),
+ )
+ }
+}
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutView.kt
index b7c2663b72..81075969c6 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutView.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutView.kt
@@ -50,12 +50,12 @@ fun AboutView(
@Preview
@Composable
-fun AboutViewLightPreview(@PreviewParameter(AboutStateProvider::class) state: AboutState) =
+internal fun AboutViewLightPreview(@PreviewParameter(AboutStateProvider::class) state: AboutState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
-fun AboutViewDarkPreview(@PreviewParameter(AboutStateProvider::class) state: AboutState) =
+internal fun AboutViewDarkPreview(@PreviewParameter(AboutStateProvider::class) state: AboutState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsView.kt
index 165406c6f5..3ee7365122 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsView.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsView.kt
@@ -46,12 +46,12 @@ fun AnalyticsSettingsView(
@Preview
@Composable
-fun AnalyticsSettingsViewLightPreview(@PreviewParameter(AnalyticsSettingsStateProvider::class) state: AnalyticsSettingsState) =
+internal fun AnalyticsSettingsViewLightPreview(@PreviewParameter(AnalyticsSettingsStateProvider::class) state: AnalyticsSettingsState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
-fun AnalyticsSettingsViewDarkPreview(@PreviewParameter(AnalyticsSettingsStateProvider::class) state: AnalyticsSettingsState) =
+internal fun AnalyticsSettingsViewDarkPreview(@PreviewParameter(AnalyticsSettingsStateProvider::class) state: AnalyticsSettingsState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt
index 1a8216ff1b..010e17bd35 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt
@@ -110,6 +110,7 @@ class DeveloperSettingsPresenter @Inject constructor(
FeatureUiModel(
key = feature.key,
title = feature.title,
+ description = feature.description,
isEnabled = isEnabled
)
}
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt
index 7c5b5d91c8..23ea4faf86 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt
@@ -96,12 +96,12 @@ fun FeatureListContent(
@Preview
@Composable
-fun DeveloperSettingsViewLightPreview(@PreviewParameter(DeveloperSettingsStateProvider::class) state: DeveloperSettingsState) =
+internal fun DeveloperSettingsViewLightPreview(@PreviewParameter(DeveloperSettingsStateProvider::class) state: DeveloperSettingsState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
-fun DeveloperSettingsViewDarkPreview(@PreviewParameter(DeveloperSettingsStateProvider::class) state: DeveloperSettingsState) =
+internal fun DeveloperSettingsViewDarkPreview(@PreviewParameter(DeveloperSettingsStateProvider::class) state: DeveloperSettingsState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt
index 90eade31ab..4b589ad2b0 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt
@@ -24,8 +24,6 @@ import androidx.compose.material.icons.outlined.DeveloperMode
import androidx.compose.material.icons.outlined.Help
import androidx.compose.material.icons.outlined.InsertChart
import androidx.compose.material.icons.outlined.VerifiedUser
-import androidx.compose.material3.Snackbar
-import androidx.compose.material3.SnackbarHost
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@@ -39,8 +37,9 @@ import io.element.android.libraries.designsystem.components.preferences.Preferen
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.LargeHeightPreview
-import io.element.android.libraries.designsystem.theme.components.Divider
+import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.designsystem.utils.SnackbarHost
import io.element.android.libraries.designsystem.utils.rememberSnackbarHostState
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.MatrixUserProvider
@@ -65,13 +64,7 @@ fun PreferencesRootView(
modifier = modifier,
onBackPressed = onBackPressed,
title = stringResource(id = CommonStrings.common_settings),
- snackbarHost = {
- SnackbarHost(snackbarHostState) { data ->
- Snackbar(
- snackbarData = data,
- )
- }
- }
+ snackbarHost = { SnackbarHost(snackbarHostState) }
) {
UserPreferences(state.myUser)
if (state.showCompleteVerification) {
@@ -80,7 +73,7 @@ fun PreferencesRootView(
icon = Icons.Outlined.VerifiedUser,
onClick = onVerifyClicked,
)
- Divider()
+ HorizontalDivider()
}
if (state.showAnalyticsSettings) {
PreferenceText(
@@ -102,7 +95,7 @@ fun PreferencesRootView(
if (state.showDeveloperSettings) {
DeveloperPreferencesView(onOpenDeveloperSettings)
}
- Divider()
+ HorizontalDivider()
LogoutPreferenceView(
state = state.logoutState,
)
@@ -129,12 +122,12 @@ fun DeveloperPreferencesView(onOpenDeveloperSettings: () -> Unit) {
@LargeHeightPreview
@Composable
-fun PreferencesRootViewLightPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) =
+internal fun PreferencesRootViewLightPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) =
ElementPreviewLight { ContentToPreview(matrixUser) }
@LargeHeightPreview
@Composable
-fun PreferencesRootViewDarkPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) =
+internal fun PreferencesRootViewDarkPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) =
ElementPreviewDark { ContentToPreview(matrixUser) }
@Composable
diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesView.kt b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesView.kt
index 73e04fb5d4..3cc2a8211a 100644
--- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesView.kt
+++ b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesView.kt
@@ -68,12 +68,12 @@ fun RageshakePreferencesView(
@Preview
@Composable
-fun RageshakePreferencesViewLightPreview(@PreviewParameter(RageshakePreferencesStateProvider::class) state: RageshakePreferencesState) =
+internal fun RageshakePreferencesViewLightPreview(@PreviewParameter(RageshakePreferencesStateProvider::class) state: RageshakePreferencesState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
-fun RageshakePreferencesViewDarkPreview(@PreviewParameter(RageshakePreferencesStateProvider::class) state: RageshakePreferencesState) =
+internal fun RageshakePreferencesViewDarkPreview(@PreviewParameter(RageshakePreferencesStateProvider::class) state: RageshakePreferencesState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/reporter/BugReporter.kt b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/reporter/BugReporter.kt
index 0af13dcdda..99849ef1d4 100644
--- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/reporter/BugReporter.kt
+++ b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/reporter/BugReporter.kt
@@ -16,6 +16,8 @@
package io.element.android.features.rageshake.api.reporter
+import java.io.File
+
interface BugReporter {
/**
* Send a bug report.
@@ -43,4 +45,14 @@ interface BugReporter {
customFields: Map? = null,
listener: BugReporterListener?
)
+
+ /**
+ * Clean the log files if needed to avoid wasting disk space.
+ */
+ fun cleanLogDirectoryIfNeeded()
+
+ /**
+ * Provide the log directory.
+ */
+ fun logDirectory(): File
}
diff --git a/features/rageshake/api/src/main/res/values-ru/translations.xml b/features/rageshake/api/src/main/res/values-ru/translations.xml
new file mode 100644
index 0000000000..6cb17a3401
--- /dev/null
+++ b/features/rageshake/api/src/main/res/values-ru/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "При последнем использовании %1$s произошел сбой. Хотите поделиться отчетом о сбое?"
+ "Кажется, вы трясли телефон. Хотите открыть экран отчета об ошибке?"
+
diff --git a/features/rageshake/impl/build.gradle.kts b/features/rageshake/impl/build.gradle.kts
index 3283e3f37a..464d521689 100644
--- a/features/rageshake/impl/build.gradle.kts
+++ b/features/rageshake/impl/build.gradle.kts
@@ -32,6 +32,7 @@ anvil {
dependencies {
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
+ implementation(projects.services.toolbox.api)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.core)
implementation(projects.libraries.network)
diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt
index 74f3a13d13..32d45b29ff 100644
--- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt
+++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt
@@ -31,6 +31,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@@ -47,8 +48,8 @@ import io.element.android.libraries.designsystem.components.preferences.Preferen
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.debugPlaceholderBackground
-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.Button
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
@@ -97,6 +98,7 @@ fun BugReportView(
eventSink(BugReportEvents.SetDescription(it))
},
keyboardOptions = KeyboardOptions(
+ capitalization = KeyboardCapitalization.Sentences,
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Next
),
@@ -147,14 +149,13 @@ fun BugReportView(
// Submit
PreferenceRow {
Button(
+ text = stringResource(id = CommonStrings.action_send),
onClick = { eventSink(BugReportEvents.SendBugReport) },
enabled = state.submitEnabled,
modifier = Modifier
.fillMaxWidth()
.padding(top = 24.dp, bottom = 16.dp)
- ) {
- Text(text = stringResource(id = CommonStrings.action_send))
- }
+ )
}
}
@@ -176,11 +177,11 @@ fun BugReportView(
@Preview
@Composable
-fun BugReportViewLightPreview(@PreviewParameter(BugReportStateProvider::class) state: BugReportState) = ElementPreviewLight { ContentToPreview(state) }
+internal fun BugReportViewLightPreview(@PreviewParameter(BugReportStateProvider::class) state: BugReportState) = ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
-fun BugReportViewDarkPreview(@PreviewParameter(BugReportStateProvider::class) state: BugReportState) = ElementPreviewDark { ContentToPreview(state) }
+internal fun BugReportViewDarkPreview(@PreviewParameter(BugReportStateProvider::class) state: BugReportState) = ElementPreviewDark { ContentToPreview(state) }
@Composable
private fun ContentToPreview(state: BugReportState) {
diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandler.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandler.kt
index a5e7edf405..b71c8af372 100644
--- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandler.kt
+++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandler.kt
@@ -62,9 +62,9 @@ class VectorUncaughtExceptionHandler(
totalSize = info.totalMemory()
usedSize = totalSize - freeSize
}
- append("usedSize " + usedSize / 1048576L + " MB\n")
- append("freeSize " + freeSize / 1048576L + " MB\n")
- append("totalSize " + totalSize / 1048576L + " MB\n")
+ append("usedSize " + usedSize / 1_048_576L + " MB\n")
+ append("freeSize " + freeSize / 1_048_576L + " MB\n")
+ append("totalSize " + totalSize / 1_048_576L + " MB\n")
append("Thread: ")
append(thread.name)
append(", Exception: ")
diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt
index 65dc48aca8..a8491b5a74 100755
--- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt
+++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt
@@ -18,6 +18,7 @@ package io.element.android.features.rageshake.impl.reporter
import android.content.Context
import android.os.Build
+import android.text.format.DateUtils.DAY_IN_MILLIS
import androidx.core.net.toFile
import androidx.core.net.toUri
import com.squareup.anvil.annotations.ContributesBinding
@@ -27,10 +28,10 @@ import io.element.android.features.rageshake.api.reporter.BugReporterListener
import io.element.android.features.rageshake.api.reporter.ReportType
import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder
import io.element.android.features.rageshake.impl.R
-import io.element.android.features.rageshake.impl.logs.VectorFileLogger
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.data.tryOrNull
import io.element.android.libraries.core.extensions.toOnOff
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.mimetype.MimeTypes
@@ -38,7 +39,10 @@ import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.libraries.sessionstorage.api.SessionStore
+import io.element.android.services.toolbox.api.systemclock.SystemClock
+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
@@ -65,6 +69,8 @@ class DefaultBugReporter @Inject constructor(
@ApplicationContext private val context: Context,
private val screenshotHolder: ScreenshotHolder,
private val crashDataStore: CrashDataStore,
+ private val coroutineScope: CoroutineScope,
+ private val systemClock: SystemClock,
private val coroutineDispatchers: CoroutineDispatchers,
private val okHttpClient: Provider,
private val userAgentProvider: UserAgentProvider,
@@ -87,6 +93,7 @@ class DefaultBugReporter @Inject constructor(
// filenames
private const val LOG_CAT_ERROR_FILENAME = "logcatError.log"
private const val LOG_CAT_FILENAME = "logcat.log"
+ private const val LOG_DIRECTORY_NAME = "logs"
// private const val KEY_REQUESTS_FILENAME = "keyRequests.log"
private const val BUFFER_SIZE = 1024 * 1024 * 50
@@ -158,9 +165,8 @@ class DefaultBugReporter @Inject constructor(
val gzippedFiles = ArrayList()
- val vectorFileLogger = VectorFileLogger.getFromTimber()
- if (withDevicesLogs && vectorFileLogger != null) {
- val files = vectorFileLogger.getLogFiles()
+ if (withDevicesLogs) {
+ val files = getLogFiles()
files.mapNotNullTo(gzippedFiles) { f ->
if (!mIsCancelled) {
compressFile(f)
@@ -168,6 +174,7 @@ class DefaultBugReporter @Inject constructor(
null
}
}
+ files.deleteAllExceptMostRecent()
}
if (!mIsCancelled && (withCrashLogs || withDevicesLogs)) {
@@ -458,6 +465,54 @@ class DefaultBugReporter @Inject constructor(
)
}
+ override fun logDirectory(): File {
+ return File(context.cacheDir, LOG_DIRECTORY_NAME)
+ }
+
+ override fun cleanLogDirectoryIfNeeded() {
+ coroutineScope.launch(coroutineDispatchers.io) {
+ // delete the log files older than 1 day, except the most recent one
+ deleteOldLogFiles(systemClock.epochMillis() - DAY_IN_MILLIS)
+ }
+ }
+
+ /**
+ * @return the files on the log directory.
+ */
+ private fun getLogFiles(): List {
+ return tryOrNull(
+ onError = { Timber.e(it, "## getLogFiles() failed") }
+ ) {
+ val logDirectory = logDirectory()
+ logDirectory.listFiles()?.toList()
+ }.orEmpty()
+ }
+
+ /**
+ * Delete the log files older than the given time except the most recent one.
+ * @param time the time in ms
+ */
+ private fun deleteOldLogFiles(time: Long) {
+ val logFiles = getLogFiles()
+ val oldLogFiles = logFiles.filter { it.lastModified() < time }
+ oldLogFiles.deleteAllExceptMostRecent()
+ }
+
+ /**
+ * Delete all the log files except the most recent one.
+ *
+ */
+ private fun List.deleteAllExceptMostRecent() {
+ if (size > 1) {
+ val mostRecentFile = maxByOrNull { it.lastModified() }
+ forEach { file ->
+ if (file != mostRecentFile) {
+ file.safeDelete()
+ }
+ }
+ }
+ }
+
// ==============================================================================================================
// Logcat management
// ==============================================================================================================
@@ -485,6 +540,10 @@ class DefaultBugReporter @Inject constructor(
Timber.e(error, "## saveLogCat() : fail to write logcat OOM")
} catch (e: Exception) {
Timber.e(e, "## saveLogCat() : fail to write logcat")
+ } finally {
+ if (logCatErrFile.exists()) {
+ logCatErrFile.safeDelete()
+ }
}
return null
diff --git a/features/rageshake/impl/src/main/res/values-de/translations.xml b/features/rageshake/impl/src/main/res/values-de/translations.xml
index a24318545d..b316d8b45e 100644
--- a/features/rageshake/impl/src/main/res/values-de/translations.xml
+++ b/features/rageshake/impl/src/main/res/values-de/translations.xml
@@ -1,14 +1,14 @@
"Bildschirmfoto anhängen"
- "Sie können mich kontaktieren, wenn Sie weitere Fragen haben"
+ "Ihr könnt mich kontaktieren, wenn ihr weitere Fragen habt"
"Kontaktiere mich"
"Bildschirmfoto bearbeiten"
"Beschreibe bitte den Fehler. Was hast du gemacht? Was hätte passieren sollen? Was ist passiert? Bitte beschreibe alles mit so vielen Details wie möglich."
"Beschreibe den Fehler…"
- "Wenn möglich, verfassen Sie die Beschreibung bitte auf Englisch."
+ "Wenn möglich, verfasse die Beschreibung bitte auf Englisch."
"Absturzprotokolle senden"
- "Senden Sie Protokolle, um zu helfen"
+ "Logs zulassen"
"Bildschirmfoto senden"
"Um zu überprüfen, ob alles wie vorgesehen funktioniert, werden Protokolle mit deiner Nachricht gesendet. Diese werden privat sein. Um nur Ihre Nachricht zu senden, schalte diese Einstellung aus."
"%1$s ist bei der letzten Verwendung abgestürzt. Möchtest du uns einen Absturzbericht senden?"
diff --git a/features/rageshake/impl/src/main/res/values-fr/translations.xml b/features/rageshake/impl/src/main/res/values-fr/translations.xml
index bf6ad2d215..53b95af6be 100644
--- a/features/rageshake/impl/src/main/res/values-fr/translations.xml
+++ b/features/rageshake/impl/src/main/res/values-fr/translations.xml
@@ -1,7 +1,7 @@
"Joindre une capture d\'écran"
- "Vous pouvez me contacter si vous avez des questions complémentaires"
+ "Vous pouvez me contacter si vous avez des questions complémentaires."
"Me contacter"
"Modifier la capture d\'écran"
"S\'il vous plait, veuillez décrire le bogue. Qu\'avez-vous fait ? À quoi vous attendiez-vous ? Que s\'est-il réellement passé. Veuillez ajouter le plus de détails possible."
diff --git a/features/rageshake/impl/src/main/res/values-ru/translations.xml b/features/rageshake/impl/src/main/res/values-ru/translations.xml
new file mode 100644
index 0000000000..8f05a3148d
--- /dev/null
+++ b/features/rageshake/impl/src/main/res/values-ru/translations.xml
@@ -0,0 +1,15 @@
+
+
+ "Приложить снимок экрана"
+ "Вы можете связаться со мной, если у Вас возникнут какие-либо дополнительные вопросы."
+ "Связаться со мной"
+ "Редактировать снимок экрана"
+ "Пожалуйста, опишите ошибку. Что вы сделали? Что вы ожидали, что произойдет? Что произошло на самом деле. Пожалуйста, опишите все как можно подробнее."
+ "Опишите ошибку…"
+ "Если возможно, пожалуйста, напишите описание на английском языке."
+ "Отправка журналов сбоев"
+ "Разрешить ведение журналов"
+ "Отправить снимок экрана"
+ "Чтобы убедиться, что все работает правильно, в сообщение будут включены журналы. Чтобы отправить сообщение без журналов, отключите эту настройку."
+ "При последнем использовании %1$s произошел сбой. Хотите поделиться отчетом о сбое?"
+
diff --git a/features/rageshake/impl/src/main/res/values-zh-rTW/translations.xml b/features/rageshake/impl/src/main/res/values-zh-rTW/translations.xml
new file mode 100644
index 0000000000..6e9eaabed3
--- /dev/null
+++ b/features/rageshake/impl/src/main/res/values-zh-rTW/translations.xml
@@ -0,0 +1,7 @@
+
+
+ "附上螢幕截圖"
+ "聯絡我"
+ "編輯螢幕截圖"
+ "傳送螢幕截圖"
+
diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/FakeBugReporter.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/FakeBugReporter.kt
index ac8940a1ac..82edaf563d 100644
--- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/FakeBugReporter.kt
+++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/FakeBugReporter.kt
@@ -21,6 +21,7 @@ import io.element.android.features.rageshake.api.reporter.BugReporterListener
import io.element.android.features.rageshake.api.reporter.ReportType
import io.element.android.libraries.matrix.test.A_FAILURE_REASON
import kotlinx.coroutines.delay
+import java.io.File
class FakeBugReporter(val mode: FakeBugReporterMode = FakeBugReporterMode.Success) : BugReporter {
override suspend fun sendBugReport(
@@ -55,6 +56,14 @@ class FakeBugReporter(val mode: FakeBugReporterMode = FakeBugReporterMode.Succes
delay(100)
listener?.onUploadSucceed(null)
}
+
+ override fun cleanLogDirectoryIfNeeded() {
+ // No op
+ }
+
+ override fun logDirectory(): File {
+ return File("fake")
+ }
}
enum class FakeBugReporterMode {
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt
index 63868c8227..51754ca6de 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt
@@ -17,13 +17,11 @@
package io.element.android.features.roomdetails.impl
import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
-import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@@ -49,6 +47,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.features.leaveroom.api.LeaveRoomView
@@ -68,7 +67,6 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.LargeHeightPreview
import io.element.android.libraries.designsystem.theme.components.DropdownMenu
import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem
-import io.element.android.libraries.designsystem.theme.components.DropdownMenuItemText
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
@@ -196,7 +194,7 @@ internal fun RoomDetailsTopBar(
onDismissRequest = { showMenu = false },
) {
DropdownMenuItem(
- text = { DropdownMenuItemText(stringResource(id = CommonStrings.action_edit)) },
+ text = { Text(stringResource(id = CommonStrings.action_edit)) },
onClick = {
// Explicitly close the menu before handling the action, as otherwise it stays open during the
// transition and renders really badly.
@@ -225,18 +223,30 @@ internal fun RoomHeaderSection(
roomAlias: String?,
modifier: Modifier = Modifier
) {
- Column(modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
- Box(modifier = Modifier.size(70.dp)) {
- Avatar(
- avatarData = AvatarData(roomId, roomName, avatarUrl, AvatarSize.RoomHeader),
- modifier = Modifier.fillMaxSize()
- )
- }
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Avatar(
+ avatarData = AvatarData(roomId, roomName, avatarUrl, AvatarSize.RoomHeader),
+ modifier = Modifier.size(70.dp)
+ )
Spacer(modifier = Modifier.height(24.dp))
- Text(roomName, style = ElementTheme.typography.fontHeadingLgBold)
+ Text(
+ text = roomName,
+ style = ElementTheme.typography.fontHeadingLgBold,
+ textAlign = TextAlign.Center,
+ )
if (roomAlias != null) {
Spacer(modifier = Modifier.height(6.dp))
- Text(roomAlias, style = ElementTheme.typography.fontBodyLgRegular, color = MaterialTheme.colorScheme.secondary)
+ Text(
+ text = roomAlias,
+ style = ElementTheme.typography.fontBodyLgRegular,
+ color = MaterialTheme.colorScheme.secondary,
+ textAlign = TextAlign.Center,
+ )
}
Spacer(Modifier.height(32.dp))
}
@@ -321,12 +331,12 @@ internal fun OtherActionsSection(onLeaveRoom: () -> Unit, modifier: Modifier = M
@LargeHeightPreview
@Composable
-fun RoomDetailsLightPreview(@PreviewParameter(RoomDetailsStateProvider::class) state: RoomDetailsState) =
+internal fun RoomDetailsLightPreview(@PreviewParameter(RoomDetailsStateProvider::class) state: RoomDetailsState) =
ElementPreviewLight { ContentToPreview(state) }
@LargeHeightPreview
@Composable
-fun RoomDetailsDarkPreview(@PreviewParameter(RoomDetailsStateProvider::class) state: RoomDetailsState) =
+internal fun RoomDetailsDarkPreview(@PreviewParameter(RoomDetailsStateProvider::class) state: RoomDetailsState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditView.kt
index 029f5ac5df..cd0cbf878e 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditView.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditView.kt
@@ -34,6 +34,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetValue
@@ -52,6 +53,7 @@ import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
@@ -66,7 +68,6 @@ import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
-import io.element.android.libraries.designsystem.theme.aliasButtonText
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Scaffold
@@ -113,17 +114,13 @@ fun RoomDetailsEditView(
navigationIcon = { BackButton(onClick = onBackPressed) },
actions = {
TextButton(
+ text = stringResource(CommonStrings.action_save),
enabled = state.saveButtonEnabled,
onClick = {
focusManager.clearFocus()
state.eventSink(RoomDetailsEditEvents.Save)
},
- ) {
- Text(
- text = stringResource(CommonStrings.action_save),
- style = ElementTheme.typography.aliasButtonText,
- )
- }
+ )
}
)
},
@@ -164,6 +161,9 @@ fun RoomDetailsEditView(
placeholder = stringResource(CommonStrings.common_topic_placeholder),
maxLines = 10,
onValueChange = { state.eventSink(RoomDetailsEditEvents.UpdateRoomTopic(it)) },
+ keyboardOptions = KeyboardOptions(
+ capitalization = KeyboardCapitalization.Sentences,
+ ),
)
} else {
LabelledReadOnlyField(
@@ -287,12 +287,12 @@ private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier =
@Preview
@Composable
-fun RoomDetailsEditViewLightPreview(@PreviewParameter(RoomDetailsEditStateProvider::class) state: RoomDetailsEditState) =
+internal fun RoomDetailsEditViewLightPreview(@PreviewParameter(RoomDetailsEditStateProvider::class) state: RoomDetailsEditState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
-fun RoomDetailsEditViewDarkPreview(@PreviewParameter(RoomDetailsEditStateProvider::class) state: RoomDetailsEditState) =
+internal fun RoomDetailsEditViewDarkPreview(@PreviewParameter(RoomDetailsEditStateProvider::class) state: RoomDetailsEditState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt
index 555f04af20..601830808f 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt
@@ -38,7 +38,7 @@ import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
-import io.element.android.libraries.designsystem.theme.components.Divider
+import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.SearchBar
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
@@ -128,10 +128,8 @@ fun RoomInviteMembersTopBar(
navigationIcon = { BackButton(onClick = onBackPressed) },
actions = {
TextButton(
+ text = stringResource(CommonStrings.action_send),
onClick = onSendPressed,
- content = {
- Text(stringResource(CommonStrings.action_send))
- },
enabled = canSend,
)
}
@@ -210,7 +208,7 @@ private fun RoomInviteMembersSearchBar(
}
if (index < results.lastIndex) {
- Divider()
+ HorizontalDivider()
}
}
}
@@ -220,12 +218,12 @@ private fun RoomInviteMembersSearchBar(
@Preview
@Composable
-fun RoomInviteMembersLightPreview(@PreviewParameter(RoomInviteMembersStateProvider::class) state: RoomInviteMembersState) =
+internal fun RoomInviteMembersLightPreview(@PreviewParameter(RoomInviteMembersStateProvider::class) state: RoomInviteMembersState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
-fun RoomInviteMembersDarkPreview(@PreviewParameter(RoomInviteMembersStateProvider::class) state: RoomInviteMembersState) =
+internal fun RoomInviteMembersDarkPreview(@PreviewParameter(RoomInviteMembersStateProvider::class) state: RoomInviteMembersState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt
index 3bfde66c06..b0bac12d4e 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt
@@ -45,7 +45,6 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
-import io.element.android.libraries.designsystem.theme.aliasButtonText
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Scaffold
@@ -212,14 +211,9 @@ private fun RoomMemberListTopBar(
actions = {
if (canInvite) {
TextButton(
- modifier = Modifier.padding(horizontal = 8.dp),
+ text = stringResource(CommonStrings.action_invite),
onClick = onInvitePressed,
- ) {
- Text(
- text = stringResource(CommonStrings.action_invite),
- style = ElementTheme.typography.aliasButtonText,
- )
- }
+ )
}
}
)
@@ -257,12 +251,12 @@ private fun RoomMemberSearchBar(
@Preview
@Composable
-fun RoomMemberListLightPreview(@PreviewParameter(RoomMemberListStateProvider::class) state: RoomMemberListState) =
+internal fun RoomMemberListLightPreview(@PreviewParameter(RoomMemberListStateProvider::class) state: RoomMemberListState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
-fun RoomMemberListDarkPreview(@PreviewParameter(RoomMemberListStateProvider::class) state: RoomMemberListState) =
+internal fun RoomMemberListDarkPreview(@PreviewParameter(RoomMemberListStateProvider::class) state: RoomMemberListState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsView.kt
index 72b9d4c20c..84dd9319cf 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsView.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsView.kt
@@ -39,6 +39,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.features.roomdetails.impl.blockuser.BlockUserDialogs
@@ -118,10 +119,16 @@ internal fun RoomMemberHeaderSection(
}
Spacer(modifier = Modifier.height(24.dp))
if (userName != null) {
- Text(userName, style = ElementTheme.typography.fontHeadingLgBold)
+ Text(text = userName, style = ElementTheme.typography.fontHeadingLgBold)
Spacer(modifier = Modifier.height(6.dp))
}
- Text(userId, style = ElementTheme.typography.fontBodyLgRegular, color = MaterialTheme.colorScheme.secondary)
+ Text(
+ text = userId,
+ style = ElementTheme.typography.fontBodyLgRegular,
+ color = MaterialTheme.colorScheme.secondary,
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
+ textAlign = TextAlign.Center,
+ )
Spacer(Modifier.height(40.dp))
}
}
@@ -146,12 +153,12 @@ internal fun SendMessageSection(onSendMessage: () -> Unit, modifier: Modifier =
@LargeHeightPreview
@Composable
-fun RoomMemberDetailsViewLightPreview(@PreviewParameter(RoomMemberDetailsStateProvider::class) state: RoomMemberDetailsState) =
+internal fun RoomMemberDetailsViewLightPreview(@PreviewParameter(RoomMemberDetailsStateProvider::class) state: RoomMemberDetailsState) =
ElementPreviewLight { ContentToPreview(state) }
@LargeHeightPreview
@Composable
-fun RoomMemberDetailsViewDarkPreview(@PreviewParameter(RoomMemberDetailsStateProvider::class) state: RoomMemberDetailsState) =
+internal fun RoomMemberDetailsViewDarkPreview(@PreviewParameter(RoomMemberDetailsStateProvider::class) state: RoomMemberDetailsState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
diff --git a/features/roomdetails/impl/src/main/res/values-fr/translations.xml b/features/roomdetails/impl/src/main/res/values-fr/translations.xml
index ee34445805..2696cc99ea 100644
--- a/features/roomdetails/impl/src/main/res/values-fr/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-fr/translations.xml
@@ -1,7 +1,7 @@
- - "1 membre"
+ - "%1$d membre"
- "%1$d membres"
"Définir un sujet"
@@ -12,17 +12,22 @@
"Impossible de mettre à jour le salon"
"Les messages sont sécurisés par des cadenas numériques. Seuls vous et les destinataires possédez les clés uniques pour les déverrouiller."
"Chiffrement des messages activé"
+ "Une erreur s’est produite lors du chargement des paramètres de notification."
+ "Impossible de désactiver les notifications de cette salle, veuillez réessayer."
+ "Impossible de réactiver les notifications de cette salle, veuillez réessayer."
"Inviter des personnes"
+ "Personnalisé"
+ "Par défaut"
"Notifications"
"Nom du salon"
"Partager le salon"
"Mise à jour du salon…"
"En attente"
"Bloquer"
- "Les utilisateurs bloqués ne pourront pas vous envoyer de messages et tous leurs messages seront masqués. Vous pouvez annuler cette action à tout moment."
+ "Les utilisateurs bloqués ne pourront pas vous envoyer de messages et tous leurs messages seront masqués. Vous pouvez les débloquer à tout moment."
"Bloquer l\'utilisateur"
"Débloquer"
- "Lorsque vous débloquez l\'utilisateur, vous pourrez à nouveau voir tous leur messages."
+ "Vous pourrez à nouveau voir tous leurs messages."
"Débloquer l\'utilisateur"
"Quitter le salon"
"Personnes"
diff --git a/features/roomdetails/impl/src/main/res/values-ru/translations.xml b/features/roomdetails/impl/src/main/res/values-ru/translations.xml
new file mode 100644
index 0000000000..4d2664ab30
--- /dev/null
+++ b/features/roomdetails/impl/src/main/res/values-ru/translations.xml
@@ -0,0 +1,38 @@
+
+
+
+ - "%1$d пользователь"
+ - "%1$d пользователя"
+ - "%1$d пользователей"
+
+ "Добавить тему"
+ "Уже зарегистрирован"
+ "Уже приглашены"
+ "Редактировать комнату"
+ "Произошла неизвестная ошибка, и информацию нельзя было изменить."
+ "Не удалось обновить комнату"
+ "Сообщения зашифрованы. Только у вас и у получателей есть уникальные ключи для их разблокировки."
+ "Шифрование сообщений включено"
+ "При загрузке настроек уведомлений произошла ошибка."
+ "Не удалось отключить звук в этой комнате, попробуйте еще раз."
+ "Не удалось включить звук в эту комнату, попробуйте еще раз."
+ "Пригласить участника"
+ "Пользовательский"
+ "По умолчанию"
+ "Уведомления"
+ "Название комнаты"
+ "Поделиться комнатой"
+ "Обновление комнаты…"
+ "В ожидании"
+ "Участники комнаты"
+ "Заблокировать"
+ "Заблокированные пользователи не смогут отправлять вам сообщения, а все их сообщения будут скрыты. Вы можете разблокировать их в любое время."
+ "Заблокировать пользователя"
+ "Разблокировать"
+ "Вы снова сможете увидеть все сообщения."
+ "Разблокировать пользователя"
+ "Покинуть комнату"
+ "Пользователи"
+ "Безопасность"
+ "Тема"
+
diff --git a/features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml b/features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml
new file mode 100644
index 0000000000..cbac73f938
--- /dev/null
+++ b/features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml
@@ -0,0 +1,27 @@
+
+
+
+ - "%1$d 位夥伴"
+
+ "新增主題"
+ "已是成員"
+ "已邀請"
+ "編輯聊天室"
+ "訊息已加密"
+ "邀請夥伴"
+ "自訂"
+ "預設"
+ "通知"
+ "聊天室名稱"
+ "分享聊天室"
+ "正在更新聊天室…"
+ "待定"
+ "聊天室成員"
+ "封鎖"
+ "封鎖使用者"
+ "解除封鎖"
+ "解除封鎖使用者"
+ "離開聊天室"
+ "夥伴"
+ "主題"
+
diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml
index 158ba386b4..b1f67dab1e 100644
--- a/features/roomdetails/impl/src/main/res/values/localazy.xml
+++ b/features/roomdetails/impl/src/main/res/values/localazy.xml
@@ -1,7 +1,7 @@
- - "1 person"
+ - "%1$d person"
- "%1$d people"
"Add topic"
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt
index 5a82b20723..dbef9c799b 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt
@@ -135,6 +135,6 @@ class RoomListPresenter @Inject constructor(
// Safe to give bigger size than room list
val extendedRangeEnd = range.last + midExtendedRangeSize
val extendedRange = IntRange(extendedRangeStart, extendedRangeEnd)
- client.roomSummaryDataSource.updateAllRoomsVisibleRange(extendedRange)
+ client.roomListService.updateAllRoomsVisibleRange(extendedRange)
}
}
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt
index 657648df42..176962ca2a 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt
@@ -28,8 +28,6 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Snackbar
-import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
@@ -54,11 +52,12 @@ import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.features.roomlist.impl.search.RoomListSearchResultView
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
-import io.element.android.libraries.designsystem.theme.components.Divider
+import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
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.Scaffold
import io.element.android.libraries.designsystem.utils.LogCompositions
+import io.element.android.libraries.designsystem.utils.SnackbarHost
import io.element.android.libraries.designsystem.utils.rememberSnackbarHostState
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.designsystem.R as DrawableR
@@ -208,7 +207,7 @@ fun RoomListContent(
onLongClick = onRoomLongClicked,
)
if (index != state.roomList.lastIndex) {
- Divider()
+ HorizontalDivider()
}
}
}
@@ -227,13 +226,7 @@ fun RoomListContent(
)
}
},
- snackbarHost = {
- SnackbarHost(snackbarHostState) { data ->
- Snackbar(
- snackbarData = data,
- )
- }
- },
+ snackbarHost = { SnackbarHost(snackbarHostState) },
)
}
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RequestVerificationHeader.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RequestVerificationHeader.kt
index 16eea28f20..9d6b54366a 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RequestVerificationHeader.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RequestVerificationHeader.kt
@@ -19,7 +19,6 @@ package io.element.android.features.roomlist.impl.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@@ -37,7 +36,7 @@ import androidx.compose.ui.unit.dp
import io.element.android.features.roomlist.impl.R
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
-import io.element.android.libraries.designsystem.theme.aliasButtonText
+import io.element.android.libraries.designsystem.theme.components.ButtonSize
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.Surface
@@ -83,15 +82,11 @@ internal fun RequestVerificationHeader(
)
Spacer(modifier = Modifier.height(12.dp))
Button(
+ text = stringResource(CommonStrings.action_continue),
+ size = ButtonSize.Medium,
modifier = Modifier.fillMaxWidth(),
- contentPadding = PaddingValues(horizontal = 20.dp, vertical = 7.dp),
onClick = onVerifyClicked,
- ) {
- Text(
- stringResource(CommonStrings.action_continue),
- style = ElementTheme.typography.aliasButtonText
- )
- }
+ )
}
}
}
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt
index db0ea8c11d..001c048e4e 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt
@@ -43,10 +43,11 @@ import io.element.android.libraries.designsystem.components.avatar.Avatar
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.text.applyScaleDown
+import io.element.android.libraries.designsystem.text.toSp
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
import io.element.android.libraries.designsystem.theme.components.DropdownMenu
import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem
-import io.element.android.libraries.designsystem.theme.components.DropdownMenuItemText
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
@@ -114,7 +115,11 @@ private fun DefaultRoomListTopBar(
val fontStyle = if (scrollBehavior.state.collapsedFraction > 0.5)
ElementTheme.typography.aliasScreenTitle
else
- ElementTheme.typography.fontHeadingLgBold
+ ElementTheme.typography.fontHeadingLgBold.copy(
+ // Due to a limitation of MediumTopAppBar, and to avoid the text to be truncated,
+ // ensure that the font size will never be bigger than 28.dp.
+ fontSize = 28.dp.applyScaleDown().toSp()
+ )
Text(
style = fontStyle,
text = stringResource(id = R.string.screen_roomlist_main_space_title)
@@ -163,7 +168,7 @@ private fun DefaultRoomListTopBar(
showMenu = false
onMenuActionClicked(RoomListMenuAction.InviteFriends)
},
- text = { DropdownMenuItemText(stringResource(id = CommonStrings.action_invite)) },
+ text = { Text(stringResource(id = CommonStrings.action_invite)) },
leadingIcon = {
Icon(
Icons.Outlined.Share,
@@ -177,7 +182,7 @@ private fun DefaultRoomListTopBar(
showMenu = false
onMenuActionClicked(RoomListMenuAction.ReportBug)
},
- text = { DropdownMenuItemText(stringResource(id = CommonStrings.common_report_a_bug)) },
+ text = { Text(stringResource(id = CommonStrings.common_report_a_bug)) },
leadingIcon = {
Icon(
Icons.Outlined.BugReport,
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/DefaultInviteStateDataSource.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/DefaultInviteStateDataSource.kt
index 3a89014799..e05efd3ddd 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/DefaultInviteStateDataSource.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/DefaultInviteStateDataSource.kt
@@ -30,7 +30,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
-import io.element.android.libraries.matrix.api.room.RoomSummary
+import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.coroutines.withContext
import javax.inject.Inject
@@ -44,8 +44,9 @@ class DefaultInviteStateDataSource @Inject constructor(
@Composable
override fun inviteState(): InvitesState {
val invites by client
- .roomSummaryDataSource
- .inviteRooms()
+ .roomListService
+ .invites()
+ .summaries
.collectAsState()
val seenInvites by seenInvitesStore
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt
index 8602f15910..e44bcd6b6b 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt
@@ -18,6 +18,8 @@ package io.element.android.features.roomlist.impl.datasource
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryPlaceholders
+import io.element.android.libraries.androidutils.diff.DiffCacheUpdater
+import io.element.android.libraries.androidutils.diff.MutableListDiffCache
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.extensions.orEmpty
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
@@ -25,8 +27,8 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
import io.element.android.libraries.matrix.api.core.RoomId
-import io.element.android.libraries.matrix.api.room.RoomSummary
-import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource
+import io.element.android.libraries.matrix.api.roomlist.RoomListService
+import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@@ -36,11 +38,13 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import javax.inject.Inject
class RoomListDataSource @Inject constructor(
- private val roomSummaryDataSource: RoomSummaryDataSource,
+ private val roomListService: RoomListService,
private val lastMessageTimestampFormatter: LastMessageTimestampFormatter,
private val roomLastMessageFormatter: RoomLastMessageFormatter,
private val coroutineDispatchers: CoroutineDispatchers,
@@ -50,15 +54,18 @@ class RoomListDataSource @Inject constructor(
private val _allRooms = MutableStateFlow>(persistentListOf())
private val _filteredRooms = MutableStateFlow>(persistentListOf())
+ private val lock = Mutex()
+ private val diffCache = MutableListDiffCache()
+ private val diffCacheUpdater = DiffCacheUpdater(diffCache = diffCache, detectMoves = true) { old, new ->
+ old?.identifier() == new?.identifier()
+ }
+
fun launchIn(coroutineScope: CoroutineScope) {
- roomSummaryDataSource
+ roomListService
.allRooms()
+ .summaries
.onEach { roomSummaries ->
- _allRooms.value = if (roomSummaries.isEmpty()) {
- RoomListRoomSummaryPlaceholders.createFakeList(16)
- } else {
- mapRoomSummaries(roomSummaries)
- }.toImmutableList()
+ replaceWith(roomSummaries)
}
.launchIn(coroutineScope)
@@ -73,7 +80,8 @@ class RoomListDataSource @Inject constructor(
}
.onEach {
_filteredRooms.value = it
- }.launchIn(coroutineScope)
+ }
+ .launchIn(coroutineScope)
}
fun updateFilter(filterValue: String) {
@@ -84,33 +92,63 @@ class RoomListDataSource @Inject constructor(
val allRooms: StateFlow> = _allRooms
val filteredRooms: StateFlow> = _filteredRooms
- private suspend fun mapRoomSummaries(
- roomSummaries: List
- ): List = withContext(coroutineDispatchers.computation) {
- roomSummaries.map { roomSummary ->
- when (roomSummary) {
- is RoomSummary.Empty -> RoomListRoomSummaryPlaceholders.create(roomSummary.identifier)
- is RoomSummary.Filled -> {
- val avatarData = AvatarData(
- id = roomSummary.identifier(),
- name = roomSummary.details.name,
- url = roomSummary.details.avatarURLString,
- size = AvatarSize.RoomListItem,
- )
- val roomIdentifier = roomSummary.identifier()
- RoomListRoomSummary(
- id = roomSummary.identifier(),
- roomId = RoomId(roomIdentifier),
- name = roomSummary.details.name,
- hasUnread = roomSummary.details.unreadNotificationCount > 0,
- timestamp = lastMessageTimestampFormatter.format(roomSummary.details.lastMessageTimestamp),
- lastMessage = roomSummary.details.lastMessage?.let { message ->
- roomLastMessageFormatter.format(message.event, roomSummary.details.isDirect)
- }.orEmpty(),
- avatarData = avatarData,
- )
- }
- }
+ private suspend fun replaceWith(roomSummaries: List) = withContext(coroutineDispatchers.computation) {
+ lock.withLock {
+ diffCacheUpdater.updateWith(roomSummaries)
+ buildAndEmitAllRooms(roomSummaries)
}
}
+
+ private suspend fun buildAndEmitAllRooms(roomSummaries: List) {
+ if (diffCache.isEmpty()) {
+ _allRooms.emit(
+ RoomListRoomSummaryPlaceholders.createFakeList(16).toImmutableList()
+ )
+ } else {
+ val roomListRoomSummaries = ArrayList()
+ for (index in diffCache.indices()) {
+ val cacheItem = diffCache.get(index)
+ if (cacheItem == null) {
+ buildAndCacheItem(roomSummaries, index)?.also { timelineItemState ->
+ roomListRoomSummaries.add(timelineItemState)
+ }
+ } else {
+ roomListRoomSummaries.add(cacheItem)
+ }
+ }
+ _allRooms.emit(roomListRoomSummaries.toImmutableList())
+ }
+ }
+
+ private fun buildAndCacheItem(
+ roomSummaries: List,
+ index: Int
+ ): RoomListRoomSummary? {
+ val roomListRoomSummary = when (val roomSummary = roomSummaries.getOrNull(index)) {
+ is RoomSummary.Empty -> RoomListRoomSummaryPlaceholders.create(roomSummary.identifier)
+ is RoomSummary.Filled -> {
+ val avatarData = AvatarData(
+ id = roomSummary.identifier(),
+ name = roomSummary.details.name,
+ url = roomSummary.details.avatarURLString,
+ size = AvatarSize.RoomListItem,
+ )
+ val roomIdentifier = roomSummary.identifier()
+ RoomListRoomSummary(
+ id = roomSummary.identifier(),
+ roomId = RoomId(roomIdentifier),
+ name = roomSummary.details.name,
+ hasUnread = roomSummary.details.unreadNotificationCount > 0,
+ timestamp = lastMessageTimestampFormatter.format(roomSummary.details.lastMessageTimestamp),
+ lastMessage = roomSummary.details.lastMessage?.let { message ->
+ roomLastMessageFormatter.format(message.event, roomSummary.details.isDirect)
+ }.orEmpty(),
+ avatarData = avatarData,
+ )
+ }
+ null -> null
+ }
+ diffCache[index] = roomListRoomSummary
+ return roomListRoomSummary
+ }
}
diff --git a/features/roomlist/impl/src/main/res/values-ru/translations.xml b/features/roomlist/impl/src/main/res/values-ru/translations.xml
new file mode 100644
index 0000000000..fcd91c56e3
--- /dev/null
+++ b/features/roomlist/impl/src/main/res/values-ru/translations.xml
@@ -0,0 +1,9 @@
+
+
+ "Создайте новую беседу или комнату"
+ "Начните переписку с отправки сообщения."
+ "Пока нет доступных чатов."
+ "Все чаты"
+ "Похоже, вы используете новое устройство. Чтобы получить доступ к зашифрованным сообщениям в дальнейшем, проверьте их на другом устройстве."
+ "Подтвердите, что это вы"
+
diff --git a/features/roomlist/impl/src/main/res/values-sk/translations.xml b/features/roomlist/impl/src/main/res/values-sk/translations.xml
index 250822f4c9..0a1879a484 100644
--- a/features/roomlist/impl/src/main/res/values-sk/translations.xml
+++ b/features/roomlist/impl/src/main/res/values-sk/translations.xml
@@ -1,6 +1,8 @@
"Vytvorte novú konverzáciu alebo miestnosť"
+ "Začnite tým, že niekomu pošlete správu."
+ "Zatiaľ žiadne konverzácie."
"Všetky konverzácie"
"Vyzerá to tak, že používate nové zariadenie. Overte svoj prístup k zašifrovaným správam pomocou vášho druhého zariadenia."
"Overte, že ste to vy"
diff --git a/features/roomlist/impl/src/main/res/values-zh-rTW/translations.xml b/features/roomlist/impl/src/main/res/values-zh-rTW/translations.xml
new file mode 100644
index 0000000000..b4d22c5b26
--- /dev/null
+++ b/features/roomlist/impl/src/main/res/values-zh-rTW/translations.xml
@@ -0,0 +1,4 @@
+
+
+ "建立新的對話或聊天室"
+
diff --git a/features/roomlist/impl/src/main/res/values/localazy.xml b/features/roomlist/impl/src/main/res/values/localazy.xml
index 3a1c3cbad6..d63f96d07c 100644
--- a/features/roomlist/impl/src/main/res/values/localazy.xml
+++ b/features/roomlist/impl/src/main/res/values/localazy.xml
@@ -1,6 +1,8 @@
"Create a new conversation or room"
+ "Get started by messaging someone."
+ "No chats yet."
"All Chats"
"Looks like you’re using a new device. Verify with another device to access your encrypted messages moving forwards."
"Verify it’s you"
diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt
index ce8e83c70d..eaa3801e12 100644
--- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt
+++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt
@@ -47,9 +47,10 @@ import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.FakeMatrixClient
-import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
+import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
+import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.TestScope
@@ -110,21 +111,20 @@ class RoomListPresenterTests {
@Test
fun `present - load 1 room with success`() = runTest {
- val roomSummaryDataSource = FakeRoomSummaryDataSource()
+ val roomListService = FakeRoomListService()
val matrixClient = FakeMatrixClient(
- roomSummaryDataSource = roomSummaryDataSource
+ roomListService = roomListService
)
val presenter = createRoomListPresenter(matrixClient)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
- skipItems(1)
- val initialState = awaitItem()
+ val initialState = consumeItemsUntilPredicate { state -> state.roomList.size == 16 }.last()
// Room list is loaded with 16 placeholders
Truth.assertThat(initialState.roomList.size).isEqualTo(16)
Truth.assertThat(initialState.roomList.all { it.isPlaceholder }).isTrue()
- roomSummaryDataSource.postAllRooms(listOf(aRoomSummaryFilled()))
- val withRoomState = awaitItem()
+ roomListService.postAllRooms(listOf(aRoomSummaryFilled()))
+ val withRoomState = consumeItemsUntilPredicate { state -> state.roomList.size == 1 }.last()
Truth.assertThat(withRoomState.roomList.size).isEqualTo(1)
Truth.assertThat(withRoomState.roomList.first())
.isEqualTo(aRoomListRoomSummary)
@@ -133,68 +133,66 @@ class RoomListPresenterTests {
@Test
fun `present - load 1 room with success and filter rooms`() = runTest {
- val roomSummaryDataSource = FakeRoomSummaryDataSource()
+ val roomListService = FakeRoomListService()
val matrixClient = FakeMatrixClient(
- roomSummaryDataSource = roomSummaryDataSource
+ roomListService = roomListService
)
val presenter = createRoomListPresenter(matrixClient)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
- roomSummaryDataSource.postAllRooms(listOf(aRoomSummaryFilled()))
- skipItems(1)
- val loadedState = awaitItem()
+ roomListService.postAllRooms(listOf(aRoomSummaryFilled()))
+ val loadedState = consumeItemsUntilPredicate { state -> state.roomList.size == 1 }.last()
// Test filtering with result
loadedState.eventSink.invoke(RoomListEvents.UpdateFilter(A_ROOM_NAME.substring(0, 3)))
- skipItems(1) // Filter update
- val withNotFilteredRoomState = awaitItem()
- Truth.assertThat(withNotFilteredRoomState.filter).isEqualTo(A_ROOM_NAME.substring(0, 3))
- Truth.assertThat(withNotFilteredRoomState.filteredRoomList.size).isEqualTo(1)
- Truth.assertThat(withNotFilteredRoomState.filteredRoomList.first())
+ val withFilteredRoomState = consumeItemsUntilPredicate { state -> state.filteredRoomList.size == 1 }.last()
+ Truth.assertThat(withFilteredRoomState.filter).isEqualTo(A_ROOM_NAME.substring(0, 3))
+ Truth.assertThat(withFilteredRoomState.filteredRoomList.size).isEqualTo(1)
+ Truth.assertThat(withFilteredRoomState.filteredRoomList.first())
.isEqualTo(aRoomListRoomSummary)
// Test filtering without result
- withNotFilteredRoomState.eventSink.invoke(RoomListEvents.UpdateFilter("tada"))
- skipItems(1) // Filter update
- Truth.assertThat(awaitItem().filter).isEqualTo("tada")
- Truth.assertThat(awaitItem().filteredRoomList).isEmpty()
+ withFilteredRoomState.eventSink.invoke(RoomListEvents.UpdateFilter("tada"))
+ val withNotFilteredRoomState = consumeItemsUntilPredicate { state -> state.filteredRoomList.size == 0 }.last()
+ Truth.assertThat(withNotFilteredRoomState.filter).isEqualTo("tada")
+ Truth.assertThat(withNotFilteredRoomState.filteredRoomList).isEmpty()
}
}
@Test
fun `present - update visible range`() = runTest {
- val roomSummaryDataSource = FakeRoomSummaryDataSource()
+ val roomListService = FakeRoomListService()
val matrixClient = FakeMatrixClient(
- roomSummaryDataSource = roomSummaryDataSource
+ roomListService = roomListService
)
val presenter = createRoomListPresenter(matrixClient)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
- roomSummaryDataSource.postAllRooms(listOf(aRoomSummaryFilled()))
+ roomListService.postAllRooms(listOf(aRoomSummaryFilled()))
val loadedState = awaitItem()
// check initial value
- Truth.assertThat(roomSummaryDataSource.latestSlidingSyncRange).isNull()
+ Truth.assertThat(roomListService.latestSlidingSyncRange).isNull()
// Test empty range
loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(1, 0)))
- Truth.assertThat(roomSummaryDataSource.latestSlidingSyncRange).isNull()
+ Truth.assertThat(roomListService.latestSlidingSyncRange).isNull()
// Update visible range and check that range is transmitted to the SDK after computation
loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(0, 0)))
- Truth.assertThat(roomSummaryDataSource.latestSlidingSyncRange)
+ Truth.assertThat(roomListService.latestSlidingSyncRange)
.isEqualTo(IntRange(0, 20))
loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(0, 1)))
- Truth.assertThat(roomSummaryDataSource.latestSlidingSyncRange)
+ Truth.assertThat(roomListService.latestSlidingSyncRange)
.isEqualTo(IntRange(0, 21))
loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(19, 29)))
- Truth.assertThat(roomSummaryDataSource.latestSlidingSyncRange)
+ Truth.assertThat(roomListService.latestSlidingSyncRange)
.isEqualTo(IntRange(0, 49))
loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(49, 59)))
- Truth.assertThat(roomSummaryDataSource.latestSlidingSyncRange)
+ Truth.assertThat(roomListService.latestSlidingSyncRange)
.isEqualTo(IntRange(29, 79))
loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(149, 159)))
- Truth.assertThat(roomSummaryDataSource.latestSlidingSyncRange)
+ Truth.assertThat(roomListService.latestSlidingSyncRange)
.isEqualTo(IntRange(129, 179))
loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(149, 259)))
- Truth.assertThat(roomSummaryDataSource.latestSlidingSyncRange)
+ Truth.assertThat(roomListService.latestSlidingSyncRange)
.isEqualTo(IntRange(129, 279))
cancelAndIgnoreRemainingEvents()
}
@@ -202,9 +200,9 @@ class RoomListPresenterTests {
@Test
fun `present - handle DismissRequestVerificationPrompt`() = runTest {
- val roomSummaryDataSource = FakeRoomSummaryDataSource()
+ val roomListService = FakeRoomListService()
val matrixClient = FakeMatrixClient(
- roomSummaryDataSource = roomSummaryDataSource
+ roomListService = roomListService
)
val presenter = createRoomListPresenter(
client = matrixClient,
@@ -319,7 +317,7 @@ class RoomListPresenterTests {
inviteStateDataSource = inviteStateDataSource,
leaveRoomPresenter = leaveRoomPresenter,
roomListDataSource = RoomListDataSource(
- client.roomSummaryDataSource,
+ client.roomListService,
lastMessageTimestampFormatter,
roomLastMessageFormatter,
coroutineDispatchers = testCoroutineDispatchers()
diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/DefaultInviteStateDataSourceTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/DefaultInviteStateDataSourceTest.kt
index 88e69068fd..ce7bf685a8 100644
--- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/DefaultInviteStateDataSourceTest.kt
+++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/DefaultInviteStateDataSourceTest.kt
@@ -25,8 +25,8 @@ import io.element.android.features.roomlist.impl.InvitesState
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
import io.element.android.libraries.matrix.test.FakeMatrixClient
-import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
+import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -35,8 +35,8 @@ internal class DefaultInviteStateDataSourceTest {
@Test
fun `emits NoInvites state if invites list is empty`() = runTest {
- val roomSummaryDataSource = FakeRoomSummaryDataSource()
- val client = FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
+ val roomListService = FakeRoomListService()
+ val client = FakeMatrixClient(roomListService = roomListService)
val seenStore = FakeSeenInvitesStore()
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers())
@@ -49,9 +49,9 @@ internal class DefaultInviteStateDataSourceTest {
@Test
fun `emits NewInvites state if unseen invite exists`() = runTest {
- val roomSummaryDataSource = FakeRoomSummaryDataSource()
- roomSummaryDataSource.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID)))
- val client = FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
+ val roomListService = FakeRoomListService()
+ roomListService.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID)))
+ val client = FakeMatrixClient(roomListService = roomListService)
val seenStore = FakeSeenInvitesStore()
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers())
@@ -65,9 +65,9 @@ internal class DefaultInviteStateDataSourceTest {
@Test
fun `emits NewInvites state if multiple invites exist and at least one is unseen`() = runTest {
- val roomSummaryDataSource = FakeRoomSummaryDataSource()
- roomSummaryDataSource.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(roomId = A_ROOM_ID_2)))
- val client = FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
+ val roomListService = FakeRoomListService()
+ roomListService.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(roomId = A_ROOM_ID_2)))
+ val client = FakeMatrixClient(roomListService = roomListService)
val seenStore = FakeSeenInvitesStore()
seenStore.publishRoomIds(setOf(A_ROOM_ID))
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers(useUnconfinedTestDispatcher = true))
@@ -82,9 +82,9 @@ internal class DefaultInviteStateDataSourceTest {
@Test
fun `emits SeenInvites state if invite exists in seen store`() = runTest {
- val roomSummaryDataSource = FakeRoomSummaryDataSource()
- roomSummaryDataSource.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID)))
- val client = FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
+ val roomListService = FakeRoomListService()
+ roomListService.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID)))
+ val client = FakeMatrixClient(roomListService = roomListService)
val seenStore = FakeSeenInvitesStore()
seenStore.publishRoomIds(setOf(A_ROOM_ID))
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers(useUnconfinedTestDispatcher = true))
@@ -100,8 +100,8 @@ internal class DefaultInviteStateDataSourceTest {
@Test
fun `emits new state in response to upstream events`() = runTest {
- val roomSummaryDataSource = FakeRoomSummaryDataSource()
- val client = FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
+ val roomListService = FakeRoomListService()
+ val client = FakeMatrixClient(roomListService = roomListService)
val seenStore = FakeSeenInvitesStore()
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers())
@@ -112,7 +112,7 @@ internal class DefaultInviteStateDataSourceTest {
Truth.assertThat(awaitItem()).isEqualTo(InvitesState.NoInvites)
// When a single invite is received, state should be NewInvites
- roomSummaryDataSource.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID)))
+ roomListService.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID)))
skipItems(1)
Truth.assertThat(awaitItem()).isEqualTo(InvitesState.NewInvites)
@@ -122,12 +122,12 @@ internal class DefaultInviteStateDataSourceTest {
Truth.assertThat(awaitItem()).isEqualTo(InvitesState.SeenInvites)
// Another new invite resets it to NewInvites
- roomSummaryDataSource.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(roomId = A_ROOM_ID_2)))
+ roomListService.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(roomId = A_ROOM_ID_2)))
skipItems(1)
Truth.assertThat(awaitItem()).isEqualTo(InvitesState.NewInvites)
// All of the invites going away reverts to NoInvites
- roomSummaryDataSource.postInviteRooms(emptyList())
+ roomListService.postInviteRooms(emptyList())
skipItems(1)
Truth.assertThat(awaitItem()).isEqualTo(InvitesState.NoInvites)
}
diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt
index 77984a3bb2..0ffd518669 100644
--- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt
+++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt
@@ -27,7 +27,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@@ -44,15 +43,16 @@ import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
-import io.element.android.libraries.designsystem.components.button.ButtonWithProgress
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
-import io.element.android.libraries.designsystem.theme.aliasButtonText
+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.Text
+import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings
+import kotlinx.coroutines.sync.Mutex
import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep as FlowStep
@Composable
@@ -75,6 +75,7 @@ fun VerifySelfSessionView(
val buttonsVisible by remember(verificationFlowStep) {
derivedStateOf { verificationFlowStep != FlowStep.AwaitingOtherDeviceResponse && verificationFlowStep != FlowStep.Completed }
}
+ Mutex()
HeaderFooterPage(
modifier = modifier,
header = {
@@ -219,35 +220,33 @@ internal fun BottomMenu(screenState: VerifySelfSessionState, goBack: () -> Unit)
ButtonColumnMolecule(
modifier = Modifier.padding(bottom = 20.dp)
) {
- ButtonWithProgress(
- text = positiveButtonTitle?.let { stringResource(it) },
- showProgress = isVerifying,
- modifier = Modifier.fillMaxWidth(),
- onClick = { positiveButtonEvent?.let { eventSink(it) } }
- )
+ if (positiveButtonTitle != null) {
+ Button(
+ text = stringResource(positiveButtonTitle),
+ showProgress = isVerifying,
+ modifier = Modifier.fillMaxWidth(),
+ onClick = { positiveButtonEvent?.let { eventSink(it) } }
+ )
+ }
if (negativeButtonTitle != null) {
TextButton(
+ text = stringResource(negativeButtonTitle),
modifier = Modifier.fillMaxWidth(),
onClick = negativeButtonCallback,
enabled = negativeButtonEnabled,
- ) {
- Text(
- text = stringResource(negativeButtonTitle),
- style = ElementTheme.typography.aliasButtonText,
- )
- }
+ )
}
}
}
@Preview
@Composable
-fun VerifySelfSessionViewLightPreview(@PreviewParameter(VerifySelfSessionStateProvider::class) state: VerifySelfSessionState) =
+internal fun VerifySelfSessionViewLightPreview(@PreviewParameter(VerifySelfSessionStateProvider::class) state: VerifySelfSessionState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
-fun VerifySelfSessionViewDarkPreview(@PreviewParameter(VerifySelfSessionStateProvider::class) state: VerifySelfSessionState) =
+internal fun VerifySelfSessionViewDarkPreview(@PreviewParameter(VerifySelfSessionStateProvider::class) state: VerifySelfSessionState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
diff --git a/features/verifysession/impl/src/main/res/values-ru/translations.xml b/features/verifysession/impl/src/main/res/values-ru/translations.xml
new file mode 100644
index 0000000000..552204fd7a
--- /dev/null
+++ b/features/verifysession/impl/src/main/res/values-ru/translations.xml
@@ -0,0 +1,19 @@
+
+
+ "Кажется, что-то не так. Время ожидания запроса истекло, либо запрос был отклонен."
+ "Убедитесь, что приведенные ниже смайлики совпадают со смайликами, показанными во время другого сеанса."
+ "Сравните смайлики"
+ "Ваш новый сеанс подтвержден. У него есть доступ к вашим зашифрованным сообщениям, и другие пользователи увидят его как доверенное."
+ "Чтобы получить доступ к зашифрованной истории сообщений, докажите, что это вы."
+ "Открыть существующий сеанс"
+ "Повторить проверку"
+ "Я готов"
+ "Ожидание соответствия"
+ "Сравните уникальные смайлики, убедившись, что они расположены в том же порядке."
+ "Они не совпадают"
+ "Они совпадают"
+ "Для продолжения работы примите запрос на запуск процесса проверки в другом сеансе."
+ "Ожидание принятия запроса"
+ "Проверка отменена"
+ "Начать"
+
diff --git a/features/verifysession/impl/src/main/res/values-zh-rTW/translations.xml b/features/verifysession/impl/src/main/res/values-zh-rTW/translations.xml
new file mode 100644
index 0000000000..fc59911a93
--- /dev/null
+++ b/features/verifysession/impl/src/main/res/values-zh-rTW/translations.xml
@@ -0,0 +1,9 @@
+
+
+ "我準備好了"
+ "等待比對"
+ "不相符"
+ "相符"
+ "驗證已取消"
+ "開始"
+
diff --git a/gradle.properties b/gradle.properties
index e15ee7a033..01bd6b9597 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -56,3 +56,6 @@ android.experimental.enableTestFixtures=true
# Create BuildConfig files as bytecode to avoid Java compilation phase
android.enableBuildConfigAsBytecode=true
+
+# This should be removed after upgrading to AGP 8.1.0
+android.suppressUnsupportedCompileSdk=34
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 248537b00b..21ba973418 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -4,9 +4,9 @@
[versions]
# Project
android_gradle_plugin = "8.1.0"
-kotlin = "1.8.22"
-ksp = "1.8.22-1.0.11"
-molecule = "1.1.0"
+kotlin = "1.9.0"
+ksp = "1.9.0-1.0.12"
+molecule = "1.2.0"
# AndroidX
material = "1.9.0"
@@ -14,16 +14,16 @@ core = "1.10.1"
datastore = "1.0.0"
constraintlayout = "2.1.4"
constraintlayout_compose = "1.0.1"
-recyclerview = "1.3.0"
+recyclerview = "1.3.1"
lifecycle = "2.6.1"
activity = "1.7.2"
startup = "1.1.1"
-media3 = "1.1.0"
+media3 = "1.1.1"
browser = "1.5.0"
# Compose
compose_bom = "2023.06.01"
-composecompiler = "1.4.8"
+composecompiler = "1.5.0"
# Coroutines
coroutines = "1.7.2"
@@ -45,11 +45,11 @@ dependencycheck = "8.3.1"
dependencyanalysis = "1.20.0"
stem = "2.3.0"
sqldelight = "1.5.5"
-telephoto = "0.4.0"
+telephoto = "0.5.0"
# DI
dagger = "2.47"
-anvil = "2.4.6"
+anvil = "2.4.7-1-8"
# Auto service
autoservice = "1.1.1"
@@ -65,7 +65,7 @@ android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref
android_desugar = "com.android.tools:desugar_jdk_libs:2.0.3"
kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
# https://firebase.google.com/docs/android/setup#available-libraries
-google_firebase_bom = "com.google.firebase:firebase-bom:32.2.0"
+google_firebase_bom = "com.google.firebase:firebase-bom:32.2.2"
# AndroidX
androidx_material = { module = "com.google.android.material:material", version.ref = "material" }
@@ -92,6 +92,7 @@ androidx_startup = { module = "androidx.startup:startup-runtime", version.ref =
androidx_preference = "androidx.preference:preference:1.2.0"
androidx_compose_bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose_bom" }
+androidx_compose_material3 = "androidx.compose.material3:material3:1.2.0-alpha05"
# Coroutines
coroutines_core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
@@ -123,7 +124,7 @@ test_junit = "junit:junit:4.13.2"
test_runner = "androidx.test:runner:1.5.2"
test_uiautomator = "androidx.test.uiautomator:uiautomator:2.2.0"
test_junitext = "androidx.test.ext:junit:1.1.5"
-test_mockk = "io.mockk:mockk:1.13.5"
+test_mockk = "io.mockk:mockk:1.13.7"
test_barista = "com.adevinta.android:barista:4.3.0"
test_hamcrest = "org.hamcrest:hamcrest:2.2"
test_orchestrator = "androidx.test:orchestrator:1.4.2"
@@ -145,7 +146,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" }
timber = "com.jakewharton.timber:timber:5.0.1"
-matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.34"
+matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.42"
sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" }
sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" }
sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
@@ -156,14 +157,14 @@ otaliastudios_transcoder = "com.otaliastudios:transcoder:0.10.5"
vanniktech_blurhash = "com.vanniktech:blurhash:0.1.0"
vanniktech_emoji = "com.vanniktech:emoji-google:0.16.0"
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
-statemachine = "com.freeletics.flowredux:compose:1.1.0"
+statemachine = "com.freeletics.flowredux:compose:1.2.0"
maplibre = "org.maplibre.gl:android-sdk:10.2.0"
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:2.0.0"
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:2.0.0"
# Analytics
posthog = "com.posthog.android:posthog:2.0.3"
-sentry = "io.sentry:sentry-android:6.26.0"
+sentry = "io.sentry:sentry-android:6.28.0"
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:42b2faa417c1e95f430bf8f6e379adba25ad5ef8"
# Di
@@ -196,7 +197,7 @@ kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
anvil = { id = "com.squareup.anvil", version.ref = "anvil" }
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
-ktlint = "org.jlleitschuh.gradle.ktlint:11.5.0"
+ktlint = "org.jlleitschuh.gradle.ktlint:11.5.1"
dependencygraph = { id = "com.savvasdalkitsis.module-dependency-graph", version.ref = "dependencygraph" }
dependencycheck = { id = "org.owasp.dependencycheck", version.ref = "dependencycheck" }
dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyanalysis" }
diff --git a/libraries/androidutils/build.gradle.kts b/libraries/androidutils/build.gradle.kts
index 92e3c46126..57e4ca3569 100644
--- a/libraries/androidutils/build.gradle.kts
+++ b/libraries/androidutils/build.gradle.kts
@@ -37,6 +37,7 @@ dependencies {
implementation(libs.timber)
implementation(libs.androidx.corektx)
implementation(libs.androidx.activity.activity)
+ implementation(libs.androidx.recyclerview)
implementation(libs.androidx.exifinterface)
implementation(libs.androidx.security.crypto)
implementation(libs.androidx.browser)
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/diff/MatrixTimelineItemsDiffCallback.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/diff/DefaultDiffCallback.kt
similarity index 70%
rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/diff/MatrixTimelineItemsDiffCallback.kt
rename to libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/diff/DefaultDiffCallback.kt
index 4a78447bd7..219441d5e6 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/diff/MatrixTimelineItemsDiffCallback.kt
+++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/diff/DefaultDiffCallback.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,14 +14,17 @@
* limitations under the License.
*/
-package io.element.android.features.messages.impl.timeline.diff
+package io.element.android.libraries.androidutils.diff
import androidx.recyclerview.widget.DiffUtil
-import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
-internal class MatrixTimelineItemsDiffCallback(
- private val oldList: List,
- private val newList: List
+/**
+ * Default implementation of [DiffUtil.Callback] that uses [areItemsTheSame] to compare items.
+ */
+internal class DefaultDiffCallback(
+ private val oldList: List,
+ private val newList: List,
+ private val areItemsTheSame: (oldItem: T?, newItem: T?) -> Boolean,
) : DiffUtil.Callback() {
override fun getOldListSize(): Int {
@@ -35,11 +38,7 @@ internal class MatrixTimelineItemsDiffCallback(
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldList.getOrNull(oldItemPosition)
val newItem = newList.getOrNull(newItemPosition)
- return if (oldItem is MatrixTimelineItem.Event && newItem is MatrixTimelineItem.Event) {
- oldItem.uniqueId == newItem.uniqueId
- } else {
- false
- }
+ return areItemsTheSame(oldItem, newItem)
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/diff/DiffCache.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/diff/DiffCache.kt
new file mode 100644
index 0000000000..3d1161e2e0
--- /dev/null
+++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/diff/DiffCache.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.androidutils.diff
+
+/**
+ * A cache that can be used to store some data that can be invalidated when a diff is applied.
+ * The cache is invalidated by the [DiffCacheInvalidator].
+ */
+interface DiffCache {
+ fun get(index: Int): E?
+ fun indices(): IntRange
+ fun isEmpty(): Boolean
+}
+
+/**
+ * A [DiffCache] that can be mutated by adding, removing or updating elements.
+ */
+interface MutableDiffCache : DiffCache {
+ fun removeAt(index: Int): E?
+ fun add(index: Int, element: E?)
+ operator fun set(index: Int, element: E?)
+}
+
+/**
+ * A [MutableDiffCache] backed by a [MutableList].
+ *
+ */
+class MutableListDiffCache(private val mutableList: MutableList = ArrayList()) : MutableDiffCache {
+
+ override fun removeAt(index: Int): E? {
+ return mutableList.removeAt(index)
+ }
+
+ override fun get(index: Int): E? {
+ return mutableList.getOrNull(index)
+ }
+
+ override fun indices(): IntRange {
+ return mutableList.indices
+ }
+
+ override fun isEmpty(): Boolean {
+ return mutableList.isEmpty()
+ }
+
+ override operator fun set(index: Int, element: E?) {
+ mutableList[index] = element
+ }
+
+ override fun add(index: Int, element: E?) {
+ mutableList.add(index, element)
+ }
+}
diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/diff/DiffCacheInvalidator.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/diff/DiffCacheInvalidator.kt
new file mode 100644
index 0000000000..4ebdc3224f
--- /dev/null
+++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/diff/DiffCacheInvalidator.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.androidutils.diff
+
+/**
+ * [DiffCacheInvalidator] is used to invalidate the cache when the list is updated.
+ * It is used by [DiffCacheUpdater].
+ * Check the default implementation [DefaultDiffCacheInvalidator].
+ */
+interface DiffCacheInvalidator {
+ fun onChanged(position: Int, count: Int, cache: MutableDiffCache)
+
+ fun onMoved(fromPosition: Int, toPosition: Int, cache: MutableDiffCache)
+
+ fun onInserted(position: Int, count: Int, cache: MutableDiffCache)
+
+ fun onRemoved(position: Int, count: Int, cache: MutableDiffCache)
+}
+
+/**
+ * Default implementation of [DiffCacheInvalidator].
+ * It invalidates the cache by setting values to null.
+ */
+class DefaultDiffCacheInvalidator : DiffCacheInvalidator {
+
+ override fun onChanged(position: Int, count: Int, cache: MutableDiffCache) {
+ for (i in position until position + count) {
+ // Invalidate cache
+ cache[i] = null
+ }
+ }
+
+ override fun onMoved(fromPosition: Int, toPosition: Int, cache: MutableDiffCache) {
+ val model = cache.removeAt(fromPosition)
+ cache.add(toPosition, model)
+ }
+
+ override fun onInserted(position: Int, count: Int, cache: MutableDiffCache) {
+ repeat(count) {
+ cache.add(position, null)
+ }
+ }
+
+ override fun onRemoved(position: Int, count: Int, cache: MutableDiffCache) {
+ repeat(count) {
+ cache.removeAt(position)
+ }
+ }
+}
diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/diff/DiffCacheUpdater.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/diff/DiffCacheUpdater.kt
new file mode 100644
index 0000000000..500edcb135
--- /dev/null
+++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/diff/DiffCacheUpdater.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.androidutils.diff
+
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListUpdateCallback
+import timber.log.Timber
+import kotlin.system.measureTimeMillis
+
+/**
+ * Class in charge of updating a [MutableDiffCache] according to the cache invalidation rules provided by the [DiffCacheInvalidator].
+ * @param ListItem the type of the items in the list
+ * @param CachedItem the type of the items in the cache
+ * @param diffCache the cache to update
+ * @param detectMoves true if DiffUtil should try to detect moved items, false otherwise
+ * @param cacheInvalidator the invalidator to use to update the cache
+ * @param areItemsTheSame the function to use to compare items
+ */
+class DiffCacheUpdater(
+ private val diffCache: MutableDiffCache,
+ private val detectMoves: Boolean = false,
+ private val cacheInvalidator: DiffCacheInvalidator = DefaultDiffCacheInvalidator(),
+ private val areItemsTheSame: (oldItem: ListItem?, newItem: ListItem?) -> Boolean,
+) {
+
+ private val lock = Object()
+ private var prevOriginalList: List = emptyList()
+
+ private val listUpdateCallback = object : ListUpdateCallback {
+ override fun onInserted(position: Int, count: Int) {
+ cacheInvalidator.onInserted(position, count, diffCache)
+ }
+
+ override fun onRemoved(position: Int, count: Int) {
+ cacheInvalidator.onRemoved(position, count, diffCache)
+ }
+
+ override fun onMoved(fromPosition: Int, toPosition: Int) {
+ cacheInvalidator.onMoved(fromPosition, toPosition, diffCache)
+ }
+
+ override fun onChanged(position: Int, count: Int, payload: Any?) {
+ cacheInvalidator.onChanged(position, count, diffCache)
+ }
+ }
+
+ fun updateWith(newOriginalList: List) = synchronized(lock) {
+ val timeToDiff = measureTimeMillis {
+ val diffCallback = DefaultDiffCallback(prevOriginalList, newOriginalList, areItemsTheSame)
+ val diffResult = DiffUtil.calculateDiff(diffCallback, detectMoves)
+ prevOriginalList = newOriginalList
+ diffResult.dispatchUpdatesTo(listUpdateCallback)
+ }
+ Timber.v("Time to apply diff on new list of ${newOriginalList.size} items: $timeToDiff ms")
+ }
+}
diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt
index 8347990303..a9a17dcceb 100644
--- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt
+++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt
@@ -183,7 +183,7 @@ fun Context.startInstallFromSourceIntent(
noActivityFoundMessage: String = getString(R.string.error_no_compatible_app_found),
) {
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
- .setData(Uri.parse(String.format("package:%s", packageName)))
+ .setData(Uri.parse("package:$packageName"))
try {
activityResultLauncher.launch(intent)
} catch (activityNotFoundException: ActivityNotFoundException) {
diff --git a/libraries/androidutils/src/main/res/values-ru/translations.xml b/libraries/androidutils/src/main/res/values-ru/translations.xml
new file mode 100644
index 0000000000..bb236dc893
--- /dev/null
+++ b/libraries/androidutils/src/main/res/values-ru/translations.xml
@@ -0,0 +1,4 @@
+
+
+ "Не найдено совместимое приложение для обработки этого действия."
+
diff --git a/libraries/androidutils/src/main/res/values/integers.xml b/libraries/androidutils/src/main/res/values/integers.xml
index ecbfa4cdda..2f9e641bdf 100644
--- a/libraries/androidutils/src/main/res/values/integers.xml
+++ b/libraries/androidutils/src/main/res/values/integers.xml
@@ -15,9 +15,9 @@
~ limitations under the License.
-->
-
+
- 1
- 0
+ 1
+ 0
diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/NodeInputs.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/NodeInputs.kt
index b96d9e166b..534c9d741b 100644
--- a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/NodeInputs.kt
+++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/NodeInputs.kt
@@ -23,5 +23,5 @@ import com.bumble.appyx.core.plugin.plugins
interface NodeInputs : Plugin
inline fun Node.inputs(): I {
- return plugins().firstOrNull() ?: throw RuntimeException("Make sure to actually pass NodeInputs plugin to your node")
+ return requireNotNull(plugins().firstOrNull()) { "Make sure to actually pass NodeInputs plugin to your node" }
}
diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/data/Try.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/data/Try.kt
index b91d249547..fe801e71f7 100644
--- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/data/Try.kt
+++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/data/Try.kt
@@ -16,11 +16,11 @@
package io.element.android.libraries.core.data
-inline fun tryOrNull(noinline onError: ((Throwable) -> Unit)? = null, operation: () -> A): A? {
+inline fun tryOrNull(onError: ((Throwable) -> Unit) = { }, operation: () -> A): A? {
return try {
operation()
} catch (any: Throwable) {
- onError?.invoke(any)
+ onError.invoke(any)
null
}
}
diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/BasicExtensions.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/BasicExtensions.kt
index db07432df0..343f5ce351 100644
--- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/BasicExtensions.kt
+++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/BasicExtensions.kt
@@ -72,7 +72,3 @@ fun String.ellipsize(length: Int): String {
return "${this.take(length)}…"
}
-
-inline fun Any?.takeAs(): R? {
- return takeIf { it is R } as R?
-}
diff --git a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageTimestampFormatterTest.kt b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageTimestampFormatterTest.kt
index 483e27af71..5aefcdcd7b 100644
--- a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageTimestampFormatterTest.kt
+++ b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageTimestampFormatterTest.kt
@@ -101,7 +101,7 @@ class DefaultLastMessageTimestampFormatterTest {
* Create DefaultLastMessageFormatter and set current time to the provided date.
*/
private fun createFormatter(@Suppress("SameParameterValue") currentDate: String): LastMessageTimestampFormatter {
- val clock = FakeClock().also { it.givenInstant(Instant.parse(currentDate)) }
+ val clock = FakeClock().apply { givenInstant(Instant.parse(currentDate)) }
val localDateTimeProvider = LocalDateTimeProvider(clock, TimeZone.UTC)
val dateFormatters = DateFormatters(Locale.US, clock, TimeZone.UTC)
return DefaultLastMessageTimestampFormatter(localDateTimeProvider, dateFormatters)
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt
index 0e33567129..47b951baa2 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt
@@ -25,4 +25,6 @@ object VectorIcons {
val DoorOpen = R.drawable.ic_door_open_24
val DeveloperMode = R.drawable.ic_developer_mode
val ReportContent = R.drawable.ic_report_content
+ val Groups = R.drawable.ic_groups
+ val Share = R.drawable.ic_share
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt
index 8f96ed6b6e..dcd1ea11bc 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt
@@ -16,7 +16,6 @@
package io.element.android.libraries.designsystem.atomic.atoms
-import android.graphics.BlurMaskFilter
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@@ -27,24 +26,16 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.clip
-import androidx.compose.ui.draw.drawBehind
-import androidx.compose.ui.geometry.CornerRadius
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.geometry.RoundRect
-import androidx.compose.ui.graphics.ClipOp
+import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Paint
-import androidx.compose.ui.graphics.Path
-import androidx.compose.ui.graphics.drawscope.clipPath
-import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
-import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.R
+import io.element.android.libraries.designsystem.modifiers.blurCompat
+import io.element.android.libraries.designsystem.modifiers.blurredShapeShadow
+import io.element.android.libraries.designsystem.modifiers.canUseBlurMaskFilter
import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.theme.ElementTheme
@@ -53,6 +44,7 @@ import io.element.android.libraries.theme.ElementTheme
fun ElementLogoAtom(
size: ElementLogoAtomSize,
modifier: Modifier = Modifier,
+ useBlurredShadow: Boolean = canUseBlurMaskFilter(),
darkTheme: Boolean = isSystemInDarkTheme(),
) {
val blur = if (darkTheme) 160.dp else 24.dp
@@ -66,22 +58,35 @@ fun ElementLogoAtom(
.border(size.borderWidth, borderColor, RoundedCornerShape(size.cornerRadius)),
contentAlignment = Alignment.Center,
) {
- Box(
- Modifier
- .size(size.outerSize)
- .shapeShadow(
- color = shadowColor,
- cornerRadius = size.cornerRadius,
- blurRadius = size.shadowRadius,
- offsetY = 8.dp,
- )
- )
+ if (useBlurredShadow) {
+ Box(
+ Modifier
+ .size(size.outerSize)
+ .blurredShapeShadow(
+ color = shadowColor,
+ cornerRadius = size.cornerRadius,
+ blurRadius = size.shadowRadius,
+ offsetY = 8.dp,
+ )
+ )
+ } else {
+ Box(
+ Modifier
+ .size(size.outerSize)
+ .shadow(
+ elevation = size.shadowRadius,
+ shape = RoundedCornerShape(size.cornerRadius),
+ clip = false,
+ ambientColor = shadowColor
+ )
+ )
+ }
Box(
Modifier
.clip(RoundedCornerShape(size.cornerRadius))
.size(size.outerSize)
.background(backgroundColor)
- .blur(blur)
+ .blurCompat(blur)
)
Image(
modifier = Modifier.size(size.logoSize),
@@ -121,44 +126,6 @@ sealed class ElementLogoAtomSize(
)
}
-fun Modifier.shapeShadow(
- color: Color = Color.Black,
- cornerRadius: Dp = 0.dp,
- offsetX: Dp = 0.dp,
- offsetY: Dp = 0.dp,
- blurRadius: Dp = 0.dp,
-) = then(
- drawBehind {
- drawIntoCanvas { canvas ->
- val path = Path().apply {
- addRoundRect(RoundRect(Rect(Offset.Zero, size), CornerRadius(cornerRadius.toPx())))
- }
-
- clipPath(path, ClipOp.Difference) {
- val paint = Paint()
- val frameworkPaint = paint.asFrameworkPaint()
- if (blurRadius != 0.dp) {
- frameworkPaint.maskFilter = BlurMaskFilter(blurRadius.toPx(), BlurMaskFilter.Blur.NORMAL)
- }
- frameworkPaint.color = color.toArgb()
-
- val leftPixel = offsetX.toPx()
- val topPixel = offsetY.toPx()
- val rightPixel = size.width + topPixel
- val bottomPixel = size.height + leftPixel
-
- canvas.drawRect(
- left = leftPixel,
- top = topPixel,
- right = rightPixel,
- bottom = bottomPixel,
- paint = paint,
- )
- }
- }
- }
-)
-
@Composable
@DayNightPreviews
internal fun ElementLogoAtomMediumPreview() {
@@ -172,7 +139,19 @@ internal fun ElementLogoAtomLargePreview() {
}
@Composable
-private fun ContentToPreview(elementLogoAtomSize: ElementLogoAtomSize) {
+@DayNightPreviews
+internal fun ElementLogoAtomMediumNoBlurShadowPreview() {
+ ContentToPreview(ElementLogoAtomSize.Medium, useBlurredShadow = false)
+}
+
+@Composable
+@DayNightPreviews
+internal fun ElementLogoAtomLargeNoBlurShadowPreview() {
+ ContentToPreview(ElementLogoAtomSize.Large, useBlurredShadow = false)
+}
+
+@Composable
+private fun ContentToPreview(elementLogoAtomSize: ElementLogoAtomSize, useBlurredShadow: Boolean = true) {
ElementPreview {
Box(
Modifier
@@ -180,7 +159,7 @@ private fun ContentToPreview(elementLogoAtomSize: ElementLogoAtomSize) {
.background(ElementTheme.colors.bgSubtlePrimary),
contentAlignment = Alignment.Center
) {
- ElementLogoAtom(elementLogoAtomSize)
+ ElementLogoAtom(elementLogoAtomSize, useBlurredShadow = useBlurredShadow)
}
}
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/InfoListItemMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/InfoListItemMolecule.kt
index 6b20c96880..2af9e77b99 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/InfoListItemMolecule.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/InfoListItemMolecule.kt
@@ -70,7 +70,7 @@ fun InfoListItemMolecule(
@DayNightPreviews
@Composable
-fun InfoListItemMoleculePreview() {
+internal fun InfoListItemMoleculePreview() {
ElementPreview {
val color = if (isSystemInDarkTheme()) Color.DarkGray else Color.LightGray
Column(
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ButtonColumnMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ButtonColumnMolecule.kt
index a8ad97a950..9fc688f227 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ButtonColumnMolecule.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ButtonColumnMolecule.kt
@@ -28,7 +28,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.components.Button
-import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.TextButton
@Composable
@@ -59,11 +59,8 @@ internal fun ButtonColumnMoleculeDarkPreview() =
@Composable
private fun ContentToPreview() {
ButtonColumnMolecule {
- Button(onClick = {}, modifier = Modifier.fillMaxWidth()) {
- Text(text = "Button")
- }
- TextButton(onClick = {}, modifier = Modifier.fillMaxWidth()) {
- Text(text = "TextButton")
- }
+ Button(text = "Button", onClick = {}, modifier = Modifier.fillMaxWidth())
+ OutlinedButton(text = "OutlinedButton", onClick = {}, modifier = Modifier.fillMaxWidth())
+ TextButton(text = "TextButton", onClick = {}, modifier = Modifier.fillMaxWidth())
}
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ButtonRowMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ButtonRowMolecule.kt
index b8b5a2146f..7c03a2106e 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ButtonRowMolecule.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ButtonRowMolecule.kt
@@ -25,7 +25,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.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
@Composable
@@ -54,11 +53,7 @@ internal fun ButtonRowMoleculeDarkPreview() =
@Composable
private fun ContentToPreview() {
ButtonRowMolecule {
- TextButton(onClick = { }) {
- Text("Button 1")
- }
- TextButton(onClick = { }) {
- Text("Button 2")
- }
+ TextButton(text = "Button 1", onClick = {})
+ TextButton(text = "Button 2", onClick = {})
}
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitlePlaceholdersRowMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitlePlaceholdersRowMolecule.kt
new file mode 100644
index 0000000000..1a10aa2886
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitlePlaceholdersRowMolecule.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.designsystem.atomic.molecules
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+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.foundation.shape.CircleShape
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
+import io.element.android.libraries.designsystem.components.avatar.AvatarSize
+import io.element.android.libraries.designsystem.preview.DayNightPreviews
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.theme.placeholderBackground
+import io.element.android.libraries.theme.ElementTheme
+
+@Composable
+fun IconTitlePlaceholdersRowMolecule(
+ iconSize: Dp,
+ modifier: Modifier = Modifier,
+ horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
+ verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
+) {
+ Row(
+ modifier = modifier,
+ horizontalArrangement = horizontalArrangement,
+ verticalAlignment = verticalAlignment,
+ ) {
+ Box(
+ modifier = Modifier
+ .size(iconSize)
+ .align(Alignment.CenterVertically)
+ .background(color = ElementTheme.colors.placeholderBackground, shape = CircleShape)
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ PlaceholderAtom(width = 20.dp, height = 7.dp)
+ Spacer(modifier = Modifier.width(7.dp))
+ PlaceholderAtom(width = 45.dp, height = 7.dp)
+ }
+}
+
+@DayNightPreviews
+@Composable
+internal fun IconTitlePlaceholdersRowMoleculePreview() = ElementPreview {
+ IconTitlePlaceholdersRowMolecule(
+ iconSize = AvatarSize.TimelineRoom.dp,
+ )
+}
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 eb9749498a..21bddd67a1 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
@@ -109,7 +109,7 @@ private fun ContentToPreview() {
linkAnnotationTag = "",
onClick = {},
onLongClick = {},
- interactionSource = MutableInteractionSource(),
+ interactionSource = remember { MutableInteractionSource() },
)
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledTextField.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledTextField.kt
index cc1d92ccd8..4856d54bff 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledTextField.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledTextField.kt
@@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -39,6 +40,7 @@ fun LabelledTextField(
placeholder: String? = null,
singleLine: Boolean = false,
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
+ keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
onValueChange: (String) -> Unit = {},
) {
Column(
@@ -59,17 +61,18 @@ fun LabelledTextField(
onValueChange = onValueChange,
singleLine = singleLine,
maxLines = maxLines,
+ keyboardOptions = keyboardOptions,
)
}
}
@Preview
@Composable
-fun LabelledTextFieldLightPreview() = ElementPreviewLight { ContentToPreview() }
+internal fun LabelledTextFieldLightPreview() = ElementPreviewLight { ContentToPreview() }
@Preview
@Composable
-fun LabelledTextFieldDarkPreview() = ElementPreviewDark { ContentToPreview() }
+internal fun LabelledTextFieldDarkPreview() = ElementPreviewDark { ContentToPreview() }
@Composable
private fun ContentToPreview() {
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/PinIcon.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/PinIcon.kt
index c6af3e1cdf..93a0c5d436 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/PinIcon.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/PinIcon.kt
@@ -51,6 +51,6 @@ fun PinIcon(
@DayNightPreviews
@Composable
-fun PinIconPreview() = ElementPreview {
+internal fun PinIconPreview() = ElementPreview {
PinIcon()
}
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 a5ad996ea7..20589c89ee 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
@@ -35,7 +35,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.components.dialogs.DialogPreview
+import io.element.android.libraries.designsystem.theme.components.DialogPreview
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
@@ -129,9 +129,10 @@ private fun ProgressDialogContent(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.BottomEnd
) {
- TextButton(onClick = onCancelClicked) {
- Text(stringResource(id = CommonStrings.action_cancel))
- }
+ TextButton(
+ text = stringResource(id = CommonStrings.action_cancel),
+ onClick = onCancelClicked,
+ )
}
}
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncFailure.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncFailure.kt
index b5aa9b85d3..d30b78ca7f 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncFailure.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncFailure.kt
@@ -48,9 +48,10 @@ fun AsyncFailure(
Text(text = throwable.message ?: stringResource(id = CommonStrings.error_unknown))
if (onRetry != null) {
Spacer(modifier = Modifier.height(24.dp))
- Button(onClick = onRetry) {
- Text(text = stringResource(id = CommonStrings.action_retry))
- }
+ Button(
+ text = stringResource(id = CommonStrings.action_retry),
+ onClick = onRetry
+ )
}
}
}
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 b28e52a5ff..2e2d98ddbb 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
@@ -31,6 +31,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
@@ -91,10 +92,14 @@ private fun InitialsAvatar(
Box(
modifier.background(color = avatarColor),
) {
+ val fontSize = avatarData.size.dp.toSp() / 2
+ val originalFont = ElementTheme.typography.fontBodyMdRegular
+ val ratio = fontSize.value / originalFont.fontSize.value
+ val lineHeight = originalFont.lineHeight * ratio
Text(
modifier = Modifier.align(Alignment.Center),
text = avatarData.initial,
- style = ElementTheme.typography.fontBodyMdRegular.copy(fontSize = avatarData.size.dp.toSp() / 2),
+ style = originalFont.copy(fontSize = fontSize, lineHeight = lineHeight, letterSpacing = 0.sp),
color = Color.White,
)
}
@@ -102,7 +107,7 @@ private fun InitialsAvatar(
@Preview(group = PreviewGroup.Avatars)
@Composable
-fun AvatarPreview(@PreviewParameter(AvatarDataProvider::class) avatarData: AvatarData) =
+internal fun AvatarPreview(@PreviewParameter(AvatarDataProvider::class) avatarData: AvatarData) =
ElementThemedPreview {
Row(
verticalAlignment = Alignment.CenterVertically,
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarDataProvider.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarDataProvider.kt
index 1727fffd1c..14c7ad3eff 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarDataProvider.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarDataProvider.kt
@@ -20,19 +20,16 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
open class AvatarDataProvider : PreviewParameterProvider {
override val values: Sequence
- get() {
- AvatarSize.values()
- .also { it.sortBy { item -> item.name } }
- .asSequence()
- return AvatarSize.values().asSequence().map {
+ get() = AvatarSize.values()
+ .asSequence()
+ .map {
sequenceOf(
anAvatarData(size = it),
anAvatarData(size = it).copy(name = null),
anAvatarData(size = it).copy(url = "aUrl"),
)
}
- .flatten()
- }
+ .flatten()
}
fun anAvatarData(
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/ButtonVisuals.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/ButtonVisuals.kt
new file mode 100644
index 0000000000..24f3989f66
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/ButtonVisuals.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.components.button
+
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.Composable
+import io.element.android.libraries.designsystem.theme.components.Button
+import io.element.android.libraries.designsystem.theme.components.IconButton
+import io.element.android.libraries.designsystem.theme.components.IconSource
+import io.element.android.libraries.designsystem.theme.components.TextButton
+
+/**
+ * A sealed class that represents the different visual styles that a button can have.
+ */
+sealed interface ButtonVisuals {
+
+ val action: () -> Unit
+
+ /**
+ * Creates a [Button] composable based on the visual state.
+ */
+ @Composable
+ fun Composable()
+
+ data class Text(val text: String, override val action: () -> Unit) : ButtonVisuals {
+ @Composable
+ override fun Composable() {
+ TextButton(text = text, onClick = action)
+ }
+ }
+ data class Icon(val iconSource: IconSource, override val action: () -> Unit) : ButtonVisuals {
+ @Composable
+ override fun Composable() {
+ IconButton(onClick = action) {
+ Icon(iconSource.getPainter(), iconSource.contentDescription)
+ }
+ }
+ }
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/ButtonWithProgress.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/ButtonWithProgress.kt
deleted file mode 100644
index 06702de253..0000000000
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/ButtonWithProgress.kt
+++ /dev/null
@@ -1,107 +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.components.button
-
-import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.progressSemantics
-import androidx.compose.material3.ButtonColors
-import androidx.compose.material3.ButtonElevation
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import io.element.android.libraries.designsystem.preview.ElementThemedPreview
-import io.element.android.libraries.designsystem.preview.PreviewGroup
-import io.element.android.libraries.designsystem.theme.aliasButtonText
-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.ElementButtonDefaults
-import io.element.android.libraries.designsystem.theme.components.Text
-import io.element.android.libraries.theme.ElementTheme
-
-/**
- * A component that will display a button with an indeterminate circular progressbar.
- * When [showProgress] is true:
- * - A circular progressbar is displayed.
- * - [text] is replaced by [progressText], if defined.
- * - [onClick] gets disabled.
- */
-@Composable
-fun ButtonWithProgress(
- text: String?,
- onClick: () -> Unit,
- modifier: Modifier = Modifier,
- showProgress: Boolean = false,
- progressText: String? = text,
- enabled: Boolean = true,
- shape: Shape = ElementButtonDefaults.shape,
- colors: ButtonColors = ElementButtonDefaults.buttonColors(),
- elevation: ButtonElevation? = ElementButtonDefaults.buttonElevation(),
- border: BorderStroke? = null,
- contentPadding: PaddingValues = ElementButtonDefaults.ContentPadding,
- interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-) {
- Button(
- onClick = {
- if (!showProgress) {
- onClick()
- }
- },
- modifier = modifier,
- enabled = enabled,
- shape = shape,
- colors = colors,
- elevation = elevation,
- border = border,
- contentPadding = contentPadding,
- interactionSource = interactionSource,
- ) {
- if (showProgress) {
- CircularProgressIndicator(
- modifier = Modifier
- .progressSemantics()
- .size(18.dp),
- color = MaterialTheme.colorScheme.onPrimary,
- strokeWidth = 2.dp,
- )
- if (progressText != null) {
- Spacer(Modifier.width(10.dp))
- Text(progressText, style = ElementTheme.typography.aliasButtonText)
- }
- } else if (text != null) {
- Text(text, style = ElementTheme.typography.aliasButtonText)
- }
- }
-}
-
-@Preview(group = PreviewGroup.Buttons)
-@Composable
-internal fun ButtonWithProgressPreview() = ElementThemedPreview {
- ButtonWithProgress(
- text = "Button with progress",
- onClick = {},
- showProgress = true,
- )
-}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/MainActionButton.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/MainActionButton.kt
index c97c1cc59e..76c256d967 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/MainActionButton.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/MainActionButton.kt
@@ -30,6 +30,7 @@ import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
@@ -51,7 +52,7 @@ fun MainActionButton(
contentDescription: String = title,
) {
val ripple = rememberRipple(bounded = false)
- val interactionSource = MutableInteractionSource()
+ val interactionSource = remember { MutableInteractionSource() }
Column(
modifier.clickable(
enabled = enabled,
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 b11c9f878d..0c2fca84d5 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
@@ -17,20 +17,15 @@
package io.element.android.libraries.designsystem.components.dialogs
import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.tooling.preview.PreviewParameter
-import androidx.compose.ui.unit.Dp
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
-import io.element.android.libraries.designsystem.utils.BooleanProvider
+import io.element.android.libraries.designsystem.theme.components.DialogPreview
+import io.element.android.libraries.designsystem.theme.components.SimpleAlertDialogContent
import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@@ -44,16 +39,8 @@ fun ConfirmationDialog(
submitText: String = stringResource(id = CommonStrings.action_ok),
cancelText: String = stringResource(id = CommonStrings.action_cancel),
thirdButtonText: String? = null,
- emphasizeSubmitButton: Boolean = false,
onCancelClicked: () -> Unit = onDismiss,
onThirdButtonClicked: () -> Unit = {},
- shape: Shape = AlertDialogDefaults.shape,
- containerColor: Color = AlertDialogDefaults.containerColor,
- iconContentColor: Color = AlertDialogDefaults.iconContentColor,
- // According to the design team, `primary` should be used here instead of the default `onSurface`
- titleContentColor: Color = MaterialTheme.colorScheme.primary,
- textContentColor: Color = AlertDialogDefaults.textContentColor,
- tonalElevation: Dp = AlertDialogDefaults.TonalElevation,
) {
AlertDialog(modifier = modifier, onDismissRequest = onDismiss) {
ConfirmationDialogContent(
@@ -65,13 +52,6 @@ fun ConfirmationDialog(
onSubmitClicked = onSubmitClicked,
onCancelClicked = onCancelClicked,
onThirdButtonClicked = onThirdButtonClicked,
- shape = shape,
- containerColor = containerColor,
- iconContentColor = iconContentColor,
- titleContentColor = titleContentColor,
- textContentColor = textContentColor,
- tonalElevation = tonalElevation,
- emphasizeSubmitButton = emphasizeSubmitButton,
)
}
}
@@ -87,13 +67,6 @@ private fun ConfirmationDialogContent(
title: String? = null,
thirdButtonText: String? = null,
onThirdButtonClicked: () -> Unit = {},
- emphasizeSubmitButton: Boolean = false,
- shape: Shape = AlertDialogDefaults.shape,
- containerColor: Color = AlertDialogDefaults.containerColor,
- iconContentColor: Color = AlertDialogDefaults.iconContentColor,
- titleContentColor: Color = AlertDialogDefaults.titleContentColor,
- textContentColor: Color = AlertDialogDefaults.textContentColor,
- tonalElevation: Dp = AlertDialogDefaults.TonalElevation,
icon: @Composable (() -> Unit)? = null,
) {
SimpleAlertDialogContent(
@@ -106,21 +79,14 @@ private fun ConfirmationDialogContent(
onCancelClicked = onCancelClicked,
thirdButtonText = thirdButtonText,
onThirdButtonClicked = onThirdButtonClicked,
- emphasizeSubmitButton = emphasizeSubmitButton,
- shape = shape,
- containerColor = containerColor,
- iconContentColor = iconContentColor,
- titleContentColor = titleContentColor,
- textContentColor = textContentColor,
- tonalElevation = tonalElevation,
icon = icon,
)
}
@Preview(group = PreviewGroup.Dialogs)
@Composable
-internal fun ConfirmationDialogPreview(@PreviewParameter(BooleanProvider::class) emphasizeSubmitButton: Boolean) =
- ElementThemedPreview {
+internal fun ConfirmationDialogPreview() =
+ ElementThemedPreview(showBackground = false) {
DialogPreview {
ConfirmationDialogContent(
content = "Content",
@@ -130,7 +96,7 @@ internal fun ConfirmationDialogPreview(@PreviewParameter(BooleanProvider::class)
thirdButtonText = "Disable",
onSubmitClicked = {},
onCancelClicked = {},
- emphasizeSubmitButton = emphasizeSubmitButton,
+ onThirdButtonClicked = {},
)
}
}
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 d69281b6e9..fb5c511bf4 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
@@ -17,17 +17,15 @@
package io.element.android.libraries.designsystem.components.dialogs
import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
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 io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
+import io.element.android.libraries.designsystem.theme.components.DialogPreview
+import io.element.android.libraries.designsystem.theme.components.SimpleAlertDialogContent
import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@@ -38,12 +36,6 @@ fun ErrorDialog(
title: String = ErrorDialogDefaults.title,
submitText: String = ErrorDialogDefaults.submitText,
onDismiss: () -> Unit = {},
- shape: Shape = AlertDialogDefaults.shape,
- containerColor: Color = AlertDialogDefaults.containerColor,
- iconContentColor: Color = AlertDialogDefaults.iconContentColor,
- titleContentColor: Color = AlertDialogDefaults.titleContentColor,
- textContentColor: Color = AlertDialogDefaults.textContentColor,
- tonalElevation: Dp = AlertDialogDefaults.TonalElevation,
) {
AlertDialog(modifier = modifier, onDismissRequest = onDismiss) {
ErrorDialogContent(
@@ -51,12 +43,6 @@ fun ErrorDialog(
content = content,
submitText = submitText,
onSubmitText = onDismiss,
- shape = shape,
- containerColor = containerColor,
- iconContentColor = iconContentColor,
- titleContentColor = titleContentColor,
- textContentColor = textContentColor,
- tonalElevation = tonalElevation,
)
}
}
@@ -68,12 +54,6 @@ private fun ErrorDialogContent(
title: String = ErrorDialogDefaults.title,
submitText: String = ErrorDialogDefaults.submitText,
onSubmitText: () -> Unit = {},
- shape: Shape = AlertDialogDefaults.shape,
- containerColor: Color = AlertDialogDefaults.containerColor,
- iconContentColor: Color = AlertDialogDefaults.iconContentColor,
- titleContentColor: Color = AlertDialogDefaults.titleContentColor,
- textContentColor: Color = AlertDialogDefaults.textContentColor,
- tonalElevation: Dp = AlertDialogDefaults.TonalElevation,
) {
SimpleAlertDialogContent(
modifier = modifier,
@@ -81,12 +61,6 @@ private fun ErrorDialogContent(
content = content,
cancelText = submitText,
onCancelClicked = onSubmitText,
- shape = shape,
- containerColor = containerColor,
- iconContentColor = iconContentColor,
- titleContentColor = titleContentColor,
- textContentColor = textContentColor,
- tonalElevation = tonalElevation,
)
}
@@ -98,7 +72,7 @@ object ErrorDialogDefaults {
@Preview(group = PreviewGroup.Dialogs)
@Composable
internal fun ErrorDialogPreview() {
- ElementThemedPreview {
+ ElementThemedPreview(showBackground = false) {
DialogPreview {
ErrorDialogContent(
content = "Content",
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/RetryDialog.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/RetryDialog.kt
index 5e22779085..85447b940b 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/RetryDialog.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/RetryDialog.kt
@@ -17,21 +17,18 @@
package io.element.android.libraries.designsystem.components.dialogs
import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.AlertDialogDefaults
-import androidx.compose.material3.TextButton
+import androidx.compose.material3.ExperimentalMaterial3Api
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 io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
-import io.element.android.libraries.designsystem.theme.components.Text
-import io.element.android.libraries.theme.ElementTheme
+import io.element.android.libraries.designsystem.theme.components.DialogPreview
+import io.element.android.libraries.designsystem.theme.components.SimpleAlertDialogContent
import io.element.android.libraries.ui.strings.CommonStrings
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RetryDialog(
content: String,
@@ -41,51 +38,17 @@ fun RetryDialog(
dismissText: String = RetryDialogDefaults.dismissText,
onRetry: () -> Unit = {},
onDismiss: () -> Unit = {},
- shape: Shape = AlertDialogDefaults.shape,
- containerColor: Color = AlertDialogDefaults.containerColor,
- iconContentColor: Color = AlertDialogDefaults.iconContentColor,
- titleContentColor: Color = AlertDialogDefaults.titleContentColor,
- textContentColor: Color = AlertDialogDefaults.textContentColor,
- tonalElevation: Dp = AlertDialogDefaults.TonalElevation,
) {
- AlertDialog(
- modifier = modifier,
- onDismissRequest = onDismiss,
- title = {
- Text(
- text = title,
- style = ElementTheme.typography.fontHeadingSmRegular,
- )
- },
- text = {
- Text(
- text = content,
- style = ElementTheme.typography.fontBodyMdRegular,
- )
- },
- confirmButton = {
- TextButton(onClick = onRetry) {
- Text(
- text = retryText,
- style = ElementTheme.typography.fontBodyMdRegular,
- )
- }
- },
- dismissButton = {
- TextButton(onClick = onDismiss) {
- Text(
- text = dismissText,
- style = ElementTheme.typography.fontBodyMdRegular,
- )
- }
- },
- shape = shape,
- containerColor = containerColor,
- iconContentColor = iconContentColor,
- titleContentColor = titleContentColor,
- textContentColor = textContentColor,
- tonalElevation = tonalElevation,
- )
+ AlertDialog(modifier = modifier, onDismissRequest = onDismiss) {
+ RetryDialogContent(
+ title = title,
+ content = content,
+ retryText = retryText,
+ dismissText = dismissText,
+ onRetry = onRetry,
+ onDismiss = onDismiss,
+ )
+ }
}
@Composable
@@ -97,12 +60,6 @@ private fun RetryDialogContent(
dismissText: String = RetryDialogDefaults.dismissText,
onRetry: () -> Unit = {},
onDismiss: () -> Unit = {},
- shape: Shape = AlertDialogDefaults.shape,
- containerColor: Color = AlertDialogDefaults.containerColor,
- iconContentColor: Color = AlertDialogDefaults.iconContentColor,
- titleContentColor: Color = AlertDialogDefaults.titleContentColor,
- textContentColor: Color = AlertDialogDefaults.textContentColor,
- tonalElevation: Dp = AlertDialogDefaults.TonalElevation,
) {
SimpleAlertDialogContent(
modifier = modifier,
@@ -112,12 +69,6 @@ private fun RetryDialogContent(
onSubmitClicked = onRetry,
cancelText = dismissText,
onCancelClicked = onDismiss,
- shape = shape,
- containerColor = containerColor,
- iconContentColor = iconContentColor,
- titleContentColor = titleContentColor,
- textContentColor = textContentColor,
- tonalElevation = tonalElevation,
)
}
@@ -129,13 +80,12 @@ object RetryDialogDefaults {
@Preview(group = PreviewGroup.Dialogs)
@Composable
-internal fun RetryDialogPreview() = ElementThemedPreview { ContentToPreview() }
-
-@Composable
-private fun ContentToPreview() {
- DialogPreview {
- RetryDialogContent(
- content = "Content",
- )
+internal fun RetryDialogPreview() {
+ ElementThemedPreview(showBackground = false) {
+ DialogPreview {
+ RetryDialogContent(
+ content = "Content",
+ )
+ }
}
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceCheckbox.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceCheckbox.kt
index 02c0e4eb05..50b5ab2fe4 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceCheckbox.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceCheckbox.kt
@@ -17,6 +17,8 @@
package io.element.android.libraries.designsystem.components.preferences
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -35,6 +37,7 @@ import io.element.android.libraries.designsystem.preview.PreviewGroup
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
+import io.element.android.libraries.designsystem.toSecondaryEnabledColor
import io.element.android.libraries.theme.ElementTheme
@Composable
@@ -42,6 +45,7 @@ fun PreferenceCheckbox(
title: String,
isChecked: Boolean,
modifier: Modifier = Modifier,
+ supportingText: String? = null,
enabled: Boolean = true,
icon: ImageVector? = null,
showIconAreaIfNoIcon: Boolean = false,
@@ -60,13 +64,23 @@ fun PreferenceCheckbox(
enabled = enabled,
isVisible = showIconAreaIfNoIcon
)
- Text(
- modifier = Modifier
- .weight(1f),
- style = ElementTheme.typography.fontBodyLgRegular,
- text = title,
- color = enabled.toEnabledColor(),
- )
+ Column(
+ modifier = Modifier.weight(1f),
+ verticalArrangement = Arrangement.spacedBy(4.dp),
+ ) {
+ Text(
+ style = ElementTheme.typography.fontBodyLgRegular,
+ text = title,
+ color = enabled.toEnabledColor(),
+ )
+ if (supportingText != null) {
+ Text(
+ style = ElementTheme.typography.fontBodyMdRegular,
+ text = supportingText,
+ color = enabled.toSecondaryEnabledColor(),
+ )
+ }
+ }
Checkbox(
modifier = Modifier
.align(Alignment.CenterVertically),
@@ -83,10 +97,19 @@ internal fun PreferenceCheckboxPreview() = ElementThemedPreview { ContentToPrevi
@Composable
private fun ContentToPreview() {
- PreferenceCheckbox(
- title = "Checkbox",
- icon = Icons.Default.Announcement,
- enabled = true,
- isChecked = true
- )
+ Column {
+ PreferenceCheckbox(
+ title = "Checkbox",
+ icon = Icons.Default.Announcement,
+ enabled = true,
+ isChecked = true
+ )
+ PreferenceCheckbox(
+ title = "Checkbox with supporting text",
+ supportingText = "Supporting text",
+ icon = Icons.Default.Announcement,
+ enabled = true,
+ isChecked = true
+ )
+ }
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceDivider.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceDivider.kt
index 2e4b9e196b..348a12bdd9 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceDivider.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceDivider.kt
@@ -21,14 +21,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
-import io.element.android.libraries.designsystem.theme.components.Divider
+import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.theme.ElementTheme
@Composable
fun PreferenceDivider(
modifier: Modifier = Modifier,
) {
- Divider(
+ HorizontalDivider(
modifier = modifier,
color = ElementTheme.colors.borderDisabled,
)
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 bbd3583688..b9a8d267f5 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
@@ -27,7 +27,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Announcement
-import androidx.compose.material3.Switch
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -37,6 +36,7 @@ import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.components.preferences.components.PreferenceIcon
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
+import io.element.android.libraries.designsystem.theme.components.Switch
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.toEnabledColor
import io.element.android.libraries.designsystem.toSecondaryEnabledColor
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Blur.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Blur.kt
new file mode 100644
index 0000000000..fb3eb86c96
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Blur.kt
@@ -0,0 +1,103 @@
+/*
+ * 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.modifiers
+
+import android.graphics.BlurMaskFilter
+import android.os.Build
+import androidx.annotation.ChecksSdkIntAtLeast
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.draw.BlurredEdgeTreatment
+import androidx.compose.ui.draw.blur
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.RoundRect
+import androidx.compose.ui.graphics.ClipOp
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Paint
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.drawscope.clipPath
+import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+/**
+ * @return true if the blur modifier is supported on the current OS version.
+ *
+ * The docs say the `blur` modifier is only supported on Android 12+:
+ * https://developer.android.com/reference/kotlin/androidx/compose/ui/Modifier#(androidx.compose.ui.Modifier).blur(androidx.compose.ui.unit.Dp,androidx.compose.ui.draw.BlurredEdgeTreatment)
+ * */
+@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.S)
+fun canUseBlur(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
+
+@Composable
+fun canUseBlurMaskFilter() = !LocalView.current.isHardwareAccelerated
+
+fun Modifier.blurredShapeShadow(
+ color: Color = Color.Black,
+ cornerRadius: Dp = 0.dp,
+ offsetX: Dp = 0.dp,
+ offsetY: Dp = 0.dp,
+ blurRadius: Dp = 0.dp,
+) = then(
+ drawBehind {
+ drawIntoCanvas { canvas ->
+ val path = Path().apply {
+ addRoundRect(RoundRect(Rect(Offset.Zero, size), CornerRadius(cornerRadius.toPx())))
+ }
+
+ // Draw the blurred shadow, then cut out the shape from it
+ clipPath(path, ClipOp.Difference) {
+ val paint = Paint()
+ val frameworkPaint = paint.asFrameworkPaint()
+ if (blurRadius != 0.dp) {
+ frameworkPaint.maskFilter = BlurMaskFilter(blurRadius.toPx(), BlurMaskFilter.Blur.NORMAL)
+ }
+ frameworkPaint.color = color.toArgb()
+
+ val leftPixel = offsetX.toPx()
+ val topPixel = offsetY.toPx()
+ val rightPixel = size.width + topPixel
+ val bottomPixel = size.height + leftPixel
+
+ canvas.drawRect(
+ left = leftPixel,
+ top = topPixel,
+ right = rightPixel,
+ bottom = bottomPixel,
+ paint = paint,
+ )
+ }
+ }
+ }
+)
+
+fun Modifier.blurCompat(
+ radius: Dp,
+ edgeTreatment: BlurredEdgeTreatment = BlurredEdgeTreatment.Rectangle
+): Modifier = composed {
+ when {
+ radius.value == 0f -> this
+ canUseBlur() -> blur(radius, edgeTreatment)
+ else -> this // Added in case we find a way to make this work on older devices
+ }
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/DayNightPreviews.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/DayNightPreviews.kt
index 201d6f7151..b91e6a1024 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/DayNightPreviews.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/DayNightPreviews.kt
@@ -42,6 +42,13 @@ const val DAY_MODE_NAME = "D"
*
* NB: Content should be wrapped into [ElementPreview] to apply proper theming.
*/
-@Preview(name = DAY_MODE_NAME)
-@Preview(name = NIGHT_MODE_NAME, uiMode = Configuration.UI_MODE_NIGHT_YES)
+@Preview(
+ name = DAY_MODE_NAME,
+ fontScale = 1f,
+)
+@Preview(
+ name = NIGHT_MODE_NAME,
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+ fontScale = 1f,
+)
annotation class DayNightPreviews
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/PreviewGroup.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/PreviewGroup.kt
index aaff9a49f9..b704d0a26e 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/PreviewGroup.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/PreviewGroup.kt
@@ -30,6 +30,7 @@ object PreviewGroup {
const val Preferences = "Preferences"
const val Progress = "Progress Indicators"
const val Search = "Search views"
+ const val Snackbars = "Snackbars"
const val Sliders = "Sliders"
const val Text = "Text"
const val TextFields = "TextFields"
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/SheetState.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/SheetState.kt
index a83bc5708c..dec26f548f 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/SheetState.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/SheetState.kt
@@ -19,9 +19,13 @@ package io.element.android.libraries.designsystem.preview
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SheetState
import androidx.compose.material3.SheetValue
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalDensity
@OptIn(ExperimentalMaterial3Api::class)
-val sheetStateForPreview = SheetState(
+@Composable
+fun sheetStateForPreview() = SheetState(
skipPartiallyExpanded = true,
initialValue = SheetValue.Expanded,
+ density = LocalDensity.current,
)
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/WithFontScale.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/WithFontScale.kt
new file mode 100644
index 0000000000..6d3ecfc82b
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/WithFontScale.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.designsystem.preview
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Density
+
+/**
+ * Showkase does not take into account the `fontScale` parameter of the Preview annotation, so alter the
+ * LocalDensity in the CompositionLocalProvider.
+ */
+@Composable
+fun WithFontScale(fontScale: Float, content: @Composable () -> Unit) {
+ CompositionLocalProvider(
+ LocalDensity provides Density(
+ density = LocalDensity.current.density,
+ fontScale = fontScale
+ )
+ ) {
+ content()
+ }
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/ruler/WithRulers.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/ruler/WithRulers.kt
index a5fc895e5d..58c9a44709 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/ruler/WithRulers.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/ruler/WithRulers.kt
@@ -24,8 +24,8 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
+import io.element.android.libraries.designsystem.theme.components.ButtonSize
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
-import io.element.android.libraries.designsystem.theme.components.Text
/**
* Debug tool to add a vertical and a horizontal ruler on top of the content.
@@ -76,8 +76,10 @@ internal fun WithRulerDarkPreview() =
@Composable
private fun ContentToPreview() {
WithRulers(xRulersOffset = 20.dp, yRulersOffset = 15.dp) {
- OutlinedButton(onClick = {}) {
- Text(text = "A Button with rulers on it!")
- }
+ OutlinedButton(
+ text = "A Button with rulers on it!",
+ size = ButtonSize.Medium,
+ onClick = {},
+ )
}
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/DpScale.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/DpScale.kt
new file mode 100644
index 0000000000..c6408b662e
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/DpScale.kt
@@ -0,0 +1,126 @@
+/*
+ * 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.text
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
+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.ElementPreviewLight
+import io.element.android.libraries.designsystem.preview.WithFontScale
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.theme.ElementTheme
+
+/**
+ * Return the maximum value between the receiver value and the value with fontScale applied.
+ * So if fontScale is >= 1f, the same value is returned, and if fontScale is < 1f, so returned value
+ * will be smaller.
+ */
+@Composable
+fun Dp.applyScaleDown(): Dp = with(LocalDensity.current) {
+ return this@applyScaleDown * fontScale.coerceAtMost(1f)
+}
+
+/**
+ * Return the minimum value between the receiver value and the value with fontScale applied.
+ * So if fontScale is <= 1f, the same value is returned, and if fontScale is > 1f, so returned value
+ * will be bigger.
+ */
+@Composable
+fun Dp.applyScaleUp(): Dp = with(LocalDensity.current) {
+ return this@applyScaleUp * fontScale.coerceAtLeast(1f)
+}
+
+@Preview
+@Composable
+internal fun DpScalePreview_0_75f() = WithFontScale(0.75f) {
+ ElementPreviewLight {
+ val fontSizeInDp = 16.dp
+ Column(
+ modifier = Modifier.padding(4.dp),
+ verticalArrangement = Arrangement.spacedBy(6.dp)
+ ) {
+ Text(
+ text = "Text with size of 16.sp",
+ style = ElementTheme.typography.fontBodyLgRegular.copy(fontSize = fontSizeInDp.toSp())
+ )
+ Text(
+ text = "Text with the same size (applyScaleUp)",
+ style = ElementTheme.typography.fontBodyLgRegular.copy(fontSize = fontSizeInDp.applyScaleUp().toSp())
+ )
+ Text(
+ text = "Text with a smaller size (applyScaleDown)",
+ style = ElementTheme.typography.fontBodyLgRegular.copy(fontSize = fontSizeInDp.applyScaleDown().toSp())
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+internal fun DpScalePreview_1_0f() = WithFontScale(1f) {
+ ElementPreviewLight {
+ val fontSizeInDp = 16.dp
+ Column(
+ modifier = Modifier.padding(4.dp),
+ verticalArrangement = Arrangement.spacedBy(6.dp)
+ ) {
+ Text(
+ text = "Text with size of 16.sp",
+ style = ElementTheme.typography.fontBodyLgRegular.copy(fontSize = fontSizeInDp.toSp())
+ )
+ Text(
+ text = "Text with the same size (applyScaleUp)",
+ style = ElementTheme.typography.fontBodyLgRegular.copy(fontSize = fontSizeInDp.applyScaleUp().toSp())
+ )
+ Text(
+ text = "Text with the same size (applyScaleDown)",
+ style = ElementTheme.typography.fontBodyLgRegular.copy(fontSize = fontSizeInDp.applyScaleDown().toSp())
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+internal fun DpScalePreview_1_5f() = WithFontScale(1.5f) {
+ ElementPreviewLight {
+ val fontSizeInDp = 16.dp
+ Column(
+ modifier = Modifier.padding(4.dp),
+ verticalArrangement = Arrangement.spacedBy(6.dp)
+ ) {
+ Text(
+ text = "Text with size of 16.sp",
+ style = ElementTheme.typography.fontBodyLgRegular.copy(fontSize = fontSizeInDp.toSp())
+ )
+ Text(
+ text = "Text with a bigger size (applyScaleUp)",
+ style = ElementTheme.typography.fontBodyLgRegular.copy(fontSize = fontSizeInDp.applyScaleUp().toSp())
+ )
+ Text(
+ text = "Text with the same size (applyScaleDown)",
+ style = ElementTheme.typography.fontBodyLgRegular.copy(fontSize = fontSizeInDp.applyScaleDown().toSp())
+ )
+ }
+ }
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/AlertDialogContent.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/AlertDialogContent.kt
similarity index 64%
rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/AlertDialogContent.kt
rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/AlertDialogContent.kt
index c1e8b0c055..a3c7274c45 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/AlertDialogContent.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/AlertDialogContent.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package io.element.android.libraries.designsystem.components.dialogs
+package io.element.android.libraries.designsystem.theme.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -22,26 +22,32 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
-import androidx.compose.material3.AlertDialogDefaults
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Surface
-import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Placeable
+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.Text
+import io.element.android.libraries.designsystem.preview.ElementThemedPreview
+import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.theme.ElementTheme
import kotlin.math.max
+// Figma designs: https://www.figma.com/file/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?type=design&node-id=911%3A343492&mode=design&t=jeyd1bXKOOx8y10r-1
+
@Composable
internal fun SimpleAlertDialogContent(
content: String,
@@ -53,13 +59,6 @@ internal fun SimpleAlertDialogContent(
onSubmitClicked: () -> Unit = {},
thirdButtonText: String? = null,
onThirdButtonClicked: () -> Unit = {},
- emphasizeSubmitButton: Boolean = false,
- shape: Shape = AlertDialogDefaults.shape,
- containerColor: Color = AlertDialogDefaults.containerColor,
- iconContentColor: Color = AlertDialogDefaults.iconContentColor,
- titleContentColor: Color = AlertDialogDefaults.titleContentColor,
- textContentColor: Color = AlertDialogDefaults.textContentColor,
- tonalElevation: Dp = AlertDialogDefaults.TonalElevation,
icon: @Composable (() -> Unit)? = null,
) {
AlertDialogContent(
@@ -71,54 +70,47 @@ internal fun SimpleAlertDialogContent(
if (thirdButtonText != null) {
// If there is a 3rd item it should be at the end of the dialog
// Having this 3rd action is discouraged, see https://m3.material.io/components/dialogs/guidelines#e13b68f5-e367-4275-ad6f-c552ee8e358f
- TextButton(onClick = onThirdButtonClicked) {
- Text(
- text = thirdButtonText,
- style = ElementTheme.typography.fontBodyMdRegular,
- )
- }
- }
- TextButton(onClick = onCancelClicked) {
- Text(
- text = cancelText,
- style = ElementTheme.typography.fontBodyMdRegular,
+ TextButton(
+ text = thirdButtonText,
+ size = ButtonSize.Medium,
+ onClick = onThirdButtonClicked,
)
}
+ TextButton(
+ text = cancelText,
+ size = ButtonSize.Medium,
+ onClick = onCancelClicked,
+ )
if (submitText != null) {
- TextButton(onClick = onSubmitClicked) {
- Text(
- text = submitText,
- style = if (emphasizeSubmitButton) {
- ElementTheme.typography.fontBodyMdMedium
- } else {
- ElementTheme.typography.fontBodyMdRegular
- }
- )
- }
+ Button(
+ text = submitText,
+ size = ButtonSize.Medium,
+ onClick = onSubmitClicked,
+ )
}
}
},
modifier = modifier,
- title = {
- if (title != null) {
+ title = title?.let { titleText ->
+ @Composable {
Text(
- text = title,
- style = ElementTheme.typography.fontHeadingSmRegular,
+ text = titleText,
+ style = ElementTheme.typography.fontHeadingSmMedium,
)
}
},
text = {
Text(
text = content,
- style = ElementTheme.typography.fontBodyMdRegular,
+ style = ElementTheme.materialTypography.bodyMedium,
)
},
- shape = shape,
- containerColor = containerColor,
- iconContentColor = iconContentColor,
- titleContentColor = titleContentColor,
- textContentColor = textContentColor,
- tonalElevation = tonalElevation,
+ shape = DialogContentDefaults.shape,
+ containerColor = DialogContentDefaults.containerColor,
+ iconContentColor = DialogContentDefaults.iconContentColor,
+ titleContentColor = DialogContentDefaults.titleContentColor,
+ textContentColor = DialogContentDefaults.textContentColor,
+ tonalElevation = 0.dp,
icon = icon,
// Note that a button content color is provided here from the dialog's token, but in
// most cases, TextButtons should be used for dismiss and confirm buttons.
@@ -128,6 +120,9 @@ internal fun SimpleAlertDialogContent(
)
}
+/**
+ * Copy of M3's `AlertDialogContent` so we can use it for previews.
+ */
@Composable
internal fun AlertDialogContent(
buttons: @Composable () -> Unit,
@@ -150,13 +145,13 @@ internal fun AlertDialogContent(
tonalElevation = tonalElevation,
) {
Column(
- modifier = Modifier.padding(DialogPadding)
+ modifier = Modifier.padding(DialogContentDefaults.externalPadding)
) {
icon?.let {
CompositionLocalProvider(LocalContentColor provides iconContentColor) {
Box(
Modifier
- .padding(IconPadding)
+ .padding(DialogContentDefaults.iconPadding)
.align(Alignment.CenterHorizontally)
) {
icon()
@@ -170,7 +165,7 @@ internal fun AlertDialogContent(
Box(
// Align the title to the center when an icon is present.
Modifier
- .padding(TitlePadding)
+ .padding(DialogContentDefaults.titlePadding)
.align(
if (icon == null) {
Alignment.Start
@@ -192,7 +187,7 @@ internal fun AlertDialogContent(
Box(
Modifier
.weight(weight = 1f, fill = false)
- .padding(TextPadding)
+ .padding(DialogContentDefaults.textPadding)
.align(Alignment.Start)
) {
text()
@@ -216,7 +211,7 @@ internal fun AlertDialogContent(
* customization.
*/
@Composable
-internal fun AlertDialogFlowRow(
+private fun AlertDialogFlowRow(
mainAxisSpacing: Dp,
crossAxisSpacing: Dp,
content: @Composable () -> Unit
@@ -243,7 +238,8 @@ internal fun AlertDialogFlowRow(
if (sequences.isNotEmpty()) {
crossAxisSpace += crossAxisSpacing.roundToPx()
}
- sequences += currentSequence.toList()
+ // Ensures that confirming actions appear above dismissive actions.
+ sequences.add(0, currentSequence.toList())
crossAxisSizes += currentCrossAxisSize
crossAxisPositions += crossAxisSpace
@@ -287,12 +283,11 @@ internal fun AlertDialogFlowRow(
placeables[j].width +
if (j < placeables.lastIndex) mainAxisSpacing.roundToPx() else 0
}
- val arrangement = Arrangement.Bottom
- // TODO(soboleva): rtl support
- // Handle vertical direction
+ val arrangement = Arrangement.End
val mainAxisPositions = IntArray(childrenMainAxisSizes.size) { 0 }
with(arrangement) {
- arrange(mainAxisLayoutSize, childrenMainAxisSizes, mainAxisPositions)
+ arrange(mainAxisLayoutSize, childrenMainAxisSizes,
+ layoutDirection, mainAxisPositions)
}
placeables.forEachIndexed { j, placeable ->
placeable.place(
@@ -317,14 +312,87 @@ internal fun DialogPreview(content: @Composable () -> Unit) {
}
}
-// Paddings for each of the dialog's parts.
-private val DialogPadding = PaddingValues(all = 24.dp)
-private val IconPadding = PaddingValues(bottom = 16.dp)
-private val TitlePadding = PaddingValues(bottom = 16.dp)
-private val TextPadding = PaddingValues(bottom = 24.dp)
+internal object DialogContentDefaults {
+ val shape = RoundedCornerShape(12.dp)
+ val externalPadding = PaddingValues(all = 24.dp)
+ val titlePadding = PaddingValues(bottom = 16.dp)
+ val iconPadding = PaddingValues(bottom = 8.dp)
+ val textPadding = PaddingValues(bottom = 16.dp)
+ val containerColor: Color
+ @Composable
+ @ReadOnlyComposable
+ get()= ElementTheme.colors.bgCanvasDefault
+
+ val textContentColor: Color
+ @Composable
+ @ReadOnlyComposable
+ get()= ElementTheme.materialColors.onSurfaceVariant
+
+ val titleContentColor: Color
+ @Composable
+ @ReadOnlyComposable
+ get()= ElementTheme.materialColors.onSurface
+
+ val iconContentColor: Color
+ @Composable
+ @ReadOnlyComposable
+ get()= ElementTheme.materialColors.primary
+}
+
+// Paddings for each of the dialog's parts. Taken from M3 source code.
internal val ButtonsMainAxisSpacing = 8.dp
internal val ButtonsCrossAxisSpacing = 12.dp
internal val DialogMinWidth = 280.dp
internal val DialogMaxWidth = 560.dp
+
+@Preview(group = PreviewGroup.Dialogs, name = "Dialog with title, icon and ok button")
+@Composable
+@Suppress("MaxLineLength")
+internal fun DialogWithTitleIconAndOkButtonPreview() {
+ ElementThemedPreview(showBackground = false) {
+ DialogPreview {
+ SimpleAlertDialogContent(
+ icon = {
+ Icon(imageVector = Icons.Default.Notifications, contentDescription = null)
+ },
+ title = "Dialog Title",
+ content = "A dialog is a type of modal window that appears in front of app content to provide critical information, or prompt for a decision to be made. Learn more",
+ cancelText = "OK",
+ onCancelClicked = {},
+ )
+ }
+ }
+}
+
+@Preview(group = PreviewGroup.Dialogs, name = "Dialog with title and ok button")
+@Composable
+@Suppress("MaxLineLength")
+internal fun DialogWithTitleAndOkButtonPreview() {
+ ElementThemedPreview(showBackground = false) {
+ DialogPreview {
+ SimpleAlertDialogContent(
+ title = "Dialog Title",
+ content = "A dialog is a type of modal window that appears in front of app content to provide critical information, or prompt for a decision to be made. Learn more",
+ cancelText = "OK",
+ onCancelClicked = {},
+ )
+ }
+ }
+}
+
+@Preview(group = PreviewGroup.Dialogs, name = "Dialog with only message and ok button")
+@Composable
+@Suppress("MaxLineLength")
+internal fun DialogWithOnlyMessageAndOkButtonPreview() {
+ ElementThemedPreview(showBackground = false) {
+ DialogPreview {
+ SimpleAlertDialogContent(
+ content = "A dialog is a type of modal window that appears in front of app content to provide critical information, or prompt for a decision to be made. Learn more",
+ cancelText = "OK",
+ onCancelClicked = {},
+ )
+ }
+ }
+}
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 64c79a3906..8c5d96c400 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
@@ -18,68 +18,367 @@ package io.element.android.libraries.designsystem.theme.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.progressSemantics
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Share
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults
-import androidx.compose.material3.ButtonElevation
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.isSpecified
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.rememberVectorPainter
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
+import io.element.android.libraries.theme.ElementTheme
+
+// Designs: https://www.figma.com/file/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?type=design&mode=design&t=U03tOFZz5FSLVUMa-1
@Composable
fun Button(
+ text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
- shape: Shape = ElementButtonDefaults.shape,
- colors: ButtonColors = ElementButtonDefaults.buttonColors(),
- elevation: ButtonElevation? = ElementButtonDefaults.buttonElevation(),
- border: BorderStroke? = null,
- contentPadding: PaddingValues = ElementButtonDefaults.ContentPadding,
- interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- content: @Composable RowScope.() -> Unit
+ size: ButtonSize = ButtonSize.Large,
+ showProgress: Boolean = false,
+ leadingIcon: IconSource? = null,
+) = ButtonInternal(
+ text = text,
+ onClick = onClick,
+ style = ButtonStyle.Filled,
+ modifier = modifier,
+ enabled = enabled,
+ size = size,
+ showProgress = showProgress,
+ leadingIcon = leadingIcon
+)
+
+@Composable
+fun OutlinedButton(
+ text: String,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ size: ButtonSize = ButtonSize.Large,
+ showProgress: Boolean = false,
+ leadingIcon: IconSource? = null,
+) = ButtonInternal(
+ text = text,
+ onClick = onClick,
+ style = ButtonStyle.Outlined,
+ modifier = modifier,
+ enabled = enabled,
+ size = size,
+ showProgress = showProgress,
+ leadingIcon = leadingIcon
+)
+
+@Composable
+fun TextButton(
+ text: String,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ size: ButtonSize = ButtonSize.Large,
+ showProgress: Boolean = false,
+ leadingIcon: IconSource? = null,
+) = ButtonInternal(
+ text = text,
+ onClick = onClick,
+ style = ButtonStyle.Text,
+ modifier = modifier,
+ enabled = enabled,
+ size = size,
+ showProgress = showProgress,
+ leadingIcon = leadingIcon
+)
+
+@Composable
+internal fun ButtonInternal(
+ text: String,
+ onClick: () -> Unit,
+ style: ButtonStyle,
+ modifier: Modifier = Modifier,
+ colors: ButtonColors = style.getColors(),
+ enabled: Boolean = true,
+ size: ButtonSize = ButtonSize.Large,
+ showProgress: Boolean = false,
+ leadingIcon: IconSource? = null,
) {
+ val minHeight = when (size) {
+ ButtonSize.Medium -> 40.dp
+ ButtonSize.Large -> 48.dp
+ }
+
+ val contentPadding = when (size) {
+ ButtonSize.Medium -> {
+ when (style) {
+ ButtonStyle.Text -> PaddingValues(horizontal = 12.dp, vertical = 10.dp)
+ else -> PaddingValues(horizontal = 16.dp, vertical = 10.dp)
+ }
+ }
+ ButtonSize.Large -> {
+ when (style) {
+ ButtonStyle.Text -> PaddingValues(horizontal = 16.dp, vertical = 13.dp)
+ else -> PaddingValues(horizontal = 24.dp, vertical = 13.dp)
+ }
+ }
+ }
+
+ val shape = when (style) {
+ ButtonStyle.Filled, ButtonStyle.Outlined -> RoundedCornerShape(percent = 50)
+ ButtonStyle.Text -> RectangleShape
+ }
+
+ val border = when (style) {
+ ButtonStyle.Filled, ButtonStyle.Text -> null
+ ButtonStyle.Outlined -> BorderStroke(
+ width = 1.dp,
+ color = ElementTheme.colors.borderInteractiveSecondary
+ )
+ }
+
+ val textStyle = when (size) {
+ ButtonSize.Medium -> MaterialTheme.typography.labelLarge
+ ButtonSize.Large -> ElementTheme.typography.fontBodyLgMedium
+ }
+
+ val internalPadding = when {
+ style == ButtonStyle.Text -> if (leadingIcon != null) PaddingValues(start = 8.dp) else PaddingValues(0.dp)
+ else -> PaddingValues(horizontal = 8.dp)
+ }
+
androidx.compose.material3.Button(
- onClick = onClick,
- modifier = modifier,
+ onClick = {
+ if (!showProgress) {
+ onClick()
+ }
+ },
+ modifier = modifier.heightIn(min = minHeight),
enabled = enabled,
shape = shape,
colors = colors,
- elevation = elevation,
+ elevation = null,
border = border,
contentPadding = contentPadding,
- interactionSource = interactionSource,
- content = content,
- )
+ interactionSource = remember { MutableInteractionSource() },
+ ) {
+ when {
+ showProgress -> {
+ CircularProgressIndicator(
+ modifier = Modifier
+ .progressSemantics()
+ .size(20.dp),
+ color = LocalContentColor.current,
+ strokeWidth = 2.dp,
+ )
+ }
+ leadingIcon != null -> {
+ androidx.compose.material.Icon(
+ painter = leadingIcon.getPainter(),
+ contentDescription = null,
+ tint = LocalContentColor.current,
+ modifier = Modifier.size(20.dp),
+ )
+ }
+ else -> Unit
+ }
+ Text(
+ text = text,
+ style = textStyle,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ modifier = Modifier.padding(internalPadding),
+ )
+ }
}
-object ElementButtonDefaults {
- val ContentPadding = PaddingValues(horizontal = 24.dp, vertical = 14.dp)
- val shape: Shape @Composable get() = ButtonDefaults.shape
- @Composable
- fun buttonElevation(): ButtonElevation = ButtonDefaults.buttonElevation()
+sealed interface IconSource {
+ val contentDescription: String?
+
+ data class Resource(val id: Int, override val contentDescription: String? = null) : IconSource
+ data class Vector(val vector: ImageVector, override val contentDescription: String? = null) : IconSource
@Composable
- fun buttonColors(): ButtonColors = ButtonDefaults.buttonColors()
+ fun getPainter(): Painter = when (this) {
+ is Resource -> painterResource(id)
+ is Vector -> rememberVectorPainter(image = vector)
+ }
+}
+enum class ButtonSize {
+ Medium, Large
+}
+
+internal enum class ButtonStyle {
+ Filled, Outlined, Text;
+
+ @Composable
+ fun getColors(): ButtonColors = when (this) {
+ Filled -> ButtonDefaults.buttonColors(
+ containerColor = ElementTheme.materialColors.primary,
+ contentColor = ElementTheme.materialColors.onPrimary,
+ disabledContainerColor = ElementTheme.colors.bgActionPrimaryDisabled,
+ disabledContentColor = ElementTheme.colors.textOnSolidPrimary
+ )
+ Outlined -> ButtonDefaults.buttonColors(
+ containerColor = Color.Transparent,
+ contentColor = ElementTheme.materialColors.primary,
+ disabledContainerColor = Color.Transparent,
+ disabledContentColor = ElementTheme.colors.textDisabled,
+ )
+ Text -> ButtonDefaults.buttonColors(
+ containerColor = Color.Transparent,
+ contentColor = if (LocalContentColor.current.isSpecified) LocalContentColor.current else ElementTheme.materialColors.primary,
+ disabledContainerColor = Color.Transparent,
+ disabledContentColor = ElementTheme.colors.textDisabled,
+ )
+ }
}
@Preview(group = PreviewGroup.Buttons)
@Composable
-internal fun ButtonPreview() = ElementThemedPreview {
- Column {
- Button(onClick = {}, enabled = true) {
- Text(text = "Click me! - Enabled")
- }
- Button(onClick = {}, enabled = false) {
- Text(text = "Click me! - Disabled")
+internal fun FilledButtonMediumPreview() {
+ ButtonCombinationPreview(
+ style = ButtonStyle.Filled,
+ size = ButtonSize.Medium,
+ )
+}
+
+@Preview(group = PreviewGroup.Buttons)
+@Composable
+internal fun FilledButtonLargePreview() {
+ ButtonCombinationPreview(
+ style = ButtonStyle.Filled,
+ size = ButtonSize.Large,
+ )
+}
+
+@Preview(group = PreviewGroup.Buttons)
+@Composable
+internal fun OutlinedButtonMediumPreview() {
+ ButtonCombinationPreview(
+ style = ButtonStyle.Outlined,
+ size = ButtonSize.Medium,
+ )
+}
+
+@Preview(group = PreviewGroup.Buttons)
+@Composable
+internal fun OutlinedButtonLargePreview() {
+ ButtonCombinationPreview(
+ style = ButtonStyle.Outlined,
+ size = ButtonSize.Large,
+ )
+}
+
+@Preview(group = PreviewGroup.Buttons)
+@Composable
+internal fun TextButtonMediumPreview() {
+ ButtonCombinationPreview(
+ style = ButtonStyle.Text,
+ size = ButtonSize.Medium,
+ )
+}
+
+@Preview(group = PreviewGroup.Buttons)
+@Composable
+internal fun TextButtonLargePreview() {
+ ButtonCombinationPreview(
+ style = ButtonStyle.Text,
+ size = ButtonSize.Large,
+ )
+}
+
+@Composable
+private fun ButtonCombinationPreview(
+ style: ButtonStyle,
+ size: ButtonSize,
+ modifier: Modifier = Modifier,
+) {
+ ElementThemedPreview {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ modifier = Modifier
+ .padding(16.dp)
+ .width(IntrinsicSize.Max),
+ ) {
+ // Normal
+ ButtonRowPreview(
+ modifier = Modifier.then(modifier),
+ style = style,
+ size = size,
+ )
+
+ // With icon
+ ButtonRowPreview(
+ modifier = Modifier.then(modifier),
+ leadingIcon = IconSource.Vector(Icons.Outlined.Share),
+ style = style,
+ size = size,
+ )
+
+ // With progress
+ ButtonRowPreview(
+ modifier = Modifier.then(modifier),
+ showProgress = true,
+ style = style,
+ size = size,
+ )
}
}
}
+
+@Composable
+private fun ButtonRowPreview(
+ style: ButtonStyle,
+ size: ButtonSize,
+ modifier: Modifier = Modifier,
+ leadingIcon: IconSource? = null,
+ showProgress: Boolean = false,
+) {
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally)) {
+ ButtonInternal(
+ text = "A button",
+ showProgress = showProgress,
+ onClick = {},
+ style = style,
+ size = size,
+ leadingIcon = leadingIcon,
+ modifier = Modifier.then(modifier),
+ )
+ ButtonInternal(
+ text = "A button",
+ showProgress = showProgress,
+ enabled = false,
+ onClick = {},
+ style = style,
+ size = size,
+ leadingIcon = leadingIcon,
+ modifier = Modifier.then(modifier),
+ )
+ }
+}
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 b754b3d420..31c6cc3647 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
@@ -17,15 +17,25 @@
package io.element.android.libraries.designsystem.theme.components
import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
import androidx.compose.material3.CheckboxColors
import androidx.compose.material3.CheckboxDefaults
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
+import io.element.android.libraries.theme.ElementTheme
+
+// Designs in https://www.figma.com/file/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?type=design&mode=design&t=qb99xBP5mwwCtGkN-1
@Composable
fun Checkbox(
@@ -33,12 +43,20 @@ fun Checkbox(
onCheckedChange: ((Boolean) -> Unit)?,
modifier: Modifier = Modifier,
enabled: Boolean = true,
- colors: CheckboxColors = CheckboxDefaults.colors(),
+ hasError: Boolean = false,
+ indeterminate: Boolean = false,
+ colors: CheckboxColors = if (hasError) compoundErrorCheckBoxColors() else compoundCheckBoxColors(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
) {
- androidx.compose.material3.Checkbox(
- checked = checked,
- onCheckedChange = onCheckedChange,
+ var indeterminateState by remember { mutableStateOf(indeterminate) }
+ androidx.compose.material3.TriStateCheckbox(
+ state = if (!checked && indeterminateState) ToggleableState.Indeterminate else ToggleableState(checked),
+ onClick = onCheckedChange?.let {
+ {
+ indeterminateState = false
+ onCheckedChange(!checked)
+ }
+ },
modifier = modifier,
enabled = enabled,
colors = colors,
@@ -46,6 +64,30 @@ fun Checkbox(
)
}
+@Composable
+private fun compoundCheckBoxColors(): CheckboxColors {
+ return CheckboxDefaults.colors(
+ checkedColor = ElementTheme.materialColors.primary,
+ uncheckedColor = ElementTheme.colors.borderInteractivePrimary,
+ checkmarkColor = ElementTheme.materialColors.onPrimary,
+ disabledUncheckedColor = ElementTheme.colors.borderDisabled,
+ disabledCheckedColor = ElementTheme.colors.iconDisabled,
+ disabledIndeterminateColor = ElementTheme.colors.iconDisabled,
+ )
+}
+
+@Composable
+private fun compoundErrorCheckBoxColors(): CheckboxColors {
+ return CheckboxDefaults.colors(
+ checkedColor = ElementTheme.materialColors.error,
+ uncheckedColor = ElementTheme.materialColors.error,
+ checkmarkColor = ElementTheme.materialColors.onPrimary,
+ disabledUncheckedColor = ElementTheme.colors.borderDisabled,
+ disabledCheckedColor = ElementTheme.colors.iconDisabled,
+ disabledIndeterminateColor = ElementTheme.colors.iconDisabled,
+ )
+}
+
@Preview(group = PreviewGroup.Toggles)
@Composable
internal fun CheckboxesPreview() = ElementThemedPreview(vertical = false) { ContentToPreview() }
@@ -53,9 +95,33 @@ internal fun CheckboxesPreview() = ElementThemedPreview(vertical = false) { Cont
@Composable
private fun ContentToPreview() {
Column {
- Checkbox(onCheckedChange = {}, enabled = true, checked = true)
- Checkbox(onCheckedChange = {}, enabled = true, checked = false)
- Checkbox(onCheckedChange = {}, enabled = false, checked = true)
- Checkbox(onCheckedChange = {}, enabled = false, checked = false)
+ // Unchecked
+ Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
+ Checkbox(onCheckedChange = {}, enabled = true, checked = false)
+ Checkbox(onCheckedChange = {}, enabled = false, checked = false)
+ }
+ // Checked
+ Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
+ Checkbox(onCheckedChange = {}, enabled = true, checked = true)
+ Checkbox(onCheckedChange = {}, enabled = false, checked = true)
+ }
+ // Indeterminate
+ Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
+ Checkbox(onCheckedChange = {}, enabled = true, checked = false, indeterminate = true)
+ Checkbox(onCheckedChange = {}, enabled = false, checked = false, indeterminate = true)
+ }
+ // Error
+ Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
+ Checkbox(hasError = true, onCheckedChange = {}, checked = false)
+ Checkbox(hasError = true, onCheckedChange = {}, enabled = false, checked = false)
+ }
+ Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
+ Checkbox(hasError = true, onCheckedChange = {}, enabled = true, checked = true)
+ Checkbox(hasError = true, onCheckedChange = {}, enabled = false, checked = true)
+ }
+ Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
+ Checkbox(onCheckedChange = {}, enabled = true, checked = false, indeterminate = true, hasError = true)
+ Checkbox(onCheckedChange = {}, enabled = false, checked = false, indeterminate = true, hasError = true)
+ }
}
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/DropdownMenu.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/DropdownMenu.kt
index 175c0fb402..c0160bb7f8 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/DropdownMenu.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/DropdownMenu.kt
@@ -26,7 +26,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.PopupProperties
import io.element.android.libraries.theme.ElementTheme
-private val minMenuWidth = 200.dp
+// Figma designs: https://www.figma.com/file/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?type=design&node-id=1032%3A44063&mode=design&t=rsNegTbEVLYAXL76-1
@Composable
fun DropdownMenu(
@@ -38,19 +38,17 @@ fun DropdownMenu(
properties: PopupProperties = PopupProperties(focusable = true),
content: @Composable ColumnScope.() -> Unit
) {
- val bgColor = if (ElementTheme.isLightTheme) {
- ElementTheme.materialColors.background
- } else {
- ElementTheme.colors.bgSubtlePrimary
- }
+ // Note: the internal shape corner radius should be 8dp, but there is a 4p value hardcoded in the internal Surface component
androidx.compose.material3.DropdownMenu(
expanded = expanded,
onDismissRequest = onDismissRequest,
modifier = modifier
- .background(color = bgColor)
+ .background(color = ElementTheme.colors.bgCanvasDefault)
.widthIn(min = minMenuWidth),
offset = offset,
properties = properties,
content = content
)
}
+
+private val minMenuWidth = 200.dp
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/DropdownMenuItem.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/DropdownMenuItem.kt
index b8c18f99c6..1e1ccf8d30 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/DropdownMenuItem.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/DropdownMenuItem.kt
@@ -17,20 +17,26 @@
package io.element.android.libraries.designsystem.theme.components
import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowRight
import androidx.compose.material.icons.filled.BugReport
-import androidx.compose.material.icons.filled.Share
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuDefaults
-import androidx.compose.material3.MenuItemColors
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
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.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.theme.ElementTheme
+// Figma designs: https://www.figma.com/file/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?type=design&node-id=1032%3A44063&mode=design&t=rsNegTbEVLYAXL76-1
+
@Composable
fun DropdownMenuItem(
text: @Composable () -> Unit,
@@ -39,34 +45,37 @@ fun DropdownMenuItem(
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
enabled: Boolean = true,
- colors: MenuItemColors = MenuDefaults.itemColors(),
- contentPadding: PaddingValues = MenuDefaults.DropdownMenuItemContentPadding,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
androidx.compose.material3.DropdownMenuItem(
- text = text,
+ text = {
+ CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodyLarge) {
+ text()
+ }
+ },
onClick = onClick,
modifier = modifier,
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
enabled = enabled,
- colors = colors,
- contentPadding = contentPadding,
+ colors = DropDownMenuItemDefaults.colors(),
+ contentPadding = DropDownMenuItemDefaults.contentPadding,
interactionSource = interactionSource
)
}
-@Composable
-fun DropdownMenuItemText(
- text: String,
- modifier: Modifier = Modifier,
-) {
- Text(
- text = text,
- color = ElementTheme.materialColors.primary,
- style = ElementTheme.typography.fontBodyLgRegular,
- modifier = modifier,
+internal object DropDownMenuItemDefaults {
+ @Composable
+ fun colors() = MenuDefaults.itemColors(
+ textColor = ElementTheme.colors.textPrimary,
+ leadingIconColor = ElementTheme.colors.iconPrimary,
+ trailingIconColor = ElementTheme.colors.iconSecondary,
+ disabledTextColor = ElementTheme.colors.textDisabled,
+ disabledLeadingIconColor = ElementTheme.colors.iconDisabled,
+ disabledTrailingIconColor = ElementTheme.colors.iconDisabled,
)
+
+ val contentPadding = PaddingValues(all = 12.dp)
}
@Preview(group = PreviewGroup.Menus)
@@ -75,10 +84,36 @@ internal fun DropdownMenuItemPreview() = ElementThemedPreview { ContentToPreview
@Composable
private fun ContentToPreview() {
- DropdownMenuItem(
- text = { DropdownMenuItemText(text = "Item") },
- onClick = {},
- leadingIcon = { Icon(Icons.Default.BugReport, contentDescription = null) },
- trailingIcon = { Icon(Icons.Default.Share, contentDescription = null) },
- )
+ Column {
+ DropdownMenuItem(
+ text = { Text(text = "Item") },
+ onClick = {},
+ trailingIcon = { Icon(Icons.Default.ArrowRight, contentDescription = null) },
+ )
+ HorizontalDivider()
+ DropdownMenuItem(
+ text = { Text(text = "Item") },
+ onClick = {},
+ leadingIcon = { Icon(Icons.Default.BugReport, contentDescription = null) },
+ )
+ DropdownMenuItem(
+ text = { Text(text = "Item") },
+ onClick = {},
+ leadingIcon = { Icon(Icons.Default.BugReport, contentDescription = null) },
+ trailingIcon = { Icon(Icons.Default.ArrowRight, contentDescription = null) },
+ )
+ DropdownMenuItem(
+ text = { Text(text = "Item") },
+ onClick = {},
+ enabled = false,
+ leadingIcon = { Icon(Icons.Default.BugReport, contentDescription = null) },
+ trailingIcon = { Icon(Icons.Default.ArrowRight, contentDescription = null) },
+ )
+ HorizontalDivider()
+ DropdownMenuItem(
+ text = { Text(text = "Multiline\nItem") },
+ onClick = {},
+ trailingIcon = { Icon(Icons.Default.ArrowRight, contentDescription = null) },
+ )
+ }
}
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/HorizontalDivider.kt
similarity index 91%
rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Divider.kt
rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/HorizontalDivider.kt
index a8d64d271d..1a3f91a431 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/HorizontalDivider.kt
@@ -30,12 +30,12 @@ import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
@Composable
-fun Divider(
+fun HorizontalDivider(
modifier: Modifier = Modifier,
thickness: Dp = ElementDividerDefaults.thickness,
color: Color = DividerDefaults.color,
) {
- androidx.compose.material3.Divider(
+ androidx.compose.material3.HorizontalDivider(
modifier = modifier,
thickness = thickness,
color = color,
@@ -48,7 +48,7 @@ object ElementDividerDefaults {
@Preview(group = PreviewGroup.Dividers)
@Composable
-internal fun DividerPreview() = ElementThemedPreview {
+internal fun HorizontalDividerPreview() = ElementThemedPreview {
Box(Modifier.padding(vertical = 10.dp), contentAlignment = Alignment.Center) {
ContentToPreview()
}
@@ -56,5 +56,5 @@ internal fun DividerPreview() = ElementThemedPreview {
@Composable
private fun ContentToPreview() {
- Divider()
+ HorizontalDivider()
}
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
index 14cc6b62ce..7063ab1eb1 100644
--- 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
@@ -17,16 +17,22 @@
package io.element.android.libraries.designsystem.theme.components
import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.IconButtonDefaults
+import androidx.compose.material3.LocalContentColor
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
+import io.element.android.libraries.theme.ElementTheme
+
+// Figma designs: https://www.figma.com/file/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?type=design&node-id=1182%3A48861&mode=design&t=Shlcvznm1oUyqGC2-1
@Composable
fun IconButton(
@@ -36,11 +42,15 @@ fun IconButton(
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
content: @Composable () -> Unit
) {
+ val colors = IconButtonDefaults.iconButtonColors(
+ contentColor = LocalContentColor.current,
+ disabledContentColor = ElementTheme.colors.iconDisabled,
+ )
androidx.compose.material3.IconButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
- colors = IconButtonDefaults.iconButtonColors(),
+ colors = colors,
interactionSource = interactionSource,
content = content,
)
@@ -53,12 +63,26 @@ internal fun IconButtonPreview() =
@Composable
private fun ContentToPreview() {
- Row {
- IconButton(onClick = {}) {
- Icon(imageVector = Icons.Filled.Close, contentDescription = "")
+ Column {
+ CompositionLocalProvider(LocalContentColor provides ElementTheme.colors.iconPrimary) {
+ Row {
+ IconButton(onClick = {}) {
+ Icon(imageVector = Icons.Filled.Close, contentDescription = "")
+ }
+ IconButton(enabled = false, onClick = {}) {
+ Icon(imageVector = Icons.Filled.Close, contentDescription = "")
+ }
+ }
}
- IconButton(enabled = false, onClick = {}) {
- Icon(imageVector = Icons.Filled.Close, contentDescription = "")
+ CompositionLocalProvider(LocalContentColor provides ElementTheme.colors.iconSecondary) {
+ Row {
+ IconButton(onClick = {}) {
+ Icon(imageVector = Icons.Filled.Close, contentDescription = "")
+ }
+ IconButton(enabled = false, onClick = {}) {
+ Icon(imageVector = Icons.Filled.Close, contentDescription = "")
+ }
+ }
}
}
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/LinearProgressIndicator.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/LinearProgressIndicator.kt
new file mode 100644
index 0000000000..54985eaa51
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/LinearProgressIndicator.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.Arrangement
+import androidx.compose.foundation.layout.Column
+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.graphics.StrokeCap
+import androidx.compose.ui.platform.LocalInspectionMode
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import io.element.android.libraries.designsystem.preview.ElementThemedPreview
+import io.element.android.libraries.designsystem.preview.PreviewGroup
+
+@Composable
+fun LinearProgressIndicator(
+ progress: Float,
+ modifier: Modifier = Modifier,
+ color: Color = ProgressIndicatorDefaults.linearColor,
+ trackColor: Color = ProgressIndicatorDefaults.linearTrackColor,
+ strokeCap: StrokeCap = ProgressIndicatorDefaults.LinearStrokeCap,
+) {
+ androidx.compose.material3.LinearProgressIndicator(
+ modifier = modifier,
+ progress = progress,
+ color = color,
+ trackColor = trackColor,
+ strokeCap = strokeCap,
+ )
+}
+
+@Composable
+fun LinearProgressIndicator(
+ modifier: Modifier = Modifier,
+ color: Color = ProgressIndicatorDefaults.linearColor,
+ trackColor: Color = ProgressIndicatorDefaults.linearTrackColor,
+ strokeCap: StrokeCap = ProgressIndicatorDefaults.LinearStrokeCap,
+) {
+ if (LocalInspectionMode.current) {
+ // Use a determinate progress indicator to improve the preview rendering
+ androidx.compose.material3.LinearProgressIndicator(
+ modifier = modifier,
+ progress = 0.75F,
+ color = color,
+ trackColor = trackColor,
+ strokeCap = strokeCap,
+ )
+ } else {
+ androidx.compose.material3.LinearProgressIndicator(
+ modifier = modifier,
+ color = color,
+ trackColor = trackColor,
+ strokeCap = strokeCap,
+ )
+ }
+}
+
+@Preview(group = PreviewGroup.Progress)
+@Composable
+internal fun LinearProgressIndicatorPreview() = ElementThemedPreview(vertical = false) { ContentToPreview() }
+
+@Composable
+private fun ContentToPreview() {
+ Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
+ // Indeterminate progress
+ LinearProgressIndicator(
+ )
+ // Fixed progress
+ LinearProgressIndicator(
+ progress = 0.90F
+ )
+ }
+}
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 d3cd2fee07..d868f49965 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
@@ -18,15 +18,21 @@ package io.element.android.libraries.designsystem.theme.components
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.TopAppBarColors
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
+import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
+import io.element.android.libraries.theme.ElementTheme
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -43,7 +49,11 @@ fun MediumTopAppBar(
title = title,
modifier = modifier,
navigationIcon = navigationIcon,
- actions = actions,
+ actions = {
+ CompositionLocalProvider(LocalContentColor provides ElementTheme.colors.textActionPrimary) {
+ actions()
+ }
+ },
windowInsets = windowInsets,
colors = colors,
scrollBehavior = scrollBehavior,
@@ -58,5 +68,14 @@ internal fun MediumTopAppBarPreview() =
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ContentToPreview() {
- MediumTopAppBar(title = { Text(text = "Title") })
+ MediumTopAppBar(
+ title = { Text(text = "Title") },
+ navigationIcon = { BackButton(onClick = {}) },
+ actions = {
+ TextButton(text = "Action", onClick = {})
+ IconButton(onClick = {}) {
+ Icon(imageVector = Icons.Default.Share, contentDescription = null)
+ }
+ }
+ )
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt
index 57b0612ad7..03e5c06eef 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt
@@ -100,7 +100,7 @@ private fun ContentToPreview() {
) {
ModalBottomSheet(
onDismissRequest = {},
- sheetState = sheetStateForPreview,
+ sheetState = sheetStateForPreview(),
) {
Text(
text = "Sheet Content",
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 2f40a90615..30e5882f26 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
@@ -14,6 +14,9 @@
* limitations under the License.
*/
+// This is actually expected, as we should remove this component soon and use ModalBottomSheet instead
+@file:Suppress("UsingMaterialAndMaterial3Libraries")
+
package io.element.android.libraries.designsystem.theme.components
import androidx.compose.foundation.background
@@ -40,6 +43,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -111,7 +115,7 @@ private fun ContentToPreview() {
ModalBottomSheetLayout(
modifier = Modifier.height(140.dp),
displayHandle = true,
- sheetState = ModalBottomSheetState(ModalBottomSheetValue.Expanded),
+ sheetState = ModalBottomSheetState(ModalBottomSheetValue.Expanded, density = LocalDensity.current),
sheetContent = {
Text(text = "Sheet Content", modifier = Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 20.dp)
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/OutlinedButton.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/OutlinedButton.kt
deleted file mode 100644
index fa7ee261f6..0000000000
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/OutlinedButton.kt
+++ /dev/null
@@ -1,91 +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.components
-
-import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.RowScope
-import androidx.compose.material3.ButtonColors
-import androidx.compose.material3.ButtonDefaults
-import androidx.compose.material3.ButtonElevation
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import io.element.android.libraries.designsystem.preview.ElementThemedPreview
-import io.element.android.libraries.designsystem.preview.PreviewGroup
-
-@Composable
-fun OutlinedButton(
- onClick: () -> Unit,
- modifier: Modifier = Modifier,
- enabled: Boolean = true,
- shape: Shape = ElementOutlinedButtonDefaults.shape,
- colors: ButtonColors = ElementOutlinedButtonDefaults.buttonColors(),
- elevation: ButtonElevation? = ElementOutlinedButtonDefaults.buttonElevation(),
- border: BorderStroke? = ElementOutlinedButtonDefaults.border,
- contentPadding: PaddingValues = ElementOutlinedButtonDefaults.ContentPadding,
- interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- content: @Composable RowScope.() -> Unit
-) {
- androidx.compose.material3.Button(
- onClick = onClick,
- modifier = modifier,
- enabled = enabled,
- shape = shape,
- colors = colors,
- elevation = elevation,
- border = border,
- contentPadding = contentPadding,
- interactionSource = interactionSource,
- content = content,
- )
-}
-
-object ElementOutlinedButtonDefaults {
- val ContentPadding = PaddingValues(horizontal = 24.dp, vertical = 14.dp)
- val shape: Shape @Composable get() = ButtonDefaults.outlinedShape
- val border: BorderStroke @Composable get() = ButtonDefaults.outlinedButtonBorder
- @Composable
- fun buttonElevation(): ButtonElevation = ButtonDefaults.buttonElevation()
-
- @Composable
- fun buttonColors(): ButtonColors = ButtonDefaults.outlinedButtonColors()
-
-
-}
-
-@Preview(group = PreviewGroup.Buttons)
-@Composable
-internal fun OutlinedButtonsPreview() = ElementThemedPreview { ContentToPreview() }
-
-@Composable
-private fun ContentToPreview() {
- Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
- OutlinedButton(onClick = {}, enabled = true) {
- Text(text = "Click me! - Enabled")
- }
- OutlinedButton(onClick = {}, enabled = false) {
- Text(text = "Click me! - Disabled")
- }
- }
-}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/RadioButton.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/RadioButton.kt
index 6b0c1b377e..a8b186a6b2 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/RadioButton.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/RadioButton.kt
@@ -17,15 +17,21 @@
package io.element.android.libraries.designsystem.theme.components
import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
import androidx.compose.material3.RadioButtonColors
import androidx.compose.material3.RadioButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
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.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
+import io.element.android.libraries.theme.ElementTheme
+
+// Designs in https://www.figma.com/file/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?type=design&node-id=425%3A24202&mode=design&t=qb99xBP5mwwCtGkN-1
@Composable
fun RadioButton(
@@ -33,7 +39,7 @@ fun RadioButton(
onClick: (() -> Unit)?,
modifier: Modifier = Modifier,
enabled: Boolean = true,
- colors: RadioButtonColors = RadioButtonDefaults.colors(),
+ colors: RadioButtonColors = compoundRadioButtonColors(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
) {
androidx.compose.material3.RadioButton(
@@ -46,6 +52,15 @@ fun RadioButton(
)
}
+@Composable
+internal fun compoundRadioButtonColors(): RadioButtonColors {
+ return RadioButtonDefaults.colors(
+ unselectedColor = ElementTheme.colors.borderInteractivePrimary,
+ disabledUnselectedColor = ElementTheme.colors.borderDisabled,
+ disabledSelectedColor = ElementTheme.colors.iconDisabled,
+ )
+}
+
@Preview(group = PreviewGroup.Toggles)
@Composable
internal fun RadioButtonPreview() = ElementThemedPreview(vertical = false) { ContentToPreview() }
@@ -53,9 +68,13 @@ internal fun RadioButtonPreview() = ElementThemedPreview(vertical = false) { Con
@Composable
private fun ContentToPreview() {
Column {
- RadioButton(selected = false, onClick = {})
- RadioButton(selected = true, onClick = {})
- RadioButton(selected = false, enabled = false, onClick = {})
- RadioButton(selected = true, enabled = false, onClick = {})
+ Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
+ RadioButton(selected = false, onClick = {})
+ RadioButton(selected = false, enabled = false, onClick = {})
+ }
+ Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
+ RadioButton(selected = true, onClick = {})
+ RadioButton(selected = true, enabled = false, onClick = {})
+ }
}
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt
index c129499a56..1eb73532d6 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt
@@ -62,7 +62,7 @@ fun SearchBar(
showBackButton: Boolean = true,
resultState: SearchBarResultState = SearchBarResultState.NotSearching(),
shape: Shape = SearchBarDefaults.inputFieldShape,
- tonalElevation: Dp = SearchBarDefaults.Elevation,
+ tonalElevation: Dp = SearchBarDefaults.TonalElevation,
windowInsets: WindowInsets = SearchBarDefaults.windowInsets,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
inactiveColors: SearchBarColors = ElementSearchBarDefaults.inactiveColors(),
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Snackbar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Snackbar.kt
new file mode 100644
index 0000000000..d2969fc3e9
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Snackbar.kt
@@ -0,0 +1,147 @@
+/*
+ * 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.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material3.SnackbarDefaults
+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.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import io.element.android.libraries.designsystem.components.button.ButtonVisuals
+import io.element.android.libraries.designsystem.preview.ElementThemedPreview
+import io.element.android.libraries.designsystem.preview.PreviewGroup
+import io.element.android.libraries.theme.ElementTheme
+import io.element.android.libraries.theme.SnackBarLabelColorDark
+import io.element.android.libraries.theme.SnackBarLabelColorLight
+
+@Composable
+fun Snackbar(
+ message: String,
+ modifier: Modifier = Modifier,
+ action: ButtonVisuals? = null,
+ dismissAction: ButtonVisuals? = null,
+ actionOnNewLine: Boolean = false,
+ shape: Shape = RoundedCornerShape(8.dp),
+ containerColor: Color = SnackbarDefaults.color,
+ contentColor: Color = ElementTheme.materialColors.inverseOnSurface,
+ actionContentColor: Color = actionContentColor(),
+ dismissActionContentColor: Color = SnackbarDefaults.dismissActionContentColor,
+) {
+ Snackbar(
+ modifier = modifier,
+ action = action?.let { @Composable { it.Composable() } },
+ dismissAction = dismissAction?.let { @Composable { it.Composable() } },
+ actionOnNewLine = actionOnNewLine,
+ shape = shape,
+ containerColor = containerColor,
+ contentColor = contentColor,
+ actionContentColor = actionContentColor,
+ dismissActionContentColor = dismissActionContentColor,
+ content = { Text(text = message) },
+ )
+}
+
+@Composable
+fun Snackbar(
+ modifier: Modifier = Modifier,
+ action: @Composable (() -> Unit)? = null,
+ dismissAction: @Composable (() -> Unit)? = null,
+ actionOnNewLine: Boolean = false,
+ shape: Shape = RoundedCornerShape(8.dp),
+ containerColor: Color = SnackbarDefaults.color,
+ contentColor: Color = ElementTheme.materialColors.inverseOnSurface,
+ actionContentColor: Color = actionContentColor(),
+ dismissActionContentColor: Color = SnackbarDefaults.dismissActionContentColor,
+ content: @Composable () -> Unit
+) {
+ androidx.compose.material3.Snackbar(
+ modifier = modifier,
+ action = action,
+ dismissAction = dismissAction,
+ actionOnNewLine = actionOnNewLine,
+ shape = shape,
+ containerColor = containerColor,
+ contentColor = contentColor,
+ actionContentColor = actionContentColor,
+ dismissActionContentColor = dismissActionContentColor,
+ content = content,
+ )
+}
+
+// TODO this color is temporary, an `inverse` version should be added to the semantic colors instead
+@Composable
+private fun actionContentColor(): Color {
+ return if (ElementTheme.isLightTheme) {
+ SnackBarLabelColorLight
+ } else {
+ SnackBarLabelColorDark
+ }
+}
+
+@Preview(name = "Snackbar", group = PreviewGroup.Snackbars)
+@Composable
+internal fun SnackbarPreview() {
+ ElementThemedPreview {
+ Snackbar(message = "Snackbar supporting text")
+ }
+}
+
+@Preview(name = "Snackbar with action", group = PreviewGroup.Snackbars)
+@Composable
+internal fun SnackbarWithActionPreview() {
+ ElementThemedPreview {
+ Snackbar(message = "Snackbar supporting text", action = ButtonVisuals.Text("Action", {}))
+ }
+}
+
+@Preview(name = "Snackbar with action and close button", group = PreviewGroup.Snackbars)
+@Composable
+internal fun SnackbarWithActionAndCloseButtonPreview() {
+ ElementThemedPreview {
+ Snackbar(
+ message = "Snackbar supporting text",
+ action = ButtonVisuals.Text("Action", {}),
+ dismissAction = ButtonVisuals.Icon(IconSource.Vector(Icons.Default.Close), {})
+ )
+ }
+}
+
+@Preview(name = "Snackbar with action on new line", group = PreviewGroup.Snackbars)
+@Composable
+internal fun SnackbarWithActionOnNewLinePreview() {
+ ElementThemedPreview {
+ Snackbar(message = "Snackbar supporting text", action = ButtonVisuals.Text("Action", {}), actionOnNewLine = true)
+ }
+}
+
+@Preview(name = "Snackbar with action and close button on new line", group = PreviewGroup.Snackbars)
+@Composable
+internal fun SnackbarWithActionOnNewLineAndCloseButtonPreview() {
+ ElementThemedPreview {
+ Snackbar(
+ message = "Snackbar supporting text",
+ action = ButtonVisuals.Text("Action", {}),
+ dismissAction = ButtonVisuals.Icon(IconSource.Vector(Icons.Default.Close), {}),
+ actionOnNewLine = true
+ )
+ }
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Switch.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Switch.kt
new file mode 100644
index 0000000000..ab4c9dee05
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Switch.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.
+ */
+
+package io.element.android.libraries.designsystem.theme.components
+
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.SwitchColors
+import androidx.compose.material3.SwitchDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+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.ElementThemedPreview
+import io.element.android.libraries.designsystem.preview.PreviewGroup
+import io.element.android.libraries.theme.ElementTheme
+import androidx.compose.material3.Switch as Material3Switch
+
+// Designs in https://www.figma.com/file/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?type=design&node-id=425%3A24203&mode=design&t=qb99xBP5mwwCtGkN-1
+
+@Composable
+fun Switch(
+ checked: Boolean,
+ onCheckedChange: ((Boolean) -> Unit)?,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ colors: SwitchColors = compoundSwitchColors(),
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ thumbContent: (@Composable () -> Unit)? = null,
+) {
+ Material3Switch(
+ checked = checked,
+ onCheckedChange = onCheckedChange,
+ modifier = modifier,
+ enabled = enabled,
+ colors = colors,
+ interactionSource = interactionSource,
+ thumbContent = thumbContent
+ )
+}
+
+@Composable
+internal fun compoundSwitchColors() = SwitchDefaults.colors(
+ uncheckedThumbColor = ElementTheme.colors.bgActionPrimaryRest,
+ uncheckedTrackColor = Color.Transparent,
+ disabledUncheckedBorderColor = ElementTheme.colors.borderDisabled,
+ disabledUncheckedThumbColor = ElementTheme.colors.iconDisabled,
+ disabledCheckedTrackColor = ElementTheme.colors.iconDisabled,
+ disabledCheckedBorderColor = ElementTheme.colors.iconDisabled,
+)
+
+@Preview(group = PreviewGroup.Toggles)
+@Composable
+internal fun SwitchPreview() {
+ var checked by remember { mutableStateOf(false) }
+ ElementThemedPreview {
+ Column(modifier = Modifier.padding(10.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
+ Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
+ Switch(checked = checked, onCheckedChange = { checked = !checked })
+ Switch(enabled = false, checked = checked, onCheckedChange = { checked = !checked })
+ }
+ Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
+ Switch(checked = !checked, onCheckedChange = { checked = !checked })
+ Switch(enabled = false, checked = !checked, onCheckedChange = { checked = !checked })
+ }
+ }
+ }
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextButton.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextButton.kt
deleted file mode 100644
index 3b4b50a5e7..0000000000
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextButton.kt
+++ /dev/null
@@ -1,76 +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.components
-
-import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.RowScope
-import androidx.compose.material3.ButtonColors
-import androidx.compose.material3.ButtonDefaults
-import androidx.compose.material3.ButtonElevation
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.tooling.preview.Preview
-import io.element.android.libraries.designsystem.preview.ElementThemedPreview
-import io.element.android.libraries.designsystem.preview.PreviewGroup
-
-@Composable
-fun TextButton(
- onClick: () -> Unit,
- modifier: Modifier = Modifier,
- enabled: Boolean = true,
- shape: Shape = ButtonDefaults.textShape,
- colors: ButtonColors = ButtonDefaults.textButtonColors(),
- elevation: ButtonElevation? = null,
- border: BorderStroke? = null,
- contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding,
- interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- content: @Composable RowScope.() -> Unit
-) {
- androidx.compose.material3.TextButton(
- onClick = onClick,
- modifier = modifier,
- enabled = enabled,
- shape = shape,
- colors = colors,
- elevation = elevation,
- border = border,
- contentPadding = contentPadding,
- interactionSource = interactionSource,
- content = content,
- )
-}
-
-@Preview(group = PreviewGroup.Buttons)
-@Composable
-internal fun TextButtonPreview() = ElementThemedPreview { ContentToPreview() }
-
-@Composable
-private fun ContentToPreview() {
- Column {
- TextButton(onClick = {}, enabled = true) {
- Text(text = "Click me! - Enabled")
- }
- TextButton(onClick = {}, enabled = false) {
- Text(text = "Click me! - Disabled")
- }
- }
-}
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 23848ef76d..93d11e8c9a 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
@@ -18,15 +18,21 @@ package io.element.android.libraries.designsystem.theme.components
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.TopAppBarColors
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
+import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
+import io.element.android.libraries.theme.ElementTheme
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -43,7 +49,11 @@ fun TopAppBar(
title = title,
modifier = modifier,
navigationIcon = navigationIcon,
- actions = actions,
+ actions = {
+ CompositionLocalProvider(LocalContentColor provides ElementTheme.colors.textActionPrimary) {
+ actions()
+ }
+ },
windowInsets = windowInsets,
colors = colors,
scrollBehavior = scrollBehavior,
@@ -58,5 +68,14 @@ internal fun TopAppBarPreview() =
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ContentToPreview() {
- TopAppBar(title = { Text(text = "Title") })
+ TopAppBar(
+ title = { Text(text = "Title") },
+ navigationIcon = { BackButton(onClick = {}) },
+ actions = {
+ TextButton(text = "Action", onClick = {})
+ IconButton(onClick = {}) {
+ Icon(imageVector = Icons.Default.Share, contentDescription = null)
+ }
+ }
+ )
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/DatePickerPreview.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/DatePickerPreview.kt
index a422c5e729..45b5eb39be 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/DatePickerPreview.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/DatePickerPreview.kt
@@ -23,7 +23,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
-import io.element.android.libraries.designsystem.components.dialogs.AlertDialogContent
+import io.element.android.libraries.designsystem.theme.components.AlertDialogContent
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.PreviewGroup
@@ -44,7 +44,7 @@ internal fun DatePickerPreviewDark() {
@Composable
private fun ContentToPreview() {
val state = rememberDatePickerState(
- initialSelectedDateMillis = 1672578000000L,
+ initialSelectedDateMillis = 1_672_578_000_000L,
)
AlertDialogContent(
buttons = { /*TODO*/ },
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/MenuPreview.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/MenuPreview.kt
index f1c2cd444d..2ca04f1008 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/MenuPreview.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/MenuPreview.kt
@@ -30,7 +30,6 @@ import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.DropdownMenu
import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem
-import io.element.android.libraries.designsystem.theme.components.DropdownMenuItemText
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
@@ -39,9 +38,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
internal fun MenuPreview() {
ElementThemedPreview {
var isExpanded by remember { mutableStateOf(false) }
- Button(onClick = { isExpanded = !isExpanded }) {
- Text("Toggle")
- }
+ Button(text = "Toggle", onClick = { isExpanded = !isExpanded })
DropdownMenu(expanded = isExpanded, onDismissRequest = { isExpanded = false }) {
for (i in 0..5) {
val leadingIcon: @Composable (() -> Unit)? = if (i in 2..3) {
@@ -60,7 +57,7 @@ internal fun MenuPreview() {
null
}
DropdownMenuItem(
- text = { DropdownMenuItemText(text = "Item $i") },
+ text = { Text(text = "Item $i") },
onClick = { isExpanded = false },
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/SwitchPreview.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/SwitchPreview.kt
deleted file mode 100644
index 11491a0a1c..0000000000
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/SwitchPreview.kt
+++ /dev/null
@@ -1,51 +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.components.previews
-
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Row
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.Check
-import androidx.compose.material3.Switch
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import io.element.android.libraries.designsystem.preview.ElementThemedPreview
-import io.element.android.libraries.designsystem.preview.PreviewGroup
-import io.element.android.libraries.designsystem.theme.components.Icon
-
-@Preview(group = PreviewGroup.Toggles)
-@Composable
-internal fun SwitchPreview() {
- ElementThemedPreview {
- Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
- var checked by remember { mutableStateOf(false) }
- Switch(checked = checked, onCheckedChange = { checked = !checked })
- Switch(checked = checked, onCheckedChange = { checked = !checked }, thumbContent = {
- Icon(imageVector = Icons.Outlined.Check, contentDescription = null)
- })
- Switch(checked = checked, enabled = false, onCheckedChange = { checked = !checked })
- Switch(checked = checked, enabled = false, onCheckedChange = { checked = !checked }, thumbContent = {
- Icon(imageVector = Icons.Outlined.Check, contentDescription = null)
- })
- }
- }
-}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/TimePickerPreview.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/TimePickerPreview.kt
index 79f0fffbee..7aae42ed0e 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/TimePickerPreview.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/previews/TimePickerPreview.kt
@@ -24,7 +24,7 @@ import androidx.compose.material3.TimePickerLayoutType
import androidx.compose.material3.rememberTimePickerState
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
-import io.element.android.libraries.designsystem.components.dialogs.AlertDialogContent
+import io.element.android.libraries.designsystem.theme.components.AlertDialogContent
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/LogCompositions.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/LogCompositions.kt
index dcbef866fe..2618a5de82 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/LogCompositions.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/LogCompositions.kt
@@ -28,10 +28,12 @@ import timber.log.Timber
@Composable
fun LogCompositions(tag: String, msg: String) {
if (BuildConfig.DEBUG) {
- val ref = remember { Ref(0) }
+ val ref = remember { Ref() }
SideEffect { ref.value++ }
Timber.tag(tag).d("Compositions: $msg ${ref.value}")
}
}
-class Ref(var value: Int)
+private class Ref {
+ var value: Int = 0
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Snackbar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Snackbar.kt
index f4e44779d1..4513a90914 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Snackbar.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Snackbar.kt
@@ -17,6 +17,8 @@
package io.element.android.libraries.designsystem.utils
import androidx.annotation.StringRes
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
@@ -25,7 +27,11 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
+import io.element.android.libraries.designsystem.components.button.ButtonVisuals
+import io.element.android.libraries.designsystem.theme.components.IconSource
+import io.element.android.libraries.designsystem.theme.components.Snackbar
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -65,6 +71,19 @@ fun SnackbarDispatcher.collectSnackbarMessageAsState(): State
return snackbarMessage.collectAsState(initial = null)
}
+@Composable
+fun SnackbarHost(hostState: SnackbarHostState, modifier: Modifier = Modifier) {
+ androidx.compose.material3.SnackbarHost(hostState, modifier) { data ->
+ Snackbar(
+ message = data.visuals.message,
+ action = data.visuals.actionLabel?.let { ButtonVisuals.Text(it, data::performAction) },
+ dismissAction = if (data.visuals.withDismissAction) {
+ ButtonVisuals.Icon(IconSource.Vector(Icons.Default.Close), data::dismiss)
+ } else null,
+ )
+ }
+}
+
@Composable
fun rememberSnackbarHostState(snackbarMessage: SnackbarMessage?): SnackbarHostState {
val snackbarHostState = remember { SnackbarHostState() }
diff --git a/libraries/designsystem/src/main/res/drawable/ic_baseline_reply_24.xml b/libraries/designsystem/src/main/res/drawable/ic_baseline_reply_24.xml
deleted file mode 100644
index 96a220a5cd..0000000000
--- a/libraries/designsystem/src/main/res/drawable/ic_baseline_reply_24.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
diff --git a/libraries/designsystem/src/main/res/drawable/ic_content_arrow_forward.xml b/libraries/designsystem/src/main/res/drawable/ic_content_arrow_forward.xml
deleted file mode 100644
index 739053947d..0000000000
--- a/libraries/designsystem/src/main/res/drawable/ic_content_arrow_forward.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt
index f1af61c8e7..0736cf61ce 100644
--- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt
+++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt
@@ -37,6 +37,8 @@ import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessa
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
import io.element.android.libraries.matrix.api.timeline.item.event.MessageType
import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
+import io.element.android.libraries.matrix.api.timeline.item.event.PollEndContent
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
@@ -94,6 +96,7 @@ class DefaultRoomLastMessageFormatter @Inject constructor(
is StateContent -> {
stateContentFormatter.format(content, senderDisplayName, isOutgoing, RenderingMode.RoomList)
}
+ is PollContent, is PollEndContent, // TODO Polls: handle last message
is FailedToParseMessageLikeContent, is FailedToParseStateContent, is UnknownContent -> {
prefixIfNeeded(sp.getString(CommonStrings.common_unsupported_event), senderDisplayName, isDmRoom)
}
diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultTimelineEventFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultTimelineEventFormatter.kt
index 8f89233a31..42d8aae083 100644
--- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultTimelineEventFormatter.kt
+++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultTimelineEventFormatter.kt
@@ -26,6 +26,8 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventTimeline
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
+import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
+import io.element.android.libraries.matrix.api.timeline.item.event.PollEndContent
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
@@ -63,6 +65,8 @@ class DefaultTimelineEventFormatter @Inject constructor(
}
RedactedContent,
is StickerContent,
+ is PollContent,
+ is PollEndContent,
is UnableToDecryptContent,
is MessageContent,
is FailedToParseMessageLikeContent,
diff --git a/libraries/eventformatter/impl/src/main/res/values-ru/translations.xml b/libraries/eventformatter/impl/src/main/res/values-ru/translations.xml
new file mode 100644
index 0000000000..4c1defbb68
--- /dev/null
+++ b/libraries/eventformatter/impl/src/main/res/values-ru/translations.xml
@@ -0,0 +1,57 @@
+
+
+ "(аватар тоже был изменен)"
+ "%1$s сменили свой аватар"
+ "Вы сменили аватар"
+ "%1$s изменил свое отображаемое имя с %2$s на %3$s"
+ "Вы изменили свое отображаемое имя с %1$s на %2$s"
+ "%1$s удалил свое отображаемое имя (оно было %2$s)"
+ "Вы удалили свое отображаемое имя (оно было %1$s)"
+ "%1$s установили свое отображаемое имя на %2$s"
+ "Вы установили отображаемое имя на %1$s"
+ "%1$s изменил аватар комнаты"
+ "Вы изменили аватар комнаты"
+ "%1$s удалил аватар комнаты"
+ "Вы удалили аватар комнаты"
+ "%1$s заблокирован %2$s"
+ "Вы заблокировали %1$s"
+ "%1$s создал комнату"
+ "Вы создали комнату"
+ "%1$s пригласил %2$s"
+ "%1$s принял приглашение"
+ "Вы приняли приглашение"
+ "Вы пригласили %1$s"
+ "Пользователь %1$s пригласил вас"
+ "%1$s присоединился к комнате"
+ "Вы вошли в комнату"
+ "%1$s запросил присоединение"
+ "%1$s разрешил %2$s присоединиться"
+ "%1$s разрешил вам присоединиться"
+ "Вы запросили присоединение"
+ "%1$s отклонил запрос %2$s на присоединение"
+ "Вы отклонили запрос %1$s на присоединение"
+ "%1$s отклонил ваш запрос на присоединение"
+ "%1$s больше не заинтересован в присоединении"
+ "Вы отменили запрос на присоединение"
+ "%1$s покинул комнату"
+ "Вы вышли из комнаты"
+ "%1$s изменил название комнаты на: %2$s"
+ "Вы изменили название комнаты на: %1$s"
+ "%1$s удалил название комнаты"
+ "Вы удалили название комнаты"
+ "%1$s отклонил приглашение"
+ "Вы отклонили приглашение"
+ "%1$s удалил %2$s"
+ "Вы удалили %1$s"
+ "%1$s отправила приглашение %2$s присоединиться к комнате"
+ "Вы отправили приглашение присоединиться к комнате %1$s"
+ "%1$s отозвал приглашение %2$s присоединиться к комнате"
+ "Вы отозвали приглашение %1$s присоединиться к комнате"
+ "%1$s изменил тему на: %2$s"
+ "Вы изменили тему на: %1$s"
+ "%1$s удалил тему комнаты"
+ "Вы удалили тему комнаты"
+ "%1$s разблокирован %2$s"
+ "Вы разблокировали %1$s"
+ "%1$s внес неизвестное изменение в составе"
+
diff --git a/libraries/eventformatter/impl/src/main/res/values-zh-rTW/translations.xml b/libraries/eventformatter/impl/src/main/res/values-zh-rTW/translations.xml
new file mode 100644
index 0000000000..45ab0acee1
--- /dev/null
+++ b/libraries/eventformatter/impl/src/main/res/values-zh-rTW/translations.xml
@@ -0,0 +1,37 @@
+
+
+ "%1$s將他的顯示名稱從%2$s變更為%3$s"
+ "您將您的顯示名稱從%1$s1變更為%2$s"
+ "%1$s的顯示名稱已被本人移除(原為%2$s)"
+ "您的顯示名稱已被您移除(原為%1$s)"
+ "%1$s將他的顯示名稱設為%2$s"
+ "您將您的顯示名稱設為%1$s"
+ "%1$s建立此聊天室"
+ "您建立此聊天室"
+ "%1$s邀請%2$s"
+ "%1$s接受邀請"
+ "您接受邀請"
+ "您邀請%1$s"
+ "%1$s邀請您"
+ "%1$s加入聊天室"
+ "您加入聊天室"
+ "%1$s請求加入"
+ "您請求加入"
+ "%1$s拒絕%2$s的加入請求"
+ "您拒絕%1$s的加入請求"
+ "%1$s拒絕您的加入請求"
+ "%1$s離開聊天室"
+ "您離開聊天室"
+ "%1$s將聊天室名稱變更為%2$s"
+ "您將聊天室名稱變更為%1$s"
+ "聊天室名稱已被%1$s移除"
+ "聊天室名稱已被您移除"
+ "%2$s已被%1$s移除"
+ "%1$s已被您移除"
+ "%1$s邀請%2$s加入聊天室"
+ "您邀請%1$s加入聊天室"
+ "%1$s將主題變更為%2$s"
+ "您將主題變更為%1$s"
+ "聊天室主題已被%1$s移除"
+ "聊天室主題已被您移除"
+
diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt
index 4c1bfed33a..176bacb2c4 100644
--- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt
+++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt
@@ -25,5 +25,11 @@ enum class FeatureFlags(
LocationSharing(
key = "feature.locationsharing",
title = "Allow user to share location",
+ ),
+ Polls(
+ key = "feature.polls",
+ title = "Polls",
+ description = "Render poll events in the timeline",
+ defaultValue = false,
)
}
diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/BuildtimeFeatureFlagProvider.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/BuildtimeFeatureFlagProvider.kt
index d226885495..83913cbac5 100644
--- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/BuildtimeFeatureFlagProvider.kt
+++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/BuildtimeFeatureFlagProvider.kt
@@ -30,6 +30,7 @@ class BuildtimeFeatureFlagProvider @Inject constructor() :
return if (feature is FeatureFlags) {
when (feature) {
FeatureFlags.LocationSharing -> true
+ FeatureFlags.Polls -> false
}
} else {
false
diff --git a/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/FeatureListView.kt b/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/FeatureListView.kt
index 87d0125278..f0fe6b3c04 100644
--- a/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/FeatureListView.kt
+++ b/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/FeatureListView.kt
@@ -54,6 +54,7 @@ fun FeaturePreferenceView(
) {
PreferenceCheckbox(
title = feature.title,
+ supportingText = feature.description,
isChecked = feature.isEnabled,
modifier = modifier,
onCheckedChange = onCheckedChange
diff --git a/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/model/FeatureUiModel.kt b/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/model/FeatureUiModel.kt
index 5a3eecebe0..9176eb168a 100644
--- a/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/model/FeatureUiModel.kt
+++ b/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/model/FeatureUiModel.kt
@@ -19,5 +19,6 @@ package io.element.android.libraries.featureflag.ui.model
data class FeatureUiModel(
val key: String,
val title: String,
+ val description: String?,
val isEnabled: Boolean
)
diff --git a/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/model/FeatureUiModelProvider.kt b/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/model/FeatureUiModelProvider.kt
index 233331d46d..12ff3b4029 100644
--- a/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/model/FeatureUiModelProvider.kt
+++ b/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/model/FeatureUiModelProvider.kt
@@ -21,7 +21,7 @@ import kotlinx.collections.immutable.persistentListOf
fun aFeatureUiModelList(): ImmutableList {
return persistentListOf(
- FeatureUiModel("key1", "Display State Events", true),
- FeatureUiModel("key2", "Display Room Events", false)
+ FeatureUiModel("key1", "Display State Events", "Show state events in the timeline", true),
+ FeatureUiModel("key2", "Display Room Events", null, false),
)
}
diff --git a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapboxMap.kt b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapboxMap.kt
index c67be52c1c..5af79e7524 100644
--- a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapboxMap.kt
+++ b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapboxMap.kt
@@ -236,7 +236,7 @@ private fun MapView.lifecycleObserver(previousState: MutableState {
//handled in onDispose
}
- else -> throw IllegalStateException()
+ Lifecycle.Event.ON_ANY -> error("ON_ANY should never be used")
}
previousState.value = event
}
diff --git a/libraries/matrix/api/build.gradle.kts b/libraries/matrix/api/build.gradle.kts
index 4ff5f3450d..a1d508885a 100644
--- a/libraries/matrix/api/build.gradle.kts
+++ b/libraries/matrix/api/build.gradle.kts
@@ -18,7 +18,7 @@ plugins {
id("io.element.android-library")
id("kotlin-parcelize")
alias(libs.plugins.anvil)
- kotlin("plugin.serialization") version "1.8.22"
+ kotlin("plugin.serialization") version "1.9.0"
}
android {
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt
index 747de5f554..67c0625a91 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt
@@ -26,7 +26,7 @@ import io.element.android.libraries.matrix.api.notification.NotificationService
import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
-import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource
+import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
import io.element.android.libraries.matrix.api.user.MatrixUser
@@ -35,7 +35,7 @@ import java.io.Closeable
interface MatrixClient : Closeable {
val sessionId: SessionId
- val roomSummaryDataSource: RoomSummaryDataSource
+ val roomListService: RoomListService
val mediaLoader: MatrixMediaLoader
suspend fun getRoom(roomId: RoomId): MatrixRoom?
suspend fun findDM(userId: UserId): MatrixRoom?
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt
index bc0f0c04bc..a668448752 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt
@@ -30,7 +30,8 @@ object MatrixPatterns {
// regex pattern to find matrix user ids in a string.
// See https://matrix.org/docs/spec/appendices#historical-user-ids
- private const val MATRIX_USER_IDENTIFIER_REGEX = "@[A-Z0-9\\x21-\\x39\\x3B-\\x7F]+$DOMAIN_REGEX"
+ // Sadly, we need to relax the regex pattern a bit as there already exist some ids that don't match the spec.
+ private const val MATRIX_USER_IDENTIFIER_REGEX = "^@.*?$DOMAIN_REGEX$"
val PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = MATRIX_USER_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE)
// regex pattern to find room ids in a string.
@@ -42,7 +43,8 @@ object MatrixPatterns {
private val PATTERN_CONTAIN_MATRIX_ALIAS = MATRIX_ROOM_ALIAS_REGEX.toRegex(RegexOption.IGNORE_CASE)
// regex pattern to find message ids in a string.
- private const val MATRIX_EVENT_IDENTIFIER_REGEX = "\\$[A-Z0-9]+$DOMAIN_REGEX"
+ // Sadly, we need to relax the regex pattern a bit as there already exist some ids that don't match the spec.
+ private const val MATRIX_EVENT_IDENTIFIER_REGEX = "^\\$.+$DOMAIN_REGEX$"
private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER = MATRIX_EVENT_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE)
// regex pattern to find message ids in a string.
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaUploadHandler.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaUploadHandler.kt
new file mode 100644
index 0000000000..17d204715e
--- /dev/null
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaUploadHandler.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.matrix.api.media
+
+/**
+ * This is an abstraction over the Rust SDK's `SendAttachmentJoinHandle` which allows us to either [await] the upload process or [cancel] it.
+ */
+interface MediaUploadHandler {
+ /** Await the upload process to finish. */
+ suspend fun await(): Result
+
+ /** Cancel the upload process. */
+ fun cancel()
+}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt
index 4ded947d56..639509a15a 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt
@@ -23,7 +23,6 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.timeline.item.event.MessageType
data class NotificationData(
- val senderId: UserId,
val eventId: EventId,
val roomId: RoomId,
val senderAvatarUrl: String?,
@@ -33,14 +32,10 @@ data class NotificationData(
val isDirect: Boolean,
val isEncrypted: Boolean,
val isNoisy: Boolean,
- val event: NotificationEvent,
-)
-
-data class NotificationEvent(
val timestamp: Long,
val content: NotificationContent,
// For images for instance
- val contentUrl: String?
+ val contentUrl: String?,
)
sealed interface NotificationContent {
@@ -61,6 +56,7 @@ sealed interface NotificationContent {
) : MessageLike
object RoomEncrypted : MessageLike
data class RoomMessage(
+ val senderId: UserId,
val messageType: MessageType
) : MessageLike
object RoomRedaction : MessageLike
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt
index 2046252930..972873ab38 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt
@@ -21,5 +21,5 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
interface NotificationService {
- fun getNotification(userId: SessionId, roomId: RoomId, eventId: EventId, filterByPushRules: Boolean): Result
+ suspend fun getNotification(userId: SessionId, roomId: RoomId, eventId: EventId): Result
}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParser.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParser.kt
index fc900e4bbb..ba9cdc1e80 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParser.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParser.kt
@@ -95,9 +95,9 @@ object PermalinkParser {
return if (signUrl.isNullOrEmpty().not() && email.isNullOrEmpty().not()) {
try {
val signValidUri = Uri.parse(signUrl)
- val identityServerHost = signValidUri.authority ?: throw IllegalArgumentException()
- val token = signValidUri.getQueryParameter("token") ?: throw IllegalArgumentException()
- val privateKey = signValidUri.getQueryParameter("private_key") ?: throw IllegalArgumentException()
+ val identityServerHost = signValidUri.authority ?: throw IllegalArgumentException("missing `authority`")
+ val token = signValidUri.getQueryParameter("token") ?: throw IllegalArgumentException("missing `token`")
+ val privateKey = signValidUri.getQueryParameter("private_key") ?: throw IllegalArgumentException("missing `private_key`")
PermalinkData.RoomEmailInviteLink(
roomId = identifier,
email = email!!,
@@ -137,7 +137,8 @@ object PermalinkParser {
.parameterList
.filter {
it.mParameter == "via"
- }.map {
+ }
+ .map {
URLDecoder.decode(it.mValue, "UTF-8")
}
}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/poll/PollAnswer.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/poll/PollAnswer.kt
new file mode 100644
index 0000000000..2d4abaafb5
--- /dev/null
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/poll/PollAnswer.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.matrix.api.poll
+
+data class PollAnswer(
+ val id: String,
+ val text: String
+)
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/poll/PollKind.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/poll/PollKind.kt
new file mode 100644
index 0000000000..85bb7c0256
--- /dev/null
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/poll/PollKind.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.matrix.api.poll
+
+enum class PollKind {
+ /** Voters should see results as soon as they have voted. */
+ Disclosed,
+
+ /** Results should be only revealed when the poll is ended. */
+ Undisclosed
+}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt
index 1445f85228..f2c51c357e 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt
@@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.media.AudioInfo
import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ImageInfo
+import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
@@ -81,13 +82,13 @@ interface MatrixRoom : Closeable {
suspend fun redactEvent(eventId: EventId, reason: String? = null): Result
- suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo, progressCallback: ProgressCallback?): Result
+ suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo, progressCallback: ProgressCallback?): Result
- suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo, progressCallback: ProgressCallback?): Result
+ suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo, progressCallback: ProgressCallback?): Result
- suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result
+ suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result
- suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result
+ suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result
suspend fun toggleReaction(emoji: String, eventId: EventId): Result
@@ -105,6 +106,8 @@ interface MatrixRoom : Closeable {
suspend fun canUserInvite(userId: UserId): Result
+ suspend fun canUserRedact(userId: UserId): Result
+
suspend fun canUserSendState(userId: UserId, type: StateEventType): Result
suspend fun canUserSendMessage(userId: UserId, type: MessageEventType): Result
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomPowerLevels.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomPowerLevels.kt
index 852401bffc..e0ba452efe 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomPowerLevels.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomPowerLevels.kt
@@ -34,3 +34,9 @@ suspend fun MatrixRoom.canSendState(type: StateEventType): Result = can
* Shortcut for calling [MatrixRoom.canUserSendMessage] with our own user.
*/
suspend fun MatrixRoom.canSendMessage(type: MessageEventType): Result = canUserSendMessage(sessionId, type)
+
+/**
+ * Shortcut for calling [MatrixRoom.canUserRedact] with our own user.
+ */
+suspend fun MatrixRoom.canRedact(): Result = canUserRedact(sessionId)
+
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomSummaryDataSource.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomList.kt
similarity index 65%
rename from libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomSummaryDataSource.kt
rename to libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomList.kt
index d677d56ed9..8714bc2c5c 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomSummaryDataSource.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomList.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package io.element.android.libraries.matrix.api.room
+package io.element.android.libraries.matrix.api.roomlist
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.flow.StateFlow
@@ -23,25 +23,34 @@ import kotlinx.coroutines.withTimeout
import timber.log.Timber
import kotlin.time.Duration
-interface RoomSummaryDataSource {
-
+/**
+ * Holds some flows related to a specific set of rooms.
+ * Can be retrieved from [RoomListService] methods.
+ */
+interface RoomList {
sealed class LoadingState {
object NotLoaded : LoadingState()
data class Loaded(val numberOfRooms: Int) : LoadingState()
}
- fun updateAllRoomsVisibleRange(range: IntRange)
- fun allRoomsLoadingState(): StateFlow
- fun allRooms(): StateFlow>
- fun inviteRooms(): StateFlow>
+ /**
+ * The list of room summaries as a flow.
+ */
+ val summaries: StateFlow>
+
+ /**
+ * The loading state of the room list as a flow.
+ * This is useful to know if a specific set of rooms is loaded or not.
+ */
+ val loadingState: StateFlow
}
-suspend fun RoomSummaryDataSource.awaitAllRoomsAreLoaded(timeout: Duration = Duration.INFINITE) {
+suspend fun RoomList.awaitLoaded(timeout: Duration = Duration.INFINITE) {
try {
Timber.d("awaitAllRoomsAreLoaded: wait")
withTimeout(timeout) {
- allRoomsLoadingState().firstOrNull {
- it is RoomSummaryDataSource.LoadingState.Loaded
+ loadingState.firstOrNull {
+ it is RoomList.LoadingState.Loaded
}
}
} catch (timeoutException: TimeoutCancellationException) {
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt
new file mode 100644
index 0000000000..99381d0e74
--- /dev/null
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.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.matrix.api.roomlist
+
+import kotlinx.coroutines.flow.StateFlow
+
+/**
+ * Entry point for the room list api.
+ * This service will provide different sets of rooms (all, invites, etc.).
+ * It requires the SyncService to be started to receive updates.
+ */
+interface RoomListService {
+
+ sealed class State {
+ object Idle : State()
+ object Running : State()
+ object Error : State()
+ object Terminated : State()
+ }
+
+ /**
+ * returns a [RoomList] object of all rooms we want to display.
+ * This will exclude some rooms like the invites, or spaces.
+ */
+ fun allRooms(): RoomList
+
+ /**
+ * returns a [RoomList] object of all invites.
+ */
+ fun invites(): RoomList
+
+ /**
+ * Will set the visible range of all rooms.
+ * This is useful to load more data when the user scrolls down.
+ */
+ fun updateAllRoomsVisibleRange(range: IntRange)
+
+ /**
+ * The state of the service as a flow.
+ */
+ val state: StateFlow
+}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomSummary.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomSummary.kt
similarity index 92%
rename from libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomSummary.kt
rename to libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomSummary.kt
index 7dedd86b63..87cf2139d6 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomSummary.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomSummary.kt
@@ -14,9 +14,10 @@
* limitations under the License.
*/
-package io.element.android.libraries.matrix.api.room
+package io.element.android.libraries.matrix.api.roomlist
import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.message.RoomMessage
sealed interface RoomSummary {
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/sync/SyncService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/sync/SyncService.kt
index 5271ec9bc0..994b35edc4 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/sync/SyncService.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/sync/SyncService.kt
@@ -27,7 +27,7 @@ interface SyncService {
/**
* Tries to stop the sync. If service is not syncing it has no effect.
*/
- fun stopSync(): Result
+ suspend fun stopSync(): Result
/**
* Flow of [SyncState]. Will be updated as soon as the current [SyncState] changes.
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt
index 203fb30794..82c322668c 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt
@@ -23,6 +23,8 @@ import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.media.VideoInfo
+import io.element.android.libraries.matrix.api.poll.PollAnswer
+import io.element.android.libraries.matrix.api.poll.PollKind
sealed interface EventContent
@@ -44,7 +46,7 @@ sealed interface InReplyTo {
/** The event details are available. */
data class Ready(
val eventId: EventId,
- val content: MessageContent,
+ val content: EventContent,
val senderId: UserId,
val senderDisplayName: String?,
val senderAvatarUrl: String?,
@@ -69,6 +71,19 @@ data class StickerContent(
val url: String
) : EventContent
+data class PollContent(
+ val question: String,
+ val kind: PollKind,
+ val maxSelections: ULong,
+ val answers: List,
+ val votes: Map>,
+ val endTime: ULong?
+) : EventContent
+
+data class PollEndContent(
+ val startEventId: String
+) : EventContent
+
data class UnableToDecryptContent(
val data: Data
) : EventContent {
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventReaction.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventReaction.kt
index 8bea4b5330..a2e68d17d2 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventReaction.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventReaction.kt
@@ -16,10 +16,7 @@
package io.element.android.libraries.matrix.api.timeline.item.event
-import io.element.android.libraries.matrix.api.core.UserId
-
data class EventReaction(
val key: String,
- val count: Long,
- val senderIds: List
+ val senders: List
)
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/ReactionSender.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/ReactionSender.kt
new file mode 100644
index 0000000000..60398cffd5
--- /dev/null
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/ReactionSender.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.libraries.matrix.api.timeline.item.event
+
+import io.element.android.libraries.matrix.api.core.UserId
+
+/**
+ * The sender of a reaction.
+ *
+ * @property senderId the ID of the user who sent the reaction
+ * @property timestamp the timestamp the reaction was received on the origin homeserver
+ */
+data class ReactionSender(
+ val senderId: UserId,
+ val timestamp: Long
+)
+
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingConfiguration.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingConfiguration.kt
index 8bcd602b9f..6381cc7ed8 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingConfiguration.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingConfiguration.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.
@@ -17,64 +17,7 @@
package io.element.android.libraries.matrix.api.tracing
data class TracingConfiguration(
- val overrides: Map = emptyMap()
-) {
-
- // Order should matters
- private val targets: MutableMap = mutableMapOf(
- Target.Common to LogLevel.Warn,
- Target.Hyper to LogLevel.Warn,
- Target.Sled to LogLevel.Warn,
- Target.MatrixSdk.Root to LogLevel.Warn,
- Target.MatrixSdk.Sled to LogLevel.Warn,
- Target.MatrixSdk.Crypto to LogLevel.Debug,
- Target.MatrixSdk.HttpClient to LogLevel.Debug,
- Target.MatrixSdk.SlidingSync to LogLevel.Trace,
- Target.MatrixSdk.BaseSlidingSync to LogLevel.Trace,
- )
-
- val filter: String
- get() {
- overrides.forEach { (target, logLevel) ->
- targets[target] = logLevel
- }
- return targets.map {
- if (it.key.filter.isEmpty()) {
- it.value.filter
- } else {
- "${it.key.filter}=${it.value.filter}"
- }
- }.joinToString(separator = ",")
- }
-}
-
-sealed class Target(open val filter: String) {
- object Common : Target("")
- object Hyper : Target("hyper")
- object Sled : Target("sled")
- sealed class MatrixSdk(override val filter: String) : Target(filter) {
- object Root : MatrixSdk("matrix_sdk")
- object Sled : MatrixSdk("matrix_sdk_sled")
- object Crypto: MatrixSdk("matrix_sdk_crypto")
- object FFI : MatrixSdk("matrix_sdk_ffi")
- object HttpClient : MatrixSdk("matrix_sdk::http_client")
- object UniffiAPI : MatrixSdk("matrix_sdk_ffi::uniffi_api")
- object SlidingSync : MatrixSdk("matrix_sdk::sliding_sync")
- object BaseSlidingSync : MatrixSdk("matrix_sdk_base::sliding_sync")
- }
-}
-
-sealed class LogLevel(val filter: String) {
- object Warn : LogLevel("warn")
- object Trace : LogLevel("trace")
- object Info : LogLevel("info")
- object Debug : LogLevel("debug")
- object Error : LogLevel("error")
-}
-
-object TracingConfigurations {
- val release = TracingConfiguration(overrides = mapOf(Target.Common to LogLevel.Info))
- val debug = TracingConfiguration(overrides = mapOf(Target.Common to LogLevel.Info))
-
- fun custom(overrides: Map) = TracingConfiguration(overrides)
-}
+ val filterConfiguration: TracingFilterConfiguration,
+ val writesToLogcat: Boolean,
+ val writesToFilesConfiguration: WriteToFilesConfiguration,
+)
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingFilterConfiguration.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingFilterConfiguration.kt
new file mode 100644
index 0000000000..21c6954c2a
--- /dev/null
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingFilterConfiguration.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.libraries.matrix.api.tracing
+
+data class TracingFilterConfiguration(
+ val overrides: Map = emptyMap(),
+) {
+
+ // Order should matters
+ private val targetsToLogLevel: MutableMap = mutableMapOf(
+ Target.COMMON to LogLevel.Info,
+ Target.HYPER to LogLevel.Warn,
+ Target.MATRIX_SDK_CRYPTO to LogLevel.Debug,
+ Target.MATRIX_SDK_HTTP_CLIENT to LogLevel.Debug,
+ Target.MATRIX_SDK_SLIDING_SYNC to LogLevel.Trace,
+ Target.MATRIX_SDK_BASE_SLIDING_SYNC to LogLevel.Trace,
+ Target.MATRIX_SDK_UI_TIMELINE to LogLevel.Info,
+ )
+
+ val filter: String
+ get() {
+ overrides.forEach { (target, logLevel) ->
+ targetsToLogLevel[target] = logLevel
+ }
+ return targetsToLogLevel.map {
+ if (it.key.filter.isEmpty()) {
+ it.value.filter
+ } else {
+ "${it.key.filter}=${it.value.filter}"
+ }
+ }.joinToString(separator = ",")
+ }
+}
+
+enum class Target(open val filter: String) {
+ COMMON(""),
+ ELEMENT("elementx"),
+ HYPER("hyper"),
+ MATRIX_SDK_FFI("matrix_sdk_ffi"),
+ MATRIX_SDK_UNIFFI_API("matrix_sdk_ffi::uniffi_api"),
+ MATRIX_SDK_CRYPTO("matrix_sdk_crypto"),
+ MATRIX_SDK_HTTP_CLIENT("matrix_sdk::http_client"),
+ MATRIX_SDK_SLIDING_SYNC("matrix_sdk::sliding_sync"),
+ MATRIX_SDK_BASE_SLIDING_SYNC("matrix_sdk_base::sliding_sync"),
+ MATRIX_SDK_UI_TIMELINE("matrix_sdk_ui::timeline"),
+}
+
+sealed class LogLevel(val filter: String) {
+ object Warn : LogLevel("warn")
+ object Trace : LogLevel("trace")
+ object Info : LogLevel("info")
+ object Debug : LogLevel("debug")
+ object Error : LogLevel("error")
+}
+
+object TracingFilterConfigurations {
+ val release = TracingFilterConfiguration(
+ overrides = mapOf(
+ Target.COMMON to LogLevel.Info,
+ Target.ELEMENT to LogLevel.Debug
+ ),
+ )
+ val debug = TracingFilterConfiguration(
+ overrides = mapOf(
+ Target.COMMON to LogLevel.Info,
+ Target.ELEMENT to LogLevel.Trace
+ )
+ )
+
+ /**
+ * Use this method to create a custom configuration where all targets will have the same log level.
+ */
+ fun custom(logLevel: LogLevel) = TracingFilterConfiguration(overrides = Target.values().associateWith { logLevel })
+
+ /**
+ * Use this method to override the log level of specific targets.
+ */
+ fun custom(overrides: Map) = TracingFilterConfiguration(overrides)
+}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/TracingConfiguration.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingService.kt
similarity index 60%
rename from libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/TracingConfiguration.kt
rename to libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingService.kt
index f32b18c9f2..4a74f83b20 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/TracingConfiguration.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingService.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,13 +14,11 @@
* limitations under the License.
*/
-package io.element.android.libraries.matrix.impl.tracing
+package io.element.android.libraries.matrix.api.tracing
-import io.element.android.libraries.matrix.api.tracing.TracingConfiguration
import timber.log.Timber
-fun setupTracing(tracingConfiguration: TracingConfiguration) {
- val filter = tracingConfiguration.filter
- Timber.v("Tracing config filter = $filter")
- org.matrix.rustcomponents.sdk.setupTracing(filter)
+interface TracingService {
+ fun setupTracing(tracingConfiguration: TracingConfiguration)
+ fun createTimberTree(): Timber.Tree
}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/WriteToFilesConfiguration.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/WriteToFilesConfiguration.kt
new file mode 100644
index 0000000000..cafa375a6a
--- /dev/null
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/WriteToFilesConfiguration.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.matrix.api.tracing
+
+sealed class WriteToFilesConfiguration {
+ object Disabled : WriteToFilesConfiguration()
+ data class Enabled(val directory: String, val filenamePrefix: String) : WriteToFilesConfiguration()
+}
diff --git a/libraries/matrix/impl/build.gradle.kts b/libraries/matrix/impl/build.gradle.kts
index 7786a3ee3f..e29d4b73db 100644
--- a/libraries/matrix/impl/build.gradle.kts
+++ b/libraries/matrix/impl/build.gradle.kts
@@ -17,7 +17,7 @@
plugins {
id("io.element.android-library")
alias(libs.plugins.anvil)
- kotlin("plugin.serialization") version "1.8.22"
+ kotlin("plugin.serialization") version "1.9.0"
}
android {
@@ -29,7 +29,7 @@ anvil {
}
dependencies {
- // api(projects.libraries.rustsdk)
+ // implementation(projects.libraries.rustsdk)
implementation(libs.matrix.sdk)
implementation(projects.libraries.di)
implementation(projects.libraries.androidutils)
@@ -45,4 +45,5 @@ dependencies {
testImplementation(libs.test.junit)
testImplementation(libs.test.truth)
testImplementation(projects.libraries.matrix.test)
+ testImplementation(libs.coroutines.test)
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
index 640e0772a9..8f5cfa496b 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
@@ -32,8 +32,8 @@ import io.element.android.libraries.matrix.api.notification.NotificationService
import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
-import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource
-import io.element.android.libraries.matrix.api.room.awaitAllRoomsAreLoaded
+import io.element.android.libraries.matrix.api.roomlist.RoomListService
+import io.element.android.libraries.matrix.api.roomlist.awaitLoaded
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
@@ -45,9 +45,8 @@ import io.element.android.libraries.matrix.impl.notification.RustNotificationSer
import io.element.android.libraries.matrix.impl.pushers.RustPushersService
import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
import io.element.android.libraries.matrix.impl.room.RustMatrixRoom
-import io.element.android.libraries.matrix.impl.room.RustRoomSummaryDataSource
-import io.element.android.libraries.matrix.impl.room.roomOrNull
-import io.element.android.libraries.matrix.impl.room.stateFlow
+import io.element.android.libraries.matrix.impl.roomlist.RustRoomListService
+import io.element.android.libraries.matrix.impl.roomlist.roomOrNull
import io.element.android.libraries.matrix.impl.sync.RustSyncService
import io.element.android.libraries.matrix.impl.usersearch.UserProfileMapper
import io.element.android.libraries.matrix.impl.usersearch.UserSearchResultMapper
@@ -90,20 +89,20 @@ class RustMatrixClient constructor(
) : MatrixClient {
override val sessionId: UserId = UserId(client.userId())
- private val roomListService = syncService.roomListService()
+ private val innerRoomListService = syncService.roomListService()
private val sessionDispatcher = dispatchers.io.limitedParallelism(64)
private val sessionCoroutineScope = appCoroutineScope.childScope(dispatchers.main, "Session-${sessionId}")
private val verificationService = RustSessionVerificationService()
- private val rustSyncService = RustSyncService(syncService, roomListService.stateFlow(), sessionCoroutineScope)
+ private val rustSyncService = RustSyncService(syncService, sessionCoroutineScope)
private val pushersService = RustPushersService(
client = client,
dispatchers = dispatchers,
)
private val notificationClient = client.notificationClient().use { builder ->
- builder.finish()
+ builder.filterByPushRules().finish()
}
- private val notificationService = RustNotificationService(notificationClient)
+ private val notificationService = RustNotificationService(sessionId, notificationClient, dispatchers, clock)
private val isLoggingOut = AtomicBoolean(false)
@@ -122,15 +121,15 @@ class RustMatrixClient constructor(
}
}
- private val rustRoomSummaryDataSource: RustRoomSummaryDataSource =
- RustRoomSummaryDataSource(
- roomListService = roomListService,
+ private val rustRoomListService: RoomListService =
+ RustRoomListService(
+ innerRoomListService = innerRoomListService,
sessionCoroutineScope = sessionCoroutineScope,
dispatcher = sessionDispatcher,
)
- override val roomSummaryDataSource: RoomSummaryDataSource
- get() = rustRoomSummaryDataSource
+ override val roomListService: RoomListService
+ get() = rustRoomListService
private val rustMediaLoader = RustMediaLoader(baseCacheDirectory, dispatchers, client)
override val mediaLoader: MatrixMediaLoader
@@ -138,7 +137,7 @@ class RustMatrixClient constructor(
private val roomMembershipObserver = RoomMembershipObserver()
- private val roomContentForwarder = RoomContentForwarder(roomListService)
+ private val roomContentForwarder = RoomContentForwarder(innerRoomListService)
init {
client.setDelegate(clientDelegate)
@@ -147,35 +146,36 @@ class RustMatrixClient constructor(
if (syncState == SyncState.Running) {
onSlidingSyncUpdate()
}
- }.launchIn(sessionCoroutineScope)
+ }
+ .launchIn(sessionCoroutineScope)
}
- override suspend fun getRoom(roomId: RoomId): MatrixRoom? {
+ override suspend fun getRoom(roomId: RoomId): MatrixRoom? = withContext(sessionDispatcher) {
// Check if already in memory...
var cachedPairOfRoom = pairOfRoom(roomId)
if (cachedPairOfRoom == null) {
//... otherwise, lets wait for the SS to load all rooms and check again.
- roomSummaryDataSource.awaitAllRoomsAreLoaded()
+ roomListService.allRooms().awaitLoaded()
cachedPairOfRoom = pairOfRoom(roomId)
}
- if (cachedPairOfRoom == null) return null
- val (roomListItem, fullRoom) = cachedPairOfRoom
- return RustMatrixRoom(
- sessionId = sessionId,
- roomListItem = roomListItem,
- innerRoom = fullRoom,
- sessionCoroutineScope = sessionCoroutineScope,
- coroutineDispatchers = dispatchers,
- systemClock = clock,
- roomContentForwarder = roomContentForwarder,
- sessionData = sessionStore.getSession(sessionId.value)!!,
- )
+ cachedPairOfRoom?.let { (roomListItem, fullRoom) ->
+ RustMatrixRoom(
+ sessionId = sessionId,
+ roomListItem = roomListItem,
+ innerRoom = fullRoom,
+ sessionCoroutineScope = sessionCoroutineScope,
+ coroutineDispatchers = dispatchers,
+ systemClock = clock,
+ roomContentForwarder = roomContentForwarder,
+ sessionData = sessionStore.getSession(sessionId.value)!!,
+ )
+ }
}
- private suspend fun pairOfRoom(roomId: RoomId): Pair? = withContext(sessionDispatcher) {
- val cachedRoomListItem = roomListService.roomOrNull(roomId.value)
+ private fun pairOfRoom(roomId: RoomId): Pair? {
+ val cachedRoomListItem = innerRoomListService.roomOrNull(roomId.value)
val fullRoom = cachedRoomListItem?.fullRoom()
- if (cachedRoomListItem == null || fullRoom == null) {
+ return if (cachedRoomListItem == null || fullRoom == null) {
Timber.d("No room cached for $roomId")
null
} else {
@@ -224,10 +224,11 @@ class RustMatrixClient constructor(
// Wait to receive the room back from the sync
withTimeout(30_000L) {
- roomSummaryDataSource.allRooms()
+ roomListService.allRooms().summaries
.filter { roomSummaries ->
roomSummaries.map { it.identifier() }.contains(roomId.value)
- }.first()
+ }
+ .first()
}
roomId
}
@@ -271,7 +272,7 @@ class RustMatrixClient constructor(
client.setDelegate(null)
verificationService.destroy()
syncService.destroy()
- roomListService.destroy()
+ innerRoomListService.destroy()
notificationClient.destroy()
client.destroy()
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt
new file mode 100644
index 0000000000..931133c266
--- /dev/null
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.matrix.impl
+
+import android.content.Context
+import io.element.android.libraries.core.coroutine.CoroutineDispatchers
+import io.element.android.libraries.di.ApplicationContext
+import io.element.android.libraries.network.useragent.UserAgentProvider
+import io.element.android.libraries.sessionstorage.api.SessionData
+import io.element.android.libraries.sessionstorage.api.SessionStore
+import io.element.android.services.toolbox.api.systemclock.SystemClock
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.withContext
+import org.matrix.rustcomponents.sdk.ClientBuilder
+import org.matrix.rustcomponents.sdk.Session
+import org.matrix.rustcomponents.sdk.use
+import java.io.File
+import javax.inject.Inject
+
+class RustMatrixClientFactory @Inject constructor(
+ @ApplicationContext private val context: Context,
+ private val baseDirectory: File,
+ private val appCoroutineScope: CoroutineScope,
+ private val coroutineDispatchers: CoroutineDispatchers,
+ private val sessionStore: SessionStore,
+ private val userAgentProvider: UserAgentProvider,
+ private val clock: SystemClock,
+) {
+
+ suspend fun create(sessionData: SessionData): RustMatrixClient = withContext(coroutineDispatchers.io) {
+ val client = ClientBuilder()
+ .basePath(baseDirectory.absolutePath)
+ .homeserverUrl(sessionData.homeserverUrl)
+ .username(sessionData.userId)
+ .userAgent(userAgentProvider.provide())
+ // FIXME Quick and dirty fix for stopping version requests on startup https://github.com/matrix-org/matrix-rust-sdk/pull/1376
+ .serverVersions(listOf("v1.0", "v1.1", "v1.2", "v1.3", "v1.4", "v1.5"))
+ .use { it.build() }
+
+ client.restoreSession(sessionData.toSession())
+
+ val syncService = client.syncService().finish()
+
+ RustMatrixClient(
+ client = client,
+ syncService = syncService,
+ sessionStore = sessionStore,
+ appCoroutineScope = appCoroutineScope,
+ dispatchers = coroutineDispatchers,
+ baseDirectory = baseDirectory,
+ baseCacheDirectory = context.cacheDir,
+ clock = clock,
+ )
+ }
+}
+
+private fun SessionData.toSession() = Session(
+ accessToken = accessToken,
+ refreshToken = refreshToken,
+ userId = userId,
+ deviceId = deviceId,
+ homeserverUrl = homeserverUrl,
+ slidingSyncProxy = slidingSyncProxy,
+)
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt
index 0bc299d020..ba06891013 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt
@@ -16,33 +16,27 @@
package io.element.android.libraries.matrix.impl.auth
-import android.content.Context
+// TODO Oidc
+// import org.matrix.rustcomponents.sdk.OidcAuthenticationUrl
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.extensions.mapFailure
import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
import io.element.android.libraries.matrix.api.auth.OidcDetails
import io.element.android.libraries.matrix.api.core.SessionId
-import io.element.android.libraries.matrix.impl.RustMatrixClient
+import io.element.android.libraries.matrix.impl.RustMatrixClientFactory
import io.element.android.libraries.matrix.impl.exception.mapClientException
import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.libraries.sessionstorage.api.SessionData
import io.element.android.libraries.sessionstorage.api.SessionStore
-import io.element.android.services.toolbox.api.systemclock.SystemClock
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext
-import org.matrix.rustcomponents.sdk.Client
-import org.matrix.rustcomponents.sdk.ClientBuilder
-// TODO Oidc
-// import org.matrix.rustcomponents.sdk.OidcAuthenticationUrl
import org.matrix.rustcomponents.sdk.Session
import org.matrix.rustcomponents.sdk.use
import java.io.File
@@ -53,13 +47,11 @@ import org.matrix.rustcomponents.sdk.AuthenticationService as RustAuthentication
@ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)
class RustMatrixAuthenticationService @Inject constructor(
- @ApplicationContext private val context: Context,
- private val baseDirectory: File,
- private val appCoroutineScope: CoroutineScope,
+ baseDirectory: File,
private val coroutineDispatchers: CoroutineDispatchers,
private val sessionStore: SessionStore,
- private val clock: SystemClock,
- private val userAgentProvider: UserAgentProvider,
+ userAgentProvider: UserAgentProvider,
+ private val rustMatrixClientFactory: RustMatrixClientFactory,
) : MatrixAuthenticationService {
private val authService: RustAuthenticationService = RustAuthenticationService(
@@ -84,16 +76,9 @@ class RustMatrixAuthenticationService @Inject constructor(
runCatching {
val sessionData = sessionStore.getSession(sessionId.value)
if (sessionData != null) {
- val client = ClientBuilder()
- .basePath(baseDirectory.absolutePath)
- .homeserverUrl(sessionData.homeserverUrl)
- .username(sessionData.userId)
- .userAgent(userAgentProvider.provide())
- .use { it.build() }
- client.restoreSession(sessionData.toSession())
- createMatrixClient(client)
+ rustMatrixClientFactory.create(sessionData)
} else {
- throw IllegalStateException("No session to restore with id $sessionId")
+ error("No session to restore with id $sessionId")
}
}.mapFailure { failure ->
failure.mapClientException()
@@ -181,30 +166,8 @@ class RustMatrixAuthenticationService @Inject constructor(
*/
}
- private suspend fun createMatrixClient(client: Client): MatrixClient {
- val syncService = client.syncService().finish()
- return RustMatrixClient(
- client = client,
- syncService = syncService,
- sessionStore = sessionStore,
- appCoroutineScope = appCoroutineScope,
- dispatchers = coroutineDispatchers,
- baseDirectory = baseDirectory,
- baseCacheDirectory = context.cacheDir,
- clock = clock,
- )
- }
}
-private fun SessionData.toSession() = Session(
- accessToken = accessToken,
- refreshToken = refreshToken,
- userId = userId,
- deviceId = deviceId,
- homeserverUrl = homeserverUrl,
- slidingSyncProxy = slidingSyncProxy,
-)
-
private fun Session.toSessionData() = SessionData(
userId = userId,
deviceId = deviceId,
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt
index bf260be6ec..dba1dbd0a3 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt
@@ -23,7 +23,7 @@ import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
-import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource
+import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
@Module
@@ -40,8 +40,8 @@ object SessionMatrixModule {
}
@Provides
- fun provideRoomSummaryDataSource(matrixClient: MatrixClient): RoomSummaryDataSource {
- return matrixClient.roomSummaryDataSource
+ fun providesRoomListService(matrixClient: MatrixClient): RoomListService {
+ return matrixClient.roomListService
}
@Provides
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaUploadHandlerImpl.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaUploadHandlerImpl.kt
new file mode 100644
index 0000000000..639f9149c4
--- /dev/null
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaUploadHandlerImpl.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.matrix.impl.media
+
+import io.element.android.libraries.androidutils.file.safeDelete
+import io.element.android.libraries.matrix.api.media.MediaUploadHandler
+import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle
+import java.io.File
+
+class MediaUploadHandlerImpl(
+ private val filesToUpload: List,
+ private val sendAttachmentJoinHandle: SendAttachmentJoinHandle,
+) : MediaUploadHandler {
+ override suspend fun await(): Result =
+ runCatching {
+ sendAttachmentJoinHandle.join()
+ }
+ .also { cleanUpFiles() }
+
+ override fun cancel() {
+ sendAttachmentJoinHandle.cancel()
+ cleanUpFiles()
+ }
+
+ private fun cleanUpFiles() {
+ filesToUpload.forEach { file -> file.safeDelete() }
+ }
+}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt
index 07acb7fec5..0d9f794173 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt
@@ -19,19 +19,29 @@ package io.element.android.libraries.matrix.impl.notification
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
-import io.element.android.libraries.matrix.api.core.UserId
+import io.element.android.libraries.matrix.api.core.SessionId
+import io.element.android.libraries.matrix.api.notification.NotificationContent
import io.element.android.libraries.matrix.api.notification.NotificationData
+import io.element.android.libraries.matrix.api.room.RoomMembershipState
+import io.element.android.services.toolbox.api.systemclock.SystemClock
+import org.matrix.rustcomponents.sdk.NotificationEvent
import org.matrix.rustcomponents.sdk.NotificationItem
import org.matrix.rustcomponents.sdk.use
-class NotificationMapper {
- private val timelineEventMapper = TimelineEventMapper()
+class NotificationMapper(
+ sessionId: SessionId,
+ private val clock: SystemClock,
+) {
+ private val notificationContentMapper = NotificationContentMapper(sessionId)
- fun map(roomId: RoomId, notificationItem: NotificationItem): NotificationData {
+ fun map(
+ eventId: EventId,
+ roomId: RoomId,
+ notificationItem: NotificationItem
+ ): NotificationData {
return notificationItem.use { item ->
NotificationData(
- senderId = UserId(item.event.senderId()),
- eventId = EventId(item.event.eventId()),
+ eventId = eventId,
roomId = roomId,
senderAvatarUrl = item.senderInfo.avatarUrl,
senderDisplayName = item.senderInfo.displayName,
@@ -39,9 +49,28 @@ class NotificationMapper {
roomDisplayName = item.roomInfo.displayName,
isDirect = item.roomInfo.isDirect,
isEncrypted = item.roomInfo.isEncrypted.orFalse(),
- isNoisy = item.isNoisy,
- event = item.event.use { event -> timelineEventMapper.map(event) }
+ isNoisy = item.isNoisy.orFalse(),
+ timestamp = item.timestamp() ?: clock.epochMillis(),
+ content = item.event.use { notificationContentMapper.map(it) },
+ contentUrl = null,
)
}
}
}
+
+class NotificationContentMapper(private val sessionId: SessionId) {
+ private val timelineEventToNotificationContentMapper = TimelineEventToNotificationContentMapper()
+
+ fun map(notificationEvent: NotificationEvent): NotificationContent =
+ when (notificationEvent) {
+ is NotificationEvent.Timeline -> timelineEventToNotificationContentMapper.map(notificationEvent.event)
+ is NotificationEvent.Invite -> NotificationContent.StateEvent.RoomMemberContent(
+ userId = sessionId.value,
+ membershipState = RoomMembershipState.INVITE,
+ )
+ }
+}
+
+private fun NotificationItem.timestamp(): Long? {
+ return (this.event as? NotificationEvent.Timeline)?.event?.timestamp()?.toLong()
+}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt
index 92c996049e..0f3aebd049 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt
@@ -16,29 +16,34 @@
package io.element.android.libraries.matrix.impl.notification
+import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.notification.NotificationData
import io.element.android.libraries.matrix.api.notification.NotificationService
+import io.element.android.services.toolbox.api.systemclock.SystemClock
+import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.NotificationClient
import org.matrix.rustcomponents.sdk.use
class RustNotificationService(
+ sessionId: SessionId,
private val notificationClient: NotificationClient,
+ private val dispatchers: CoroutineDispatchers,
+ clock: SystemClock,
) : NotificationService {
- private val notificationMapper: NotificationMapper = NotificationMapper()
+ private val notificationMapper: NotificationMapper = NotificationMapper(sessionId, clock)
- override fun getNotification(
+ override suspend fun getNotification(
userId: SessionId,
roomId: RoomId,
eventId: EventId,
- filterByPushRules: Boolean,
- ): Result {
- return runCatching {
+ ): Result = withContext(dispatchers.io) {
+ runCatching {
val item = notificationClient.getNotification(roomId.value, eventId.value)
item?.use {
- notificationMapper.map(roomId, it)
+ notificationMapper.map(eventId, roomId, it)
}
}
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt
similarity index 85%
rename from libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventMapper.kt
rename to libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt
index f7d4a00188..e30e57113d 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventMapper.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt
@@ -16,8 +16,8 @@
package io.element.android.libraries.matrix.impl.notification
+import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.NotificationContent
-import io.element.android.libraries.matrix.api.notification.NotificationEvent
import io.element.android.libraries.matrix.impl.room.RoomMemberMapper
import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessageMapper
import org.matrix.rustcomponents.sdk.MessageLikeEventContent
@@ -27,22 +27,20 @@ import org.matrix.rustcomponents.sdk.TimelineEventType
import org.matrix.rustcomponents.sdk.use
import javax.inject.Inject
-class TimelineEventMapper @Inject constructor() {
+class TimelineEventToNotificationContentMapper @Inject constructor() {
- fun map(timelineEvent: TimelineEvent): NotificationEvent {
+ fun map(timelineEvent: TimelineEvent): NotificationContent {
return timelineEvent.use {
- NotificationEvent(
- timestamp = it.timestamp().toLong(),
- content = it.eventType().toContent(),
- contentUrl = null // TODO it.eventType().toContentUrl(),
- )
+ timelineEvent.eventType().use { eventType ->
+ eventType.toContent(senderId = UserId(timelineEvent.senderId()))
+ }
}
}
}
-private fun TimelineEventType.toContent(): NotificationContent {
+private fun TimelineEventType.toContent(senderId: UserId): NotificationContent {
return when (this) {
- is TimelineEventType.MessageLike -> content.toContent()
+ is TimelineEventType.MessageLike -> content.toContent(senderId)
is TimelineEventType.State -> content.toContent()
}
}
@@ -75,9 +73,9 @@ private fun StateEventContent.toContent(): NotificationContent.StateEvent {
}
}
-private fun MessageLikeEventContent.toContent(): NotificationContent.MessageLike {
+private fun MessageLikeEventContent.toContent(senderId: UserId): NotificationContent.MessageLike {
return use {
- when (it) {
+ when (this) {
MessageLikeEventContent.CallAnswer -> NotificationContent.MessageLike.CallAnswer
MessageLikeEventContent.CallCandidates -> NotificationContent.MessageLike.CallCandidates
MessageLikeEventContent.CallHangup -> NotificationContent.MessageLike.CallHangup
@@ -89,10 +87,10 @@ private fun MessageLikeEventContent.toContent(): NotificationContent.MessageLike
MessageLikeEventContent.KeyVerificationMac -> NotificationContent.MessageLike.KeyVerificationMac
MessageLikeEventContent.KeyVerificationReady -> NotificationContent.MessageLike.KeyVerificationReady
MessageLikeEventContent.KeyVerificationStart -> NotificationContent.MessageLike.KeyVerificationStart
- is MessageLikeEventContent.ReactionContent -> NotificationContent.MessageLike.ReactionContent(it.relatedEventId)
+ is MessageLikeEventContent.ReactionContent -> NotificationContent.MessageLike.ReactionContent(relatedEventId)
MessageLikeEventContent.RoomEncrypted -> NotificationContent.MessageLike.RoomEncrypted
is MessageLikeEventContent.RoomMessage -> {
- NotificationContent.MessageLike.RoomMessage(EventMessageMapper().mapMessageType(it.messageType))
+ NotificationContent.MessageLike.RoomMessage(senderId, EventMessageMapper().mapMessageType(messageType))
}
MessageLikeEventContent.RoomRedaction -> NotificationContent.MessageLike.RoomRedaction
MessageLikeEventContent.Sticker -> NotificationContent.MessageLike.Sticker
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/poll/PollAnswer.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/poll/PollAnswer.kt
new file mode 100644
index 0000000000..c3098bdcb0
--- /dev/null
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/poll/PollAnswer.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.matrix.impl.poll
+
+import io.element.android.libraries.matrix.api.poll.PollAnswer
+import org.matrix.rustcomponents.sdk.PollAnswer as RustPollAnswer
+
+fun RustPollAnswer.map(): PollAnswer = PollAnswer(
+ id = id,
+ text = text,
+)
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/poll/PollKind.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/poll/PollKind.kt
new file mode 100644
index 0000000000..bde49464ad
--- /dev/null
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/poll/PollKind.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.matrix.impl.poll
+
+import io.element.android.libraries.matrix.api.poll.PollKind
+import org.matrix.rustcomponents.sdk.PollKind as RustPollKind
+
+fun RustPollKind.map(): PollKind = when (this) {
+ RustPollKind.DISCLOSED -> PollKind.Disclosed
+ RustPollKind.UNDISCLOSED -> PollKind.Undisclosed
+}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt
index 4e2d63d091..8ee0361ace 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt
@@ -20,6 +20,7 @@ import io.element.android.libraries.core.coroutine.parallelMap
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.ForwardEventException
+import io.element.android.libraries.matrix.impl.roomlist.roomOrNull
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.withTimeout
import org.matrix.rustcomponents.sdk.Room
@@ -80,6 +81,6 @@ class RoomContentForwarder(
}
private object NoOpTimelineListener : TimelineListener {
- override fun onUpdate(diff: TimelineDiff) = Unit
+ override fun onUpdate(diff: List) = Unit
}
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt
index 89e83b58c6..373880568f 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt
@@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.impl.room
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.childScope
+import io.element.android.libraries.core.coroutine.parallelMap
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomId
@@ -27,6 +28,7 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.media.AudioInfo
import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ImageInfo
+import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
@@ -37,6 +39,7 @@ import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
import io.element.android.libraries.matrix.impl.core.toProgressWatcher
+import io.element.android.libraries.matrix.impl.media.MediaUploadHandlerImpl
import io.element.android.libraries.matrix.impl.media.map
import io.element.android.libraries.matrix.impl.room.location.toInner
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
@@ -139,7 +142,7 @@ class RustMatrixRoom(
override val avatarUrl: String?
get() {
- return innerRoom.avatarUrl()
+ return roomListItem.avatarUrl() ?: innerRoom.avatarUrl()
}
override val isEncrypted: Boolean
@@ -168,7 +171,7 @@ class RustMatrixRoom(
val currentMembers = currentState.roomMembers()
_membersStateFlow.value = MatrixRoomMembersState.Pending(prevRoomMembers = currentMembers)
runCatching {
- innerRoom.members().map(RoomMemberMapper::map)
+ innerRoom.members().parallelMap(RoomMemberMapper::map)
}.map {
_membersStateFlow.value = MatrixRoomMembersState.Ready(it)
}.onFailure {
@@ -249,6 +252,12 @@ class RustMatrixRoom(
}
}
+ override suspend fun canUserRedact(userId: UserId): Result {
+ return runCatching {
+ innerRoom.canUserRedact(userId.value)
+ }
+ }
+
override suspend fun canUserSendState(userId: UserId, type: StateEventType): Result {
return runCatching {
innerRoom.canUserSendState(userId.value, type.map())
@@ -261,26 +270,26 @@ class RustMatrixRoom(
}
}
- override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo, progressCallback: ProgressCallback?): Result {
- return sendAttachment {
+ override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo, progressCallback: ProgressCallback?): Result {
+ return sendAttachment(listOf(file, thumbnailFile)) {
innerRoom.sendImage(file.path, thumbnailFile.path, imageInfo.map(), progressCallback?.toProgressWatcher())
}
}
- override suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo, progressCallback: ProgressCallback?): Result {
- return sendAttachment {
+ override suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo, progressCallback: ProgressCallback?): Result {
+ return sendAttachment(listOf(file, thumbnailFile)) {
innerRoom.sendVideo(file.path, thumbnailFile.path, videoInfo.map(), progressCallback?.toProgressWatcher())
}
}
- override suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result {
- return sendAttachment {
+ override suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result {
+ return sendAttachment(listOf(file)) {
innerRoom.sendAudio(file.path, audioInfo.map(), progressCallback?.toProgressWatcher())
}
}
- override suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result {
- return sendAttachment {
+ override suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result {
+ return sendAttachment(listOf(file)) {
innerRoom.sendFile(file.path, fileInfo.map(), progressCallback?.toProgressWatcher())
}
}
@@ -363,13 +372,10 @@ class RustMatrixRoom(
)
}
}
-}
-//TODO handle cancellation, need refactoring of how we are catching errors
-private suspend fun sendAttachment(handle: () -> SendAttachmentJoinHandle): Result {
- return runCatching {
- handle().use {
- it.join()
+ private suspend fun sendAttachment(files: List, handle: () -> SendAttachmentJoinHandle): Result {
+ return runCatching {
+ MediaUploadHandlerImpl(files, handle())
}
}
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomSummaryDataSource.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomSummaryDataSource.kt
deleted file mode 100644
index efdbcf34ad..0000000000
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomSummaryDataSource.kt
+++ /dev/null
@@ -1,122 +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.matrix.impl.room
-
-import io.element.android.libraries.matrix.api.room.RoomSummary
-import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.launch
-import org.matrix.rustcomponents.sdk.RoomList
-import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
-import org.matrix.rustcomponents.sdk.RoomListException
-import org.matrix.rustcomponents.sdk.RoomListInput
-import org.matrix.rustcomponents.sdk.RoomListLoadingState
-import org.matrix.rustcomponents.sdk.RoomListRange
-import org.matrix.rustcomponents.sdk.RoomListService
-import org.matrix.rustcomponents.sdk.RoomListServiceState
-import timber.log.Timber
-
-internal class RustRoomSummaryDataSource(
- private val roomListService: RoomListService,
- private val sessionCoroutineScope: CoroutineScope,
- dispatcher: CoroutineDispatcher,
- roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
-) : RoomSummaryDataSource {
-
- private val allRooms = MutableStateFlow>(emptyList())
- private val inviteRooms = MutableStateFlow>(emptyList())
-
- private val allRoomsLoadingState: MutableStateFlow = MutableStateFlow(RoomSummaryDataSource.LoadingState.NotLoaded)
- private val allRoomsListProcessor = RoomSummaryListProcessor(allRooms, roomListService, roomSummaryDetailsFactory, shouldFetchFullRoom = false)
- private val inviteRoomsListProcessor = RoomSummaryListProcessor(inviteRooms, roomListService, roomSummaryDetailsFactory, shouldFetchFullRoom = true)
-
- init {
- sessionCoroutineScope.launch(dispatcher) {
- val allRooms = roomListService.allRooms()
- allRooms
- .observeEntriesWithProcessor(allRoomsListProcessor)
- .launchIn(this)
-
- allRooms
- .loadingStateFlow()
- .map { it.toRoomSummaryDataSourceLoadingState() }
- .onEach {
- allRoomsLoadingState.value = it
- }.launchIn(this)
-
- launch {
- // Wait until running, as invites is only available after that
- roomListService.stateFlow().first {
- it == RoomListServiceState.RUNNING
- }
- roomListService.invites()
- .observeEntriesWithProcessor(inviteRoomsListProcessor)
- .launchIn(this)
- }
- }
- }
-
- override fun allRooms(): StateFlow> {
- return allRooms
- }
-
- override fun inviteRooms(): StateFlow> {
- return inviteRooms
- }
-
- override fun allRoomsLoadingState(): StateFlow {
- return allRoomsLoadingState
- }
-
- override fun updateAllRoomsVisibleRange(range: IntRange) {
- Timber.v("setVisibleRange=$range")
- sessionCoroutineScope.launch {
- try {
- val ranges = listOf(RoomListRange(range.first.toUInt(), range.last.toUInt()))
- roomListService.applyInput(
- RoomListInput.Viewport(ranges)
- )
- } catch (exception: RoomListException) {
- Timber.e(exception, "Failed updating visible range")
- }
- }
- }
-}
-
-private fun RoomListLoadingState.toRoomSummaryDataSourceLoadingState(): RoomSummaryDataSource.LoadingState {
- return when (this) {
- is RoomListLoadingState.Loaded -> RoomSummaryDataSource.LoadingState.Loaded(maximumNumberOfRooms?.toInt() ?: 0)
- is RoomListLoadingState.NotLoaded -> RoomSummaryDataSource.LoadingState.NotLoaded
- }
-}
-
-private fun RoomList.observeEntriesWithProcessor(processor: RoomSummaryListProcessor): Flow {
- return entriesFlow { roomListEntries ->
- processor.postEntries(roomListEntries)
- }.onEach { update ->
- processor.postUpdate(update)
- }
-}
-
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomListExtensions.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt
similarity index 76%
rename from libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomListExtensions.kt
rename to libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt
index 84c4eeaefb..8d96990a9e 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomListExtensions.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt
@@ -14,18 +14,19 @@
* limitations under the License.
*/
-package io.element.android.libraries.matrix.impl.room
+package io.element.android.libraries.matrix.impl.roomlist
+import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.buffer
+import kotlinx.coroutines.flow.catch
import org.matrix.rustcomponents.sdk.RoomList
import org.matrix.rustcomponents.sdk.RoomListEntriesListener
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
import org.matrix.rustcomponents.sdk.RoomListEntry
-import org.matrix.rustcomponents.sdk.RoomListException
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.RoomListLoadingState
import org.matrix.rustcomponents.sdk.RoomListLoadingStateListener
@@ -42,31 +43,34 @@ fun RoomList.loadingStateFlow(): Flow =
}
}
val result = loadingState(listener)
- send(result.state)
+ try {
+ send(result.state)
+ } catch (exception: Exception) {
+ Timber.d("loadingStateFlow() initialState failed.")
+ }
result.stateStream
+ }.catch {
+ Timber.d(it, "loadingStateFlow() failed")
}.buffer(Channel.UNLIMITED)
-fun RoomList.entriesFlow(onInitialList: suspend (List) -> Unit): Flow =
+fun RoomList.entriesFlow(onInitialList: suspend (List) -> Unit): Flow> =
mxCallbackFlow {
val listener = object : RoomListEntriesListener {
- override fun onUpdate(roomEntriesUpdate: RoomListEntriesUpdate) {
+ override fun onUpdate(roomEntriesUpdate: List) {
trySendBlocking(roomEntriesUpdate)
}
}
val result = entries(listener)
- onInitialList(result.entries)
+ try {
+ onInitialList(result.entries)
+ } catch (exception: Exception) {
+ Timber.d("entriesFlow() onInitialList failed.")
+ }
result.entriesStream
+ }.catch {
+ Timber.d(it, "entriesFlow() failed")
}.buffer(Channel.UNLIMITED)
-fun RoomListService.roomOrNull(roomId: String): RoomListItem? {
- return try {
- room(roomId)
- } catch (exception: RoomListException) {
- Timber.d(exception, "Failed finding room with id=$roomId.")
- return null
- }
-}
-
fun RoomListService.stateFlow(): Flow =
mxCallbackFlow {
val listener = object : RoomListServiceStateListener {
@@ -74,5 +78,16 @@ fun RoomListService.stateFlow(): Flow =
trySendBlocking(state)
}
}
- state(listener)
+ tryOrNull {
+ state(listener)
+ }
}.buffer(Channel.UNLIMITED)
+
+fun RoomListService.roomOrNull(roomId: String): RoomListItem? {
+ return try {
+ room(roomId)
+ } catch (exception: Exception) {
+ Timber.d(exception, "Failed finding room with id=$roomId.")
+ return null
+ }
+}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomSummaryDetailsFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryDetailsFactory.kt
similarity index 89%
rename from libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomSummaryDetailsFactory.kt
rename to libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryDetailsFactory.kt
index 7dd7bf4581..b57eb892e0 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomSummaryDetailsFactory.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryDetailsFactory.kt
@@ -14,10 +14,11 @@
* limitations under the License.
*/
-package io.element.android.libraries.matrix.impl.room
+package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.matrix.api.core.RoomId
-import io.element.android.libraries.matrix.api.room.RoomSummaryDetails
+import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
+import io.element.android.libraries.matrix.impl.room.RoomMemberMapper
import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListItem
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomSummaryListProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt
similarity index 93%
rename from libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomSummaryListProcessor.kt
rename to libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt
index a8ab4cb807..9a67ff1f30 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomSummaryListProcessor.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt
@@ -14,10 +14,10 @@
* limitations under the License.
*/
-package io.element.android.libraries.matrix.impl.room
+package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.core.coroutine.parallelMap
-import io.element.android.libraries.matrix.api.room.RoomSummary
+import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.sync.Mutex
@@ -50,12 +50,14 @@ class RoomSummaryListProcessor(
initLatch.complete(Unit)
}
- suspend fun postUpdate(update: RoomListEntriesUpdate) {
+ suspend fun postUpdate(updates: List) {
// Makes sure to process first entries before update.
initLatch.await()
updateRoomSummaries {
- Timber.v("Update rooms from postUpdate ($update) on ${Thread.currentThread()}")
- applyUpdate(update)
+ Timber.v("Update rooms from postUpdates (with ${updates.size} items) on ${Thread.currentThread()}")
+ updates.forEach { update ->
+ applyUpdate(update)
+ }
}
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomList.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomList.kt
new file mode 100644
index 0000000000..481b38dd9b
--- /dev/null
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomList.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.matrix.impl.roomlist
+
+import io.element.android.libraries.matrix.api.roomlist.RoomList
+import io.element.android.libraries.matrix.api.roomlist.RoomSummary
+import kotlinx.coroutines.flow.StateFlow
+
+/**
+ * Simple implementation of [RoomList] where state flows are provided through constructor.
+ */
+class RustRoomList(
+ override val summaries: StateFlow>,
+ override val loadingState: StateFlow
+) : RoomList
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt
new file mode 100644
index 0000000000..bf66bade3b
--- /dev/null
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt
@@ -0,0 +1,151 @@
+/*
+ * 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.matrix.impl.roomlist
+
+import io.element.android.libraries.matrix.api.roomlist.RoomList
+import io.element.android.libraries.matrix.api.roomlist.RoomListService
+import io.element.android.libraries.matrix.api.roomlist.RoomSummary
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
+import org.matrix.rustcomponents.sdk.RoomListException
+import org.matrix.rustcomponents.sdk.RoomListInput
+import org.matrix.rustcomponents.sdk.RoomListLoadingState
+import org.matrix.rustcomponents.sdk.RoomListRange
+import org.matrix.rustcomponents.sdk.RoomListServiceState
+import timber.log.Timber
+import org.matrix.rustcomponents.sdk.RoomListService as InnerRustRoomListService
+
+class RustRoomListService(
+ private val innerRoomListService: InnerRustRoomListService,
+ private val sessionCoroutineScope: CoroutineScope,
+ dispatcher: CoroutineDispatcher,
+ roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
+) : RoomListService {
+
+ private val allRooms = MutableStateFlow>(emptyList())
+ private val inviteRooms = MutableStateFlow>(emptyList())
+
+ private val allRoomsLoadingState: MutableStateFlow = MutableStateFlow(RoomList.LoadingState.NotLoaded)
+ private val allRoomsListProcessor = RoomSummaryListProcessor(allRooms, innerRoomListService, roomSummaryDetailsFactory, shouldFetchFullRoom = false)
+ private val invitesLoadingState: MutableStateFlow = MutableStateFlow(RoomList.LoadingState.NotLoaded)
+ private val inviteRoomsListProcessor = RoomSummaryListProcessor(inviteRooms, innerRoomListService, roomSummaryDetailsFactory, shouldFetchFullRoom = true)
+
+ init {
+ sessionCoroutineScope.launch(dispatcher) {
+ val allRooms = innerRoomListService.allRooms()
+ allRooms
+ .observeEntriesWithProcessor(allRoomsListProcessor)
+ .launchIn(this)
+ allRooms
+ .observeLoadingState(allRoomsLoadingState)
+ .launchIn(this)
+
+
+ launch {
+ // Wait until running, as invites is only available after that
+ innerRoomListService.stateFlow().first {
+ it == RoomListServiceState.RUNNING
+ }
+ val invites = innerRoomListService.invites()
+ invites
+ .observeEntriesWithProcessor(inviteRoomsListProcessor)
+ .launchIn(this)
+ invites
+ .observeLoadingState(invitesLoadingState)
+ .launchIn(this)
+
+ }
+ }
+ }
+
+ override fun allRooms(): RoomList {
+ return RustRoomList(allRooms, allRoomsLoadingState)
+ }
+
+ override fun invites(): RoomList {
+ return RustRoomList(inviteRooms, invitesLoadingState)
+ }
+
+ override fun updateAllRoomsVisibleRange(range: IntRange) {
+ Timber.v("setVisibleRange=$range")
+ sessionCoroutineScope.launch {
+ try {
+ val ranges = listOf(RoomListRange(range.first.toUInt(), range.last.toUInt()))
+ innerRoomListService.applyInput(
+ RoomListInput.Viewport(ranges)
+ )
+ } catch (exception: RoomListException) {
+ Timber.e(exception, "Failed updating visible range")
+ }
+ }
+ }
+
+ override val state: StateFlow =
+ innerRoomListService.stateFlow()
+ .map { it.toRoomListState() }
+ .onEach { state ->
+ Timber.d("RoomList state=$state")
+ }
+ .distinctUntilChanged()
+ .stateIn(sessionCoroutineScope, SharingStarted.Eagerly, RoomListService.State.Idle)
+}
+
+private fun RoomListLoadingState.toLoadingState(): RoomList.LoadingState {
+ return when (this) {
+ is RoomListLoadingState.Loaded -> RoomList.LoadingState.Loaded(maximumNumberOfRooms?.toInt() ?: 0)
+ RoomListLoadingState.NotLoaded -> RoomList.LoadingState.NotLoaded
+ }
+}
+
+private fun RoomListServiceState.toRoomListState(): RoomListService.State {
+ return when (this) {
+ RoomListServiceState.INIT,
+ RoomListServiceState.SETTING_UP -> RoomListService.State.Idle
+ RoomListServiceState.RUNNING -> RoomListService.State.Running
+ RoomListServiceState.ERROR -> RoomListService.State.Error
+ RoomListServiceState.TERMINATED -> RoomListService.State.Terminated
+ }
+}
+
+private fun org.matrix.rustcomponents.sdk.RoomList.observeEntriesWithProcessor(processor: RoomSummaryListProcessor): Flow> {
+ return entriesFlow { roomListEntries ->
+ processor.postEntries(roomListEntries)
+ }.onEach { update ->
+ processor.postUpdate(update)
+ }
+}
+
+private fun org.matrix.rustcomponents.sdk.RoomList.observeLoadingState(stateFlow: MutableStateFlow): Flow {
+ return loadingStateFlow()
+ .map { it.toLoadingState() }
+ .onEach {
+ stateFlow.value = it
+ }
+}
+
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/AppStateMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/AppStateMapper.kt
index 51228231f9..ae90f9fc44 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/AppStateMapper.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/AppStateMapper.kt
@@ -17,19 +17,8 @@
package io.element.android.libraries.matrix.impl.sync
import io.element.android.libraries.matrix.api.sync.SyncState
-import org.matrix.rustcomponents.sdk.RoomListServiceState
import org.matrix.rustcomponents.sdk.SyncServiceState
-internal fun RoomListServiceState.toSyncState(): SyncState {
- return when (this) {
- RoomListServiceState.INIT,
- RoomListServiceState.SETTING_UP -> SyncState.Idle
- RoomListServiceState.RUNNING -> SyncState.Running
- RoomListServiceState.ERROR -> SyncState.Error
- RoomListServiceState.TERMINATED -> SyncState.Terminated
- }
-}
-
internal fun SyncServiceState.toSyncState(): SyncState {
return when (this) {
SyncServiceState.IDLE -> SyncState.Idle
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/RustSyncService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/RustSyncService.kt
index ca40ca400c..932da42afb 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/RustSyncService.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/RustSyncService.kt
@@ -19,38 +19,40 @@ package io.element.android.libraries.matrix.impl.sync
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.sync.SyncState
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
-import org.matrix.rustcomponents.sdk.RoomListServiceState
import org.matrix.rustcomponents.sdk.SyncServiceInterface
+import org.matrix.rustcomponents.sdk.SyncServiceState
import timber.log.Timber
class RustSyncService(
private val innerSyncService: SyncServiceInterface,
- roomListStateFlow: Flow,
sessionCoroutineScope: CoroutineScope
) : SyncService {
override suspend fun startSync() = runCatching {
- Timber.v("Start sync")
+ Timber.i("Start sync")
innerSyncService.start()
+ }.onFailure {
+ Timber.d("Start sync failed: $it")
}
- override fun stopSync() = runCatching {
- Timber.v("Stop sync")
- innerSyncService.pause()
+ override suspend fun stopSync() = runCatching {
+ Timber.i("Stop sync")
+ innerSyncService.stop()
+ }.onFailure {
+ Timber.d("Stop sync failed: $it")
}
override val syncState: StateFlow =
- roomListStateFlow
- .map(RoomListServiceState::toSyncState)
+ innerSyncService.stateFlow()
+ .map(SyncServiceState::toSyncState)
.onEach { state ->
- Timber.v("Sync state=$state")
+ Timber.i("Sync state=$state")
}
.distinctUntilChanged()
.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, SyncState.Idle)
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/SyncServiceExtension.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/SyncServiceExtension.kt
index 36dabb71f3..c9e38ec7d4 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/SyncServiceExtension.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/SyncServiceExtension.kt
@@ -16,21 +16,24 @@
package io.element.android.libraries.matrix.impl.sync
+import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.buffer
-import org.matrix.rustcomponents.sdk.SyncService
+import org.matrix.rustcomponents.sdk.SyncServiceInterface
import org.matrix.rustcomponents.sdk.SyncServiceState
import org.matrix.rustcomponents.sdk.SyncServiceStateObserver
-fun SyncService.stateFlow(): Flow =
+fun SyncServiceInterface.stateFlow(): Flow =
mxCallbackFlow {
val listener = object : SyncServiceStateObserver {
override fun onUpdate(state: SyncServiceState) {
trySendBlocking(state)
}
}
- state(listener)
+ tryOrNull {
+ state(listener)
+ }
}.buffer(Channel.UNLIMITED)
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessor.kt
index b14d459697..fd880e7fe3 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessor.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessor.kt
@@ -23,6 +23,7 @@ import kotlinx.coroutines.sync.withLock
import org.matrix.rustcomponents.sdk.TimelineChange
import org.matrix.rustcomponents.sdk.TimelineDiff
import org.matrix.rustcomponents.sdk.TimelineItem
+import timber.log.Timber
internal class MatrixTimelineDiffProcessor(
private val timelineItems: MutableStateFlow>,
@@ -33,14 +34,18 @@ internal class MatrixTimelineDiffProcessor(
suspend fun postItems(items: List) {
updateTimelineItems {
+ Timber.v("Update timeline items from postItems (with ${items.size} items) on ${Thread.currentThread()}")
val mappedItems = items.map { it.asMatrixTimelineItem() }
addAll(0, mappedItems)
}
}
- suspend fun postDiff(diff: TimelineDiff) {
+ suspend fun postDiffs(diffs: List) {
updateTimelineItems {
- applyDiff(diff)
+ Timber.v("Update timeline items from postDiffs (with ${diffs.size} items) on ${Thread.currentThread()}")
+ diffs.forEach { diff ->
+ applyDiff(diff)
+ }
}
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt
index d6febb32dc..bddd2bc872 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt
@@ -16,28 +16,47 @@
package io.element.android.libraries.matrix.impl.timeline
+import io.element.android.libraries.core.data.tryOrNull
+import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
+import io.element.android.libraries.matrix.impl.util.destroyAll
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.buffer
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.catch
import org.matrix.rustcomponents.sdk.BackPaginationStatus
import org.matrix.rustcomponents.sdk.BackPaginationStatusListener
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.TimelineDiff
import org.matrix.rustcomponents.sdk.TimelineItem
import org.matrix.rustcomponents.sdk.TimelineListener
+import timber.log.Timber
-internal fun Room.timelineDiffFlow(onInitialList: suspend (List) -> Unit): Flow =
- mxCallbackFlow {
+internal fun Room.timelineDiffFlow(onInitialList: suspend (List) -> Unit): Flow> =
+ callbackFlow {
val listener = object : TimelineListener {
- override fun onUpdate(diff: TimelineDiff) {
+ override fun onUpdate(diff: List) {
trySendBlocking(diff)
}
}
+ val roomId = id()
+ Timber.d("Open timelineDiffFlow for room $roomId")
val result = addTimelineListener(listener)
- onInitialList(result.items)
- result.itemsStream
+ try {
+ onInitialList(result.items)
+ } catch (exception: Exception) {
+ Timber.d(exception, "Catch failure in timelineDiffFlow of room $roomId")
+ }
+ awaitClose {
+ Timber.d("Close timelineDiffFlow for room $roomId")
+ result.itemsStream.cancelAndDestroy()
+ result.items.destroyAll()
+ }
+ }.catch {
+ Timber.d(it, "timelineDiffFlow() failed")
}.buffer(Channel.UNLIMITED)
internal fun Room.backPaginationStatusFlow(): Flow =
@@ -47,5 +66,7 @@ internal fun Room.backPaginationStatusFlow(): Flow =
trySendBlocking(status)
}
}
- subscribeToBackPaginationStatus(listener)
+ tryOrNull {
+ subscribeToBackPaginationStatus(listener)
+ }
}.buffer(Channel.UNLIMITED)
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt
index d9b6604170..aa8427608d 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt
@@ -27,11 +27,13 @@ import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelin
import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper
import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper
import io.element.android.libraries.matrix.impl.timeline.postprocessor.TimelineEncryptedHistoryPostProcessor
+import io.element.android.libraries.matrix.impl.util.TaskHandleBag
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -40,7 +42,6 @@ import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.BackPaginationStatus
@@ -78,6 +79,7 @@ class RustMatrixTimeline(
lastLoginTimestamp = lastLoginTimestamp,
isRoomEncrypted = matrixRoom.isEncrypted,
paginationStateFlow = _paginationState,
+ dispatcher = dispatcher,
)
private val timelineItemFactory = MatrixTimelineItemMapper(
@@ -100,49 +102,55 @@ class RustMatrixTimeline(
init {
Timber.d("Initialize timeline for room ${matrixRoom.roomId}")
+
+ val taskHandleBag = TaskHandleBag()
roomCoroutineScope.launch(dispatcher) {
innerRoom.timelineDiffFlow { initialList ->
postItems(initialList)
- }.onEach { diff ->
- if (diff.eventOrigin() == EventItemOrigin.SYNC) {
+ }.onEach { diffs ->
+ if (diffs.any { diff -> diff.eventOrigin() == EventItemOrigin.SYNC }) {
onNewSyncedEvent()
}
- postDiff(diff)
+ postDiffs(diffs)
}.launchIn(this)
innerRoom.backPaginationStatusFlow()
.onEach {
postPaginationStatus(it)
- }.launchIn(this)
+ }
+ .launchIn(this)
- fetchMembers()
+ taskHandleBag += fetchMembers().getOrNull()
+ }.invokeOnCompletion {
+ taskHandleBag.dispose()
}
}
private suspend fun fetchMembers() = withContext(dispatcher) {
+ initLatch.await()
runCatching {
innerRoom.fetchMembers()
}
}
- @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
- override val timelineItems: Flow> = _timelineItems.sample(50)
- .mapLatest { items ->
- encryptedHistoryPostProcessor.process(items)
- }
+ @OptIn(ExperimentalCoroutinesApi::class)
+ override val timelineItems: Flow> = _timelineItems.mapLatest { items ->
+ encryptedHistoryPostProcessor.process(items)
+ }
- private suspend fun postItems(items: List) {
+ private suspend fun postItems(items: List) = coroutineScope {
// Split the initial items in multiple list as there is no pagination in the cached data, so we can post timelineItems asap.
items.chunked(INITIAL_MAX_SIZE).reversed().forEach {
+ ensureActive()
timelineDiffProcessor.postItems(it)
}
isInit.set(true)
initLatch.complete(Unit)
}
- private suspend fun postDiff(timelineDiff: TimelineDiff) {
+ private suspend fun postDiffs(diffs: List) {
initLatch.await()
- timelineDiffProcessor.postDiff(timelineDiff)
+ timelineDiffProcessor.postDiffs(diffs)
}
private fun postPaginationStatus(status: BackPaginationStatus) {
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt
index 96b61fd558..330a06da62 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt
@@ -42,6 +42,8 @@ import org.matrix.rustcomponents.sdk.MessageType as RustMessageType
class EventMessageMapper {
+ private val timelineEventContentMapper by lazy { TimelineEventContentMapper() }
+
fun map(message: Message): MessageContent = message.use {
val type = it.msgtype().use(this::mapMessageType)
val inReplyToId = it.inReplyTo()?.eventId?.let(::EventId)
@@ -51,7 +53,7 @@ class EventMessageMapper {
val senderProfile = details.senderProfile as? ProfileDetails.Ready
InReplyTo.Ready(
eventId = inReplyToId!!,
- content = map(details.message),
+ content = timelineEventContentMapper.map(details.content),
senderId = UserId(details.sender),
senderDisplayName = senderProfile?.displayName,
senderAvatarUrl = senderProfile?.avatarUrl,
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt
index 359b9ecdef..21e7d51638 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt
@@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
+import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender
import org.matrix.rustcomponents.sdk.Reaction
import org.matrix.rustcomponents.sdk.EventItemOrigin as RustEventItemOrigin
import org.matrix.rustcomponents.sdk.EventSendState as RustEventSendState
@@ -81,8 +82,12 @@ private fun List?.map(): List {
return this?.map {
EventReaction(
key = it.key,
- count = it.count.toLong(),
- senderIds = it.senders.map { sender -> UserId(sender.senderId) }
+ senders = it.senders.map { sender ->
+ ReactionSender(
+ senderId = UserId(sender.senderId),
+ timestamp = sender.timestamp.toLong()
+ )
+ }
)
} ?: emptyList()
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt
index 33727c15d4..7ee1d1490d 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt
@@ -22,6 +22,8 @@ import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParse
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
+import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
+import io.element.android.libraries.matrix.api.timeline.item.event.PollEndContent
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
@@ -30,6 +32,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.StickerConten
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
import io.element.android.libraries.matrix.impl.media.map
+import io.element.android.libraries.matrix.impl.poll.map
import org.matrix.rustcomponents.sdk.TimelineItemContent
import org.matrix.rustcomponents.sdk.TimelineItemContentKind
import org.matrix.rustcomponents.sdk.EncryptedMessage as RustEncryptedMessage
@@ -91,11 +94,27 @@ class TimelineEventContentMapper(private val eventMessageMapper: EventMessageMap
url = kind.url,
)
}
+ is TimelineItemContentKind.Poll -> {
+ PollContent(
+ question = kind.question,
+ kind = kind.kind.map(),
+ maxSelections = kind.maxSelections,
+ answers = kind.answers.map { answer -> answer.map() },
+ votes = kind.votes.mapValues { vote ->
+ vote.value.map { userId -> UserId(userId) }
+ },
+ endTime = kind.endTime,
+ )
+ }
+ is TimelineItemContentKind.PollEnd -> {
+ PollEndContent(startEventId = kind.startEventId)
+ }
is TimelineItemContentKind.UnableToDecrypt -> {
UnableToDecryptContent(
data = kind.msg.map()
)
}
+ else -> UnknownContent
}
}
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/TimelineEncryptedHistoryPostProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/TimelineEncryptedHistoryPostProcessor.kt
index 0fe12e6391..b273bef21b 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/TimelineEncryptedHistoryPostProcessor.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/TimelineEncryptedHistoryPostProcessor.kt
@@ -19,18 +19,23 @@ package io.element.android.libraries.matrix.impl.timeline.postprocessor
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.getAndUpdate
+import kotlinx.coroutines.withContext
+import timber.log.Timber
import java.util.Date
class TimelineEncryptedHistoryPostProcessor(
+ private val dispatcher: CoroutineDispatcher,
private val lastLoginTimestamp: Date?,
private val isRoomEncrypted: Boolean,
private val paginationStateFlow: MutableStateFlow,
) {
- fun process(items: List): List {
- if (!isRoomEncrypted || lastLoginTimestamp == null) return items
+ suspend fun process(items: List): List = withContext(dispatcher) {
+ Timber.d("Process on Thread=${Thread.currentThread()}")
+ if (!isRoomEncrypted || lastLoginTimestamp == null) return@withContext items
val filteredItems = replaceWithEncryptionHistoryBannerIfNeeded(items)
// Disable back pagination
@@ -43,7 +48,7 @@ class TimelineEncryptedHistoryPostProcessor(
)
}
}
- return filteredItems
+ filteredItems
}
private fun replaceWithEncryptionHistoryBannerIfNeeded(list: List): List {
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/LogEventLocation.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/LogEventLocation.kt
new file mode 100644
index 0000000000..712735649c
--- /dev/null
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/LogEventLocation.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.matrix.impl.tracing
+
+/**
+ * This class is used to provide file, line, column information to the Rust SDK [org.matrix.rustcomponents.sdk.logEvent] method.
+ * The data is extracted from a [StackTraceElement] instance.
+ */
+data class LogEventLocation(
+ val file: String,
+ val line: UInt?,
+) {
+
+ companion object {
+ /**
+ * Create a [LogEventLocation] from a [StackTraceElement].
+ */
+ fun from(stackTraceElement: StackTraceElement): LogEventLocation {
+ return LogEventLocation(
+ file = stackTraceElement.fileName ?: "",
+ line = stackTraceElement.lineNumber.takeIf { it >= 0 }?.toUInt()
+ )
+ }
+ }
+}
+
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingService.kt
new file mode 100644
index 0000000000..c211f48c05
--- /dev/null
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingService.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.matrix.impl.tracing
+
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.libraries.core.meta.BuildMeta
+import io.element.android.libraries.di.AppScope
+import io.element.android.libraries.matrix.api.tracing.TracingConfiguration
+import io.element.android.libraries.matrix.api.tracing.TracingService
+import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration
+import org.matrix.rustcomponents.sdk.TracingFileConfiguration
+import timber.log.Timber
+import javax.inject.Inject
+
+@ContributesBinding(AppScope::class)
+class RustTracingService @Inject constructor(private val buildMeta: BuildMeta) : TracingService {
+
+ override fun setupTracing(tracingConfiguration: TracingConfiguration) {
+ val filter = tracingConfiguration.filterConfiguration
+ val rustTracingConfiguration = org.matrix.rustcomponents.sdk.TracingConfiguration(
+ filter = tracingConfiguration.filterConfiguration.filter,
+ writeToStdoutOrSystem = tracingConfiguration.writesToLogcat,
+ writeToFiles = when (val writeToFilesConfiguration = tracingConfiguration.writesToFilesConfiguration) {
+ is WriteToFilesConfiguration.Disabled -> null
+ is WriteToFilesConfiguration.Enabled -> TracingFileConfiguration(
+ path = writeToFilesConfiguration.directory,
+ filePrefix = writeToFilesConfiguration.filenamePrefix,
+ )
+ },
+ )
+ org.matrix.rustcomponents.sdk.setupTracing(rustTracingConfiguration)
+ Timber.v("Tracing config filter = $filter")
+ }
+
+ override fun createTimberTree(): Timber.Tree {
+ return RustTracingTree(retrieveFromStackTrace = buildMeta.isDebuggable)
+ }
+}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingTree.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingTree.kt
new file mode 100644
index 0000000000..275994081d
--- /dev/null
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/tracing/RustTracingTree.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.matrix.impl.tracing
+
+import android.util.Log
+import io.element.android.libraries.matrix.api.tracing.Target
+import org.matrix.rustcomponents.sdk.LogLevel
+import org.matrix.rustcomponents.sdk.logEvent
+import timber.log.Timber
+
+/**
+ * List of fully qualified class names to ignore when looking for the first stack trace element.
+ */
+private val fqcnIgnore = listOf(
+ Timber::class.java.name,
+ Timber.Forest::class.java.name,
+ Timber.Tree::class.java.name,
+ RustTracingTree::class.java.name,
+)
+
+/**
+ * A Timber tree that passes logs to the Rust SDK.
+ */
+internal class RustTracingTree(private val retrieveFromStackTrace: Boolean) : Timber.Tree() {
+
+ override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
+ val location = if (retrieveFromStackTrace) {
+ getLogEventLocationFromStackTrace()
+ } else {
+ LogEventLocation("", null)
+ }
+ val logLevel = priority.toLogLevel()
+ logEvent(
+ file = location.file,
+ line = location.line,
+ level = logLevel,
+ target = Target.ELEMENT.filter,
+ message = message,
+ )
+ }
+
+ /**
+ * Extract the [LogEventLocation] from the stack trace.
+ */
+ private fun getLogEventLocationFromStackTrace(): LogEventLocation {
+ return Throwable(null, null).stackTrace
+ .first { it.className !in fqcnIgnore }
+ .let(LogEventLocation::from)
+ }
+}
+
+/**
+ * Convert a log priority to a Rust SDK log level.
+ */
+private fun Int.toLogLevel(): LogLevel {
+ return when (this) {
+ Log.VERBOSE -> LogLevel.TRACE
+ Log.DEBUG -> LogLevel.DEBUG
+ Log.INFO -> LogLevel.INFO
+ Log.WARN -> LogLevel.WARN
+ Log.ERROR -> LogLevel.ERROR
+ else -> LogLevel.DEBUG
+ }
+}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/CallbackFlow.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/CallbackFlow.kt
index a347973e89..fbf393e587 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/CallbackFlow.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/CallbackFlow.kt
@@ -21,11 +21,10 @@ import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import org.matrix.rustcomponents.sdk.TaskHandle
-internal fun mxCallbackFlow(block: suspend ProducerScope.() -> TaskHandle) =
+internal fun mxCallbackFlow(block: suspend ProducerScope.() -> TaskHandle?) =
callbackFlow {
- val token: TaskHandle = block(this)
+ val taskHandle: TaskHandle? = block(this)
awaitClose {
- token.cancel()
- token.destroy()
+ taskHandle?.cancelAndDestroy()
}
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/Disposables.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/Disposables.kt
new file mode 100644
index 0000000000..ac92a2e026
--- /dev/null
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/Disposables.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.matrix.impl.util
+
+import org.matrix.rustcomponents.sdk.Disposable
+
+/**
+ * Call destroy on all elements of the iterable.
+ */
+internal fun Iterable.destroyAll() = forEach { it.destroy() }
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/TaskHandleBag.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/TaskHandle.kt
similarity index 73%
rename from libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/TaskHandleBag.kt
rename to libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/TaskHandle.kt
index 9a21645351..5842ba1546 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/TaskHandleBag.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/TaskHandle.kt
@@ -19,18 +19,22 @@ package io.element.android.libraries.matrix.impl.util
import org.matrix.rustcomponents.sdk.TaskHandle
import java.util.concurrent.CopyOnWriteArraySet
-class TaskHandleBag(private val tokens: MutableSet = CopyOnWriteArraySet()) : Set by tokens {
+fun TaskHandle.cancelAndDestroy() {
+ cancel()
+ destroy()
+}
+
+class TaskHandleBag(private val taskHandles: MutableSet = CopyOnWriteArraySet()) : Set by taskHandles {
operator fun plusAssign(taskHandle: TaskHandle?) {
if (taskHandle == null) return
- tokens += taskHandle
+ taskHandles += taskHandle
}
fun dispose() {
- tokens.forEach {
- it.cancel()
- it.destroy()
+ taskHandles.forEach {
+ it.cancelAndDestroy()
}
- tokens.clear()
+ taskHandles.clear()
}
}
diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/TimelineEncryptedHistoryPostProcessorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/TimelineEncryptedHistoryPostProcessorTest.kt
index 91f0bc1883..63920cd6ce 100644
--- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/TimelineEncryptedHistoryPostProcessorTest.kt
+++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/TimelineEncryptedHistoryPostProcessorTest.kt
@@ -22,15 +22,18 @@ import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
import io.element.android.libraries.matrix.test.room.anEventTimelineItem
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.util.Date
class TimelineEncryptedHistoryPostProcessorTest {
- private val defaultLastLoginTimestamp = Date(1689061264L)
+ private val defaultLastLoginTimestamp = Date(1_689_061_264L)
@Test
- fun `given an unencrypted room, nothing is done`() {
+ fun `given an unencrypted room, nothing is done`() = runTest {
val processor = createPostProcessor(isRoomEncrypted = false)
val items = listOf(
MatrixTimelineItem.Event(0L, anEventTimelineItem())
@@ -39,7 +42,7 @@ class TimelineEncryptedHistoryPostProcessorTest {
}
@Test
- fun `given a null lastLoginTimestamp, nothing is done`() {
+ fun `given a null lastLoginTimestamp, nothing is done`() = runTest {
val processor = createPostProcessor(lastLoginTimestamp = null)
val items = listOf(
MatrixTimelineItem.Event(0L, anEventTimelineItem())
@@ -48,14 +51,14 @@ class TimelineEncryptedHistoryPostProcessorTest {
}
@Test
- fun `given an empty list, nothing is done`() {
+ fun `given an empty list, nothing is done`() = runTest {
val processor = createPostProcessor()
val items = emptyList()
assertThat(processor.process(items)).isSameInstanceAs(items)
}
@Test
- fun `given a list with no items before lastLoginTimestamp, nothing is done`() {
+ fun `given a list with no items before lastLoginTimestamp, nothing is done`() = runTest {
val processor = createPostProcessor()
val items = listOf(
MatrixTimelineItem.Event(0L, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time + 1))
@@ -64,7 +67,7 @@ class TimelineEncryptedHistoryPostProcessorTest {
}
@Test
- fun `given a list with an item with equal timestamp as lastLoginTimestamp, it's replaced`() {
+ fun `given a list with an item with equal timestamp as lastLoginTimestamp, it's replaced`() = runTest {
val processor = createPostProcessor()
val items = listOf(
MatrixTimelineItem.Event(0L, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time))
@@ -74,7 +77,7 @@ class TimelineEncryptedHistoryPostProcessorTest {
}
@Test
- fun `given a list with an item with a lower timestamp than lastLoginTimestamp, it's replaced`() {
+ fun `given a list with an item with a lower timestamp than lastLoginTimestamp, it's replaced`() = runTest {
val processor = createPostProcessor()
val items = listOf(
MatrixTimelineItem.Event(0L, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time - 1))
@@ -85,7 +88,7 @@ class TimelineEncryptedHistoryPostProcessorTest {
}
@Test
- fun `given a list with several with lower or equal timestamps than lastLoginTimestamp, they're replaced and the user can't back paginate`() {
+ fun `given a list with several with lower or equal timestamps than lastLoginTimestamp, they're replaced and the user can't back paginate`() = runTest {
val paginationStateFlow = MutableStateFlow(MatrixTimeline.PaginationState(hasMoreToLoadBackwards = true, isBackPaginating = false))
val processor = createPostProcessor(paginationStateFlow = paginationStateFlow)
val items = listOf(
@@ -102,7 +105,7 @@ class TimelineEncryptedHistoryPostProcessorTest {
assertThat(paginationStateFlow.value).isEqualTo(MatrixTimeline.PaginationState(hasMoreToLoadBackwards = false, isBackPaginating = false))
}
- private fun createPostProcessor(
+ private fun TestScope.createPostProcessor(
lastLoginTimestamp: Date? = defaultLastLoginTimestamp,
isRoomEncrypted: Boolean = true,
paginationStateFlow: MutableStateFlow =
@@ -111,5 +114,6 @@ class TimelineEncryptedHistoryPostProcessorTest {
lastLoginTimestamp = lastLoginTimestamp,
isRoomEncrypted = isRoomEncrypted,
paginationStateFlow = paginationStateFlow,
+ dispatcher = StandardTestDispatcher(testScheduler)
)
}
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt
index 1a654ac8d4..1229836e30 100644
--- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt
@@ -27,7 +27,7 @@ import io.element.android.libraries.matrix.api.notification.NotificationService
import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
-import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource
+import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
@@ -35,7 +35,7 @@ import io.element.android.libraries.matrix.test.media.FakeMediaLoader
import io.element.android.libraries.matrix.test.notification.FakeNotificationService
import io.element.android.libraries.matrix.test.pushers.FakePushersService
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
-import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
+import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.libraries.matrix.test.sync.FakeSyncService
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import io.element.android.tests.testutils.simulateLongTask
@@ -45,7 +45,7 @@ class FakeMatrixClient(
override val sessionId: SessionId = A_SESSION_ID,
private val userDisplayName: Result = Result.success(A_USER_NAME),
private val userAvatarURLString: Result = Result.success(AN_AVATAR_URL),
- override val roomSummaryDataSource: RoomSummaryDataSource = FakeRoomSummaryDataSource(),
+ override val roomListService: RoomListService = FakeRoomListService(),
override val mediaLoader: MatrixMediaLoader = FakeMediaLoader(),
private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
private val pushersService: FakePushersService = FakePushersService(),
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaUploadHandler.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaUploadHandler.kt
new file mode 100644
index 0000000000..100dbd5f66
--- /dev/null
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaUploadHandler.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.libraries.matrix.test.media
+
+import io.element.android.libraries.matrix.api.media.MediaUploadHandler
+import io.element.android.tests.testutils.simulateLongTask
+import kotlin.coroutines.cancellation.CancellationException
+
+class FakeMediaUploadHandler(
+ private var result: Result = Result.success(Unit),
+) : MediaUploadHandler {
+ override suspend fun await(): Result = simulateLongTask { result }
+
+ override fun cancel() {
+ result = Result.failure(CancellationException())
+ }
+}
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/FakeNotificationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/FakeNotificationService.kt
index 9eb5a20ba4..7cb92d35c5 100644
--- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/FakeNotificationService.kt
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/FakeNotificationService.kt
@@ -23,7 +23,7 @@ import io.element.android.libraries.matrix.api.notification.NotificationData
import io.element.android.libraries.matrix.api.notification.NotificationService
class FakeNotificationService : NotificationService {
- override fun getNotification(userId: SessionId, roomId: RoomId, eventId: EventId, filterByPushRules: Boolean): Result {
+ override suspend fun getNotification(userId: SessionId, roomId: RoomId, eventId: EventId): Result {
return Result.success(null)
}
}
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
index 59f6ed57bd..a660c56a99 100644
--- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
@@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.media.AudioInfo
import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ImageInfo
+import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
@@ -34,6 +35,7 @@ import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
+import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
import io.element.android.tests.testutils.simulateLongTask
import kotlinx.coroutines.delay
@@ -56,6 +58,7 @@ class FakeMatrixRoom(
override val joinedMemberCount: Long = 123L,
override val activeMemberCount: Long = 234L,
private val matrixTimeline: MatrixTimeline = FakeMatrixTimeline(),
+ canRedact: Boolean = false,
) : MatrixRoom {
private var ignoreResult: Result = Result.success(Unit)
@@ -66,9 +69,10 @@ class FakeMatrixRoom(
private var joinRoomResult = Result.success(Unit)
private var inviteUserResult = Result.success(Unit)
private var canInviteResult = Result.success(true)
+ private var canRedactResult = Result.success(canRedact)
private val canSendStateResults = mutableMapOf>()
private val canSendEventResults = mutableMapOf>()
- private var sendMediaResult = Result.success(Unit)
+ private var sendMediaResult = Result.success(FakeMediaUploadHandler())
private var setNameResult = Result.success(Unit)
private var setTopicResult = Result.success(Unit)
private var updateAvatarResult = Result.success(Unit)
@@ -207,6 +211,10 @@ class FakeMatrixRoom(
return canInviteResult
}
+ override suspend fun canUserRedact(userId: UserId): Result {
+ return canRedactResult
+ }
+
override suspend fun canUserSendState(userId: UserId, type: StateEventType): Result {
return canSendStateResults[type] ?: Result.failure(IllegalStateException("No fake answer"))
}
@@ -220,21 +228,34 @@ class FakeMatrixRoom(
thumbnailFile: File,
imageInfo: ImageInfo,
progressCallback: ProgressCallback?
- ): Result = fakeSendMedia(progressCallback)
+ ): Result = fakeSendMedia(progressCallback)
- override suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo, progressCallback: ProgressCallback?): Result = fakeSendMedia(
+ override suspend fun sendVideo(
+ file: File,
+ thumbnailFile: File,
+ videoInfo: VideoInfo,
+ progressCallback: ProgressCallback?
+ ): Result = fakeSendMedia(
progressCallback
)
- override suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result = fakeSendMedia(progressCallback)
+ override suspend fun sendAudio(
+ file: File,
+ audioInfo: AudioInfo,
+ progressCallback: ProgressCallback?
+ ): Result = fakeSendMedia(progressCallback)
- override suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result = fakeSendMedia(progressCallback)
+ override suspend fun sendFile(
+ file: File,
+ fileInfo: FileInfo,
+ progressCallback: ProgressCallback?
+ ): Result = fakeSendMedia(progressCallback)
override suspend fun forwardEvent(eventId: EventId, roomIds: List): Result = simulateLongTask {
forwardEventResult
}
- private suspend fun fakeSendMedia(progressCallback: ProgressCallback?): Result = simulateLongTask {
+ private suspend fun fakeSendMedia(progressCallback: ProgressCallback?): Result = simulateLongTask {
sendMediaResult.onSuccess {
progressCallbackValues.forEach { (current, total) ->
progressCallback?.onProgress(current, total)
@@ -332,7 +353,7 @@ class FakeMatrixRoom(
unignoreResult = result
}
- fun givenSendMediaResult(result: Result) {
+ fun givenSendMediaResult(result: Result) {
sendMediaResult = result
}
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt
index 4df815c54c..59bac7ad40 100644
--- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt
@@ -20,8 +20,8 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.core.UserId
-import io.element.android.libraries.matrix.api.room.RoomSummary
-import io.element.android.libraries.matrix.api.room.RoomSummaryDetails
+import io.element.android.libraries.matrix.api.roomlist.RoomSummary
+import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
import io.element.android.libraries.matrix.api.room.message.RoomMessage
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeRoomSummaryDataSource.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeRoomListService.kt
similarity index 51%
rename from libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeRoomSummaryDataSource.kt
rename to libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeRoomListService.kt
index cae36e14c8..fa2e347e3b 100644
--- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeRoomSummaryDataSource.kt
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeRoomListService.kt
@@ -14,18 +14,21 @@
* limitations under the License.
*/
-package io.element.android.libraries.matrix.test.room
+package io.element.android.libraries.matrix.test.roomlist
-import io.element.android.libraries.matrix.api.room.RoomSummary
-import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource
+import io.element.android.libraries.matrix.api.roomlist.RoomList
+import io.element.android.libraries.matrix.api.roomlist.RoomListService
+import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
-class FakeRoomSummaryDataSource : RoomSummaryDataSource {
+class FakeRoomListService : RoomListService {
private val allRoomSummariesFlow = MutableStateFlow>(emptyList())
private val inviteRoomSummariesFlow = MutableStateFlow>(emptyList())
- private val allRoomsLoadingStateFlow = MutableStateFlow(RoomSummaryDataSource.LoadingState.NotLoaded)
+ private val allRoomsLoadingStateFlow = MutableStateFlow(RoomList.LoadingState.NotLoaded)
+ private val inviteRoomsLoadingStateFlow = MutableStateFlow(RoomList.LoadingState.NotLoaded)
+ private val roomListStateFlow = MutableStateFlow(RoomListService.State.Idle)
suspend fun postAllRooms(roomSummaries: List) {
allRoomSummariesFlow.emit(roomSummaries)
@@ -35,20 +38,16 @@ class FakeRoomSummaryDataSource : RoomSummaryDataSource {
inviteRoomSummariesFlow.emit(roomSummaries)
}
- suspend fun postLoadingState(loadingState: RoomSummaryDataSource.LoadingState) {
+ suspend fun postAllRoomsLoadingState(loadingState: RoomList.LoadingState) {
allRoomsLoadingStateFlow.emit(loadingState)
}
- override fun allRoomsLoadingState(): StateFlow {
- return allRoomsLoadingStateFlow
+ suspend fun postInviteRoomsLoadingState(loadingState: RoomList.LoadingState) {
+ inviteRoomsLoadingStateFlow.emit(loadingState)
}
- override fun allRooms(): StateFlow> {
- return allRoomSummariesFlow
- }
-
- override fun inviteRooms(): StateFlow> {
- return inviteRoomSummariesFlow
+ suspend fun postState(state: RoomListService.State) {
+ roomListStateFlow.emit(state)
}
var latestSlidingSyncRange: IntRange? = null
@@ -57,4 +56,20 @@ class FakeRoomSummaryDataSource : RoomSummaryDataSource {
override fun updateAllRoomsVisibleRange(range: IntRange) {
latestSlidingSyncRange = range
}
+
+ override fun allRooms(): RoomList {
+ return SimpleRoomList(
+ summaries = allRoomSummariesFlow,
+ loadingState = allRoomsLoadingStateFlow
+ )
+ }
+
+ override fun invites(): RoomList {
+ return SimpleRoomList(
+ summaries = inviteRoomSummariesFlow,
+ loadingState = inviteRoomsLoadingStateFlow
+ )
+ }
+
+ override val state: StateFlow = roomListStateFlow
}
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimpleRoomList.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimpleRoomList.kt
new file mode 100644
index 0000000000..28b04ae318
--- /dev/null
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimpleRoomList.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.matrix.test.roomlist
+
+import io.element.android.libraries.matrix.api.roomlist.RoomList
+import io.element.android.libraries.matrix.api.roomlist.RoomSummary
+import kotlinx.coroutines.flow.StateFlow
+
+data class SimpleRoomList(
+ override val summaries: StateFlow>,
+ override val loadingState: StateFlow
+) : RoomList
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/sync/FakeSyncService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/sync/FakeSyncService.kt
index dd653a76ec..4e618deb9a 100644
--- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/sync/FakeSyncService.kt
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/sync/FakeSyncService.kt
@@ -34,7 +34,7 @@ class FakeSyncService : SyncService {
return Result.success(Unit)
}
- override fun stopSync(): Result {
+ override suspend fun stopSync(): Result {
syncStateFlow.value = SyncState.Terminated
return Result.success(Unit)
}
diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AvatarActionBottomSheet.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AvatarActionBottomSheet.kt
index b12f577c36..3b276903a0 100644
--- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AvatarActionBottomSheet.kt
+++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AvatarActionBottomSheet.kt
@@ -15,6 +15,7 @@
*/
@file:OptIn(ExperimentalMaterialApi::class)
+@file:Suppress("UsingMaterialAndMaterial3Libraries")
package io.element.android.libraries.matrix.ui.components
@@ -32,6 +33,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
@@ -111,12 +113,12 @@ private fun AvatarActionBottomSheetContent(
@Preview
@Composable
-fun AvatarActionBottomSheetLightPreview() =
+internal fun AvatarActionBottomSheetLightPreview() =
ElementPreviewLight { ContentToPreview() }
@Preview
@Composable
-fun AvatarActionBottomSheetDarkPreview() =
+internal fun AvatarActionBottomSheetDarkPreview() =
ElementPreviewDark { ContentToPreview() }
@Composable
@@ -124,7 +126,8 @@ private fun ContentToPreview() {
AvatarActionBottomSheet(
actions = persistentListOf(AvatarAction.TakePhoto, AvatarAction.ChoosePhoto, AvatarAction.Remove),
modalBottomSheetState = ModalBottomSheetState(
- initialValue = ModalBottomSheetValue.Expanded
+ initialValue = ModalBottomSheetValue.Expanded,
+ density = LocalDensity.current,
),
)
}
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 6054aa53af..a42d44b642 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
@@ -103,12 +103,12 @@ private fun MatrixUserHeaderContent(
@Preview
@Composable
-fun MatrixUserHeaderLightPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) =
+internal fun MatrixUserHeaderLightPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) =
ElementPreviewLight { ContentToPreview(matrixUser) }
@Preview
@Composable
-fun MatrixUserHeaderDarkPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) =
+internal fun MatrixUserHeaderDarkPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) =
ElementPreviewDark { ContentToPreview(matrixUser) }
@Composable
diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserHeaderPlaceholder.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserHeaderPlaceholder.kt
index 57901edc03..faa80f82e6 100644
--- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserHeaderPlaceholder.kt
+++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserHeaderPlaceholder.kt
@@ -68,12 +68,12 @@ fun MatrixUserHeaderPlaceholder(
@Preview
@Composable
-fun MatrixUserHeaderPlaceholderLightPreview() =
+internal fun MatrixUserHeaderPlaceholderLightPreview() =
ElementPreviewLight { ContentToPreview() }
@Preview
@Composable
-fun MatrixUserHeaderPlaceholderDarkPreview() =
+internal fun MatrixUserHeaderPlaceholderDarkPreview() =
ElementPreviewDark { ContentToPreview() }
@Composable
diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedRoom.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedRoom.kt
index 2881235335..da6f69d830 100644
--- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedRoom.kt
+++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedRoom.kt
@@ -46,7 +46,7 @@ import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.RoomId
-import io.element.android.libraries.matrix.api.room.RoomSummaryDetails
+import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnresolvedUserRow.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnresolvedUserRow.kt
index de4d575de0..b84cddd531 100644
--- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnresolvedUserRow.kt
+++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnresolvedUserRow.kt
@@ -41,7 +41,7 @@ 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.ElementThemedPreview
import io.element.android.libraries.designsystem.theme.components.Checkbox
-import io.element.android.libraries.designsystem.theme.components.Divider
+import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.ui.model.getAvatarData
@@ -149,11 +149,11 @@ internal fun CheckableUnresolvedUserRowPreview() =
val matrixUser = aMatrixUser()
Column {
CheckableUnresolvedUserRow(false, matrixUser.getAvatarData(AvatarSize.UserListItem), matrixUser.userId.value)
- Divider()
+ HorizontalDivider()
CheckableUnresolvedUserRow(true, matrixUser.getAvatarData(AvatarSize.UserListItem), matrixUser.userId.value)
- Divider()
+ HorizontalDivider()
CheckableUnresolvedUserRow(false, matrixUser.getAvatarData(AvatarSize.UserListItem), matrixUser.userId.value, enabled = false)
- Divider()
+ HorizontalDivider()
CheckableUnresolvedUserRow(true, matrixUser.getAvatarData(AvatarSize.UserListItem), matrixUser.userId.value, enabled = false)
}
}
diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnsavedAvatar.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnsavedAvatar.kt
index 2b5d2f6800..13fcb40792 100644
--- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnsavedAvatar.kt
+++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnsavedAvatar.kt
@@ -85,11 +85,11 @@ fun UnsavedAvatar(
@Preview
@Composable
-fun UnsavedAvatarLightPreview() = ElementPreviewLight { ContentToPreview() }
+internal fun UnsavedAvatarLightPreview() = ElementPreviewLight { ContentToPreview() }
@Preview
@Composable
-fun UnsavedAvatarDarkPreview() = ElementPreviewDark { ContentToPreview() }
+internal fun UnsavedAvatarDarkPreview() = ElementPreviewDark { ContentToPreview() }
@Composable
private fun ContentToPreview() {
diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt
index 7995672e92..668b963bf1 100644
--- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt
+++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt
@@ -57,14 +57,9 @@ fun MatrixRoom.getDirectRoomMember(roomMembersState: MatrixRoomMembersState): St
val roomMembers = roomMembersState.roomMembers()
return remember(roomMembersState) {
derivedStateOf {
- if (roomMembers == null) {
- null
- } else if (roomMembers.size == 2 && isDirect && isEncrypted) {
- roomMembers.find { it.userId != this.sessionId }
- } else {
- null
- }
+ roomMembers
+ ?.takeIf { it.size == 2 && isDirect && isEncrypted }
+ ?.find { it.userId != sessionId }
}
}
}
-
diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt
index 005a0ac747..f2a73545bf 100644
--- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt
+++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt
@@ -21,6 +21,7 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.produceState
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MessageEventType
+import io.element.android.libraries.matrix.api.room.powerlevels.canRedact
import io.element.android.libraries.matrix.api.room.powerlevels.canSendMessage
@Composable
@@ -30,3 +31,10 @@ fun MatrixRoom.canSendMessageAsState(type: MessageEventType, updateKey: Long): S
}
}
+@Composable
+fun MatrixRoom.canRedactAsState(updateKey: Long): State {
+ return produceState(initialValue = false, key1 = updateKey) {
+ value = canRedact().getOrElse { false }
+ }
+}
+
diff --git a/libraries/mediaupload/api/build.gradle.kts b/libraries/mediaupload/api/build.gradle.kts
index 111abc2bcc..c1e501d02a 100644
--- a/libraries/mediaupload/api/build.gradle.kts
+++ b/libraries/mediaupload/api/build.gradle.kts
@@ -38,5 +38,12 @@ android {
api(projects.libraries.matrix.api)
implementation(libs.inject)
implementation(libs.coroutines.core)
+
+ testImplementation(projects.libraries.matrix.test)
+ testImplementation(projects.libraries.mediaupload.test)
+ testImplementation(libs.test.junit)
+ testImplementation(libs.test.truth)
+ testImplementation(libs.coroutines.test)
+ testImplementation(libs.test.robolectric)
}
}
diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt
index 1622ab2eef..899e92efc5 100644
--- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt
+++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt
@@ -17,9 +17,13 @@
package io.element.android.libraries.mediaupload.api
import android.net.Uri
-import io.element.android.libraries.core.extensions.flatMap
+import io.element.android.libraries.core.extensions.flatMapCatching
import io.element.android.libraries.matrix.api.core.ProgressCallback
+import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.room.MatrixRoom
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.Job
+import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
class MediaSender @Inject constructor(
@@ -27,6 +31,9 @@ class MediaSender @Inject constructor(
private val room: MatrixRoom,
) {
+ private val ongoingUploadJobs = ConcurrentHashMap()
+ val hasOngoingMediaUploads get() = ongoingUploadJobs.isNotEmpty()
+
suspend fun sendMedia(
uri: Uri,
mimeType: String,
@@ -40,16 +47,25 @@ class MediaSender @Inject constructor(
deleteOriginal = true,
compressIfPossible = compressIfPossible
)
- .flatMap { info ->
+ .flatMapCatching { info ->
room.sendMedia(info, progressCallback)
}
+ .onFailure { error ->
+ val job = ongoingUploadJobs.remove(Job)
+ if (error !is CancellationException) {
+ job?.cancel()
+ }
+ }
+ .onSuccess {
+ ongoingUploadJobs.remove(Job)
+ }
}
private suspend fun MatrixRoom.sendMedia(
uploadInfo: MediaUploadInfo,
- progressCallback: ProgressCallback?
+ progressCallback: ProgressCallback?,
): Result {
- return when (uploadInfo) {
+ val handler = when (uploadInfo) {
is MediaUploadInfo.Image -> {
sendImage(
file = uploadInfo.file,
@@ -83,5 +99,11 @@ class MediaSender @Inject constructor(
)
}
}
+
+ return handler
+ .flatMapCatching { uploadHandler ->
+ ongoingUploadJobs[Job] = uploadHandler
+ uploadHandler.await()
+ }
}
}
diff --git a/libraries/mediaupload/api/src/test/kotlin/io/element/android/libraries/mediaupload/api/MediaSenderTests.kt b/libraries/mediaupload/api/src/test/kotlin/io/element/android/libraries/mediaupload/api/MediaSenderTests.kt
new file mode 100644
index 0000000000..480cf8065f
--- /dev/null
+++ b/libraries/mediaupload/api/src/test/kotlin/io/element/android/libraries/mediaupload/api/MediaSenderTests.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.mediaupload.api
+
+import android.net.Uri
+import com.google.common.truth.Truth.assertThat
+import io.element.android.libraries.matrix.api.room.MatrixRoom
+import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
+import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class MediaSenderTests {
+
+ @Test
+ fun `given an attachment when sending it the preprocessor always runs`() = runTest {
+ val preProcessor = FakeMediaPreProcessor()
+ val sender = aMediaSender(preProcessor)
+
+ val uri = Uri.parse("content://image.jpg")
+ sender.sendMedia(uri = uri, mimeType = "image/jpeg", compressIfPossible = true)
+
+ assertThat(preProcessor.processCallCount).isEqualTo(1)
+ }
+
+ @Test
+ fun `given an attachment when sending it the MatrixRoom will call sendMedia`() = runTest {
+ val room = FakeMatrixRoom()
+ val sender = aMediaSender(room = room)
+
+ val uri = Uri.parse("content://image.jpg")
+ sender.sendMedia(uri = uri, mimeType = "image/jpeg", compressIfPossible = true)
+
+ assertThat(room.sendMediaCount).isEqualTo(1)
+ }
+
+ @Test
+ fun `given a failure in the preprocessor when sending the whole process fails`() = runTest {
+ val preProcessor = FakeMediaPreProcessor().apply {
+ givenResult(Result.failure(Exception()))
+ }
+ val sender = aMediaSender(preProcessor)
+
+ val uri = Uri.parse("content://image.jpg")
+ val result = sender.sendMedia(uri = uri, mimeType = "image/jpeg", compressIfPossible = true)
+
+ assertThat(result.exceptionOrNull()).isNotNull()
+ }
+
+ @Test
+ fun `given a failure in the media upload when sending the whole process fails`() = runTest {
+ val room = FakeMatrixRoom().apply {
+ givenSendMediaResult(Result.failure(Exception()))
+ }
+ val sender = aMediaSender(room = room)
+
+ val uri = Uri.parse("content://image.jpg")
+ val result = sender.sendMedia(uri = uri, mimeType = "image/jpeg", compressIfPossible = true)
+
+ assertThat(result.exceptionOrNull()).isNotNull()
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun `given a cancellation in the media upload when sending the job is cancelled`() = runTest(StandardTestDispatcher()) {
+ val room = FakeMatrixRoom()
+ val sender = aMediaSender(room = room)
+ val sendJob = launch {
+ val uri = Uri.parse("content://image.jpg")
+ sender.sendMedia(uri = uri, mimeType = "image/jpeg", compressIfPossible = true)
+ }
+ // Wait until several internal tasks run and the file is being uploaded
+ advanceTimeBy(3L)
+
+ // Assert the file is being uploaded
+ assertThat(sender.hasOngoingMediaUploads).isTrue()
+
+ // Cancel the coroutine
+ sendJob.cancel()
+
+ // Wait for the coroutine cleanup to happen
+ advanceTimeBy(1L)
+
+ // Assert the file is not being uploaded anymore
+ assertThat(sender.hasOngoingMediaUploads).isFalse()
+ }
+
+ private fun aMediaSender(
+ preProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
+ room: MatrixRoom = FakeMatrixRoom(),
+ ) = MediaSender(
+ preProcessor,
+ room,
+ )
+}
diff --git a/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt b/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt
index 0cc7803578..d94414d2d7 100644
--- a/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt
+++ b/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt
@@ -25,6 +25,9 @@ import java.io.File
class FakeMediaPreProcessor : MediaPreProcessor {
+ var processCallCount = 0
+ private set
+
private var result: Result = Result.success(
MediaUploadInfo.AnyFile(
File("test"),
@@ -43,6 +46,7 @@ class FakeMediaPreProcessor : MediaPreProcessor {
deleteOriginal: Boolean,
compressIfPossible: Boolean
): Result = simulateLongTask {
+ processCallCount++
result
}
diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt
index 30f19aa31c..d9dba2a0d3 100644
--- a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt
+++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt
@@ -83,12 +83,12 @@ fun PermissionsView(
@Preview
@Composable
-fun PermissionsViewLightPreview(@PreviewParameter(PermissionsViewStateProvider::class) state: PermissionsState) =
+internal fun PermissionsViewLightPreview(@PreviewParameter(PermissionsViewStateProvider::class) state: PermissionsState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
-fun PermissionsViewDarkPreview(@PreviewParameter(PermissionsViewStateProvider::class) state: PermissionsState) =
+internal fun PermissionsViewDarkPreview(@PreviewParameter(PermissionsViewStateProvider::class) state: PermissionsState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts
index b0f3be6deb..30a686cad6 100644
--- a/libraries/push/impl/build.gradle.kts
+++ b/libraries/push/impl/build.gradle.kts
@@ -16,7 +16,7 @@
plugins {
id("io.element.android-library")
alias(libs.plugins.anvil)
- kotlin("plugin.serialization") version "1.8.22"
+ kotlin("plugin.serialization") version "1.9.0"
}
android {
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt
index 29ac866347..e5af7785db 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt
@@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.ThreadId
+import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.NotificationContent
import io.element.android.libraries.matrix.api.notification.NotificationData
import io.element.android.libraries.matrix.api.room.RoomMembershipState
@@ -68,31 +69,27 @@ class NotifiableEventResolver @Inject constructor(
userId = sessionId,
roomId = roomId,
eventId = eventId,
- // FIXME should be true in the future, but right now it's broken
- // (https://github.com/vector-im/element-x-android/issues/640#issuecomment-1612913658)
- filterByPushRules = false,
).onFailure {
Timber.tag(loggerTag.value).e(it, "Unable to resolve event: $eventId.")
}.getOrNull()
// TODO this notificationData is not always valid at the moment, sometimes the Rust SDK can't fetch the matching event
return notificationData?.asNotifiableEvent(sessionId)
- ?: fallbackNotifiableEvent(sessionId, roomId, eventId)
}
private fun NotificationData.asNotifiableEvent(userId: SessionId): NotifiableEvent? {
- return when (val content = this.event.content) {
+ return when (val content = this.content) {
is NotificationContent.MessageLike.RoomMessage -> {
buildNotifiableMessageEvent(
sessionId = userId,
+ senderId = content.senderId,
roomId = roomId,
eventId = eventId,
noisy = isNoisy,
- timestamp = event.timestamp,
+ timestamp = this.timestamp,
senderName = senderDisplayName,
- senderId = senderId.value,
body = descriptionFromMessageContent(content),
- imageUriString = event.contentUrl,
+ imageUriString = this.contentUrl,
roomName = roomDisplayName,
roomIsDirect = isDirect,
roomAvatarPath = roomAvatarUrl,
@@ -109,7 +106,7 @@ class NotifiableEventResolver @Inject constructor(
canBeReplaced = true,
roomName = roomDisplayName,
noisy = isNoisy,
- timestamp = event.timestamp,
+ timestamp = this.timestamp,
soundName = null,
isRedacted = false,
isUpdated = false,
@@ -118,10 +115,10 @@ class NotifiableEventResolver @Inject constructor(
title = null, // TODO check if title is needed anymore
)
} else {
- null
+ fallbackNotifiableEvent(userId, roomId, eventId)
}
}
- else -> null
+ else -> fallbackNotifiableEvent(userId, roomId, eventId)
}
}
@@ -177,6 +174,7 @@ class NotifiableEventResolver @Inject constructor(
@Suppress("LongParameterList")
private fun buildNotifiableMessageEvent(
sessionId: SessionId,
+ senderId: UserId,
roomId: RoomId,
eventId: EventId,
editedEventId: EventId? = null,
@@ -184,7 +182,6 @@ private fun buildNotifiableMessageEvent(
noisy: Boolean,
timestamp: Long,
senderName: String?,
- senderId: String?,
body: String?,
// We cannot use Uri? type here, as that could trigger a
// NotSerializableException when persisting this to storage
@@ -202,6 +199,7 @@ private fun buildNotifiableMessageEvent(
isUpdated: Boolean = false
) = NotifiableMessageEvent(
sessionId = sessionId,
+ senderId = senderId,
roomId = roomId,
eventId = eventId,
editedEventId = editedEventId,
@@ -209,7 +207,6 @@ private fun buildNotifiableMessageEvent(
noisy = noisy,
timestamp = timestamp,
senderName = senderName,
- senderId = senderId,
body = body,
imageUriString = imageUriString,
threadId = threadId,
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomEventGroupInfo.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomEventGroupInfo.kt
index 734c34b051..96a8b90f06 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomEventGroupInfo.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomEventGroupInfo.kt
@@ -23,17 +23,15 @@ import io.element.android.libraries.matrix.api.core.SessionId
* Data class to hold information about a group of notifications for a room.
*/
data class RoomEventGroupInfo(
- val sessionId: SessionId,
- val roomId: RoomId,
- val roomDisplayName: String,
- val isDirect: Boolean = false
-) {
+ val sessionId: SessionId,
+ val roomId: RoomId,
+ val roomDisplayName: String,
+ val isDirect: Boolean = false,
// An event in the list has not yet been display
- var hasNewEvent: Boolean = false
-
+ val hasNewEvent: Boolean = false,
// true if at least one on the not yet displayed event is noisy
- var shouldBing: Boolean = false
- var customSound: String? = null
- var hasSmartReplyError: Boolean = false
- var isUpdated: Boolean = false
-}
+ val shouldBing: Boolean = false,
+ val customSound: String? = null,
+ val hasSmartReplyError: Boolean = false,
+ val isUpdated: Boolean = false,
+)
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt
index 989ba2ad09..29d828d34c 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt
@@ -85,12 +85,11 @@ class RoomGroupMessageCreator @Inject constructor(
roomId = roomId,
roomDisplayName = roomName,
isDirect = !roomIsGroup,
- ).also {
- it.hasSmartReplyError = smartReplyErrors.isNotEmpty()
- it.shouldBing = meta.shouldBing
- it.customSound = events.last().soundName
- it.isUpdated = events.last().isUpdated
- },
+ hasSmartReplyError = smartReplyErrors.isNotEmpty(),
+ shouldBing = meta.shouldBing,
+ customSound = events.last().soundName,
+ isUpdated = events.last().isUpdated,
+ ),
threadId = lastKnownRoomEvent.threadId,
largeIcon = largeBitmap,
lastMessageTimestamp,
@@ -108,7 +107,7 @@ class RoomGroupMessageCreator @Inject constructor(
Person.Builder()
.setName(event.senderName?.annotateForDebug(70))
.setIcon(bitmapLoader.getUserIcon(event.senderAvatarPath))
- .setKey(event.senderId)
+ .setKey(event.senderId.value)
.build()
}
when {
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt
index 57a3eb45aa..1b6bb8a67a 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt
@@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.ThreadId
+import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
import io.element.android.services.appnavstate.api.AppNavigationState
import io.element.android.services.appnavstate.api.currentRoomId
@@ -32,10 +33,10 @@ data class NotifiableMessageEvent(
override val eventId: EventId,
override val editedEventId: EventId?,
override val canBeReplaced: Boolean,
+ val senderId: UserId,
val noisy: Boolean,
val timestamp: Long,
val senderName: String?,
- val senderId: String?,
val body: String?,
// We cannot use Uri? type here, as that could trigger a
// NotSerializableException when persisting this to storage
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/SimpleNotifiableEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/SimpleNotifiableEvent.kt
index f252765530..4b262983d4 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/SimpleNotifiableEvent.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/SimpleNotifiableEvent.kt
@@ -30,7 +30,7 @@ data class SimpleNotifiableEvent(
val type: String?,
val timestamp: Long,
val soundName: String?,
- override var canBeReplaced: Boolean,
+ override val canBeReplaced: Boolean,
override val isRedacted: Boolean = false,
override val isUpdated: Boolean = false
) : NotifiableEvent
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt
index aa1d0032e0..c3d68e52ac 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt
@@ -100,15 +100,15 @@ class DefaultPushHandler @Inject constructor(
}
val clientSecret = pushData.clientSecret
- val userId = if (clientSecret == null) {
- // Should not happen. In this case, restore default session
- null
- } else {
- // Get userId from client secret
- pushClientSecret.getUserIdFromSecret(clientSecret)
- } ?: run {
- matrixAuthenticationService.getLatestSessionId()
- }
+ // clientSecret should not be null. If this happens, restore default session
+ val userId = clientSecret
+ ?.let {
+ // Get userId from client secret
+ pushClientSecret.getUserIdFromSecret(clientSecret)
+ }
+ ?: run {
+ matrixAuthenticationService.getLatestSessionId()
+ }
if (userId == null) {
Timber.w("Unable to get a session")
diff --git a/libraries/push/impl/src/main/res/values-ru/translations.xml b/libraries/push/impl/src/main/res/values-ru/translations.xml
new file mode 100644
index 0000000000..697a0f01d8
--- /dev/null
+++ b/libraries/push/impl/src/main/res/values-ru/translations.xml
@@ -0,0 +1,58 @@
+
+
+ "Позвонить"
+ "Прослушивание событий"
+ "Шумные уведомления"
+ "Бесшумные уведомления"
+ "** Не удалось отправить - пожалуйста, откройте комнату"
+ "Присоединиться"
+ "Отклонить"
+ "Пригласил вас в чат"
+ "Новые сообщения"
+ "Отреагировал на %1$s"
+ "Отметить как прочитанное"
+ "Пригласил вас в комнату"
+ "Я"
+ "Вы просматриваете уведомление! Нажмите на меня!"
+ "%1$s: %2$s"
+ "%1$s: %2$s %3$s"
+ "%1$s и %2$s"
+ "%1$s в %2$s"
+ "%1$s в %2$s и %3$s"
+
+ - "%1$s: %2$d сообщение"
+ - "%1$s: %2$d сообщения"
+ - "%1$s: %2$d сообщений"
+
+
+ - "%d уведомление"
+ - "%d уведомления"
+ - "%d уведомлений"
+
+
+ - "%d приглашение"
+ - "%d приглашения"
+ - "%d приглашений"
+
+
+ - "%d новое сообщение"
+ - "%d новых сообщения"
+ - "%d новых сообщений"
+
+
+ - "%d непрочитанное уведомление"
+ - "%d непрочитанных уведомления"
+ - "%d непрочитанных уведомлений"
+
+
+ - "%d комната"
+ - "%d комнаты"
+ - "%d комнат"
+
+ "Выберите способ получения уведомлений"
+ "Фоновая синхронизация"
+ "Сервисы Google"
+ "Не найдены действующие службы Google Play. Уведомления могут работать некорректно."
+ "Уведомление"
+ "Быстрый ответ"
+
diff --git a/libraries/push/impl/src/main/res/values-zh-rTW/translations.xml b/libraries/push/impl/src/main/res/values-zh-rTW/translations.xml
new file mode 100644
index 0000000000..248fae8b0b
--- /dev/null
+++ b/libraries/push/impl/src/main/res/values-zh-rTW/translations.xml
@@ -0,0 +1,33 @@
+
+
+ "通話"
+ "無聲通知"
+ "加入"
+ "拒絕"
+ "邀請您聊天"
+ "新訊息"
+ "標示為已讀"
+ "邀請您加入聊天室"
+ "我"
+ "您正在查看通知!點我!"
+
+ - "%1$s:%2$d 則訊息"
+
+
+ - "%d 個通知"
+
+
+ - "%d 個邀請"
+
+
+ - "%d 則新訊息"
+
+
+ - "%d 個聊天室"
+
+ "選擇接收通知的機制"
+ "背景同步"
+ "Google 服務"
+ "通知"
+ "快速回覆"
+
diff --git a/libraries/push/impl/src/main/res/values/dimens.xml b/libraries/push/impl/src/main/res/values/dimens.xml
deleted file mode 100644
index ce2fee2015..0000000000
--- a/libraries/push/impl/src/main/res/values/dimens.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
- 50dp
-
-
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationIdProviderTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationIdProviderTest.kt
index 57f28e72db..b9664ef577 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationIdProviderTest.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationIdProviderTest.kt
@@ -25,7 +25,7 @@ class NotificationIdProviderTest {
@Test
fun `test notification id provider`() {
val sut = NotificationIdProvider()
- val offsetForASessionId = 305410
+ val offsetForASessionId = 305_410
assertThat(sut.getSummaryNotificationId(A_SESSION_ID)).isEqualTo(offsetForASessionId + 0)
assertThat(sut.getRoomMessagesNotificationId(A_SESSION_ID)).isEqualTo(offsetForASessionId + 1)
assertThat(sut.getRoomEventNotificationId(A_SESSION_ID)).isEqualTo(offsetForASessionId + 2)
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt
index 9a998abf43..780d2abb71 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt
@@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.ThreadId
+import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
@@ -84,7 +85,7 @@ fun aNotifiableMessageEvent(
noisy = false,
timestamp = 0,
senderName = "sender-name",
- senderId = "sending-id",
+ senderId = UserId("@sending-id:domain.com"),
body = "message-body",
roomId = roomId,
threadId = threadId,
diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/PushDataFirebase.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/PushDataFirebase.kt
index 9dedf9648f..795c8bb1e8 100644
--- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/PushDataFirebase.kt
+++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/PushDataFirebase.kt
@@ -36,7 +36,7 @@ import io.element.android.libraries.pushproviders.api.PushData
data class PushDataFirebase(
val eventId: String?,
val roomId: String?,
- var unread: Int?,
+ val unread: Int?,
val clientSecret: String?
)
diff --git a/libraries/pushproviders/firebase/src/main/res/values/firebase.xml b/libraries/pushproviders/firebase/src/main/res/values/firebase.xml
index 163717db91..b73238c79d 100644
--- a/libraries/pushproviders/firebase/src/main/res/values/firebase.xml
+++ b/libraries/pushproviders/firebase/src/main/res/values/firebase.xml
@@ -1,10 +1,10 @@
-
+
912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com
https://vector-alpha.firebaseio.com
912726360885
AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c
AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c
vector-alpha.appspot.com
- vector-alpha
+ vector-alpha
diff --git a/libraries/pushproviders/unifiedpush/build.gradle.kts b/libraries/pushproviders/unifiedpush/build.gradle.kts
index abc4c0babc..a6565c25f0 100644
--- a/libraries/pushproviders/unifiedpush/build.gradle.kts
+++ b/libraries/pushproviders/unifiedpush/build.gradle.kts
@@ -16,7 +16,7 @@
plugins {
id("io.element.android-library")
alias(libs.plugins.anvil)
- kotlin("plugin.serialization") version "1.8.22"
+ kotlin("plugin.serialization") version "1.9.0"
}
android {
diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/PushDataUnifiedPush.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/PushDataUnifiedPush.kt
index f092d0167c..4485cb2c7f 100644
--- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/PushDataUnifiedPush.kt
+++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/PushDataUnifiedPush.kt
@@ -47,7 +47,7 @@ data class PushDataUnifiedPush(
data class PushDataUnifiedPushNotification(
@SerialName("event_id") val eventId: String? = null,
@SerialName("room_id") val roomId: String? = null,
- @SerialName("counts") var counts: PushDataUnifiedPushCounts? = null,
+ @SerialName("counts") val counts: PushDataUnifiedPushCounts? = null,
)
@Serializable
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 8c4c361707..3c0e0fa723 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
@@ -38,6 +38,7 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.ripple.rememberRipple
@@ -48,6 +49,7 @@ import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@@ -65,6 +67,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
@@ -73,6 +76,7 @@ import io.element.android.libraries.designsystem.VectorIcons
import io.element.android.libraries.designsystem.modifiers.applyIf
import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.text.applyScaleUp
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
@@ -109,12 +113,15 @@ fun TextComposer(
) {
AttachmentButton(onClick = onAddAttachment, modifier = Modifier.padding(vertical = 6.dp))
Spacer(modifier = Modifier.width(12.dp))
- var lineCount by remember { mutableStateOf(0) }
+ val roundCornerSmall = 20.dp.applyScaleUp()
+ val roundCornerLarge = 28.dp.applyScaleUp()
+ var lineCount by remember { mutableIntStateOf(0) }
+
val roundedCornerSize = remember(lineCount, composerMode) {
if (lineCount > 1 || composerMode is MessageComposerMode.Special) {
- 20.dp
+ roundCornerSmall
} else {
- 28.dp
+ roundCornerLarge
}
}
val roundedCornerSizeState = animateDpAsState(
@@ -124,7 +131,7 @@ fun TextComposer(
)
)
val roundedCorners = RoundedCornerShape(roundedCornerSizeState.value)
- val minHeight = 42.dp
+ val minHeight = 42.dp.applyScaleUp()
val bgColor = ElementTheme.colors.bgSubtleSecondary
// Change border color depending on focus
var hasFocus by remember { mutableStateOf(false) }
@@ -155,6 +162,9 @@ fun TextComposer(
onTextLayout = {
lineCount = it.lineCount
},
+ keyboardOptions = KeyboardOptions(
+ capitalization = KeyboardCapitalization.Sentences,
+ ),
textStyle = defaultTypography.copy(color = MaterialTheme.colorScheme.primary),
cursorBrush = SolidColor(ElementTheme.colors.iconAccentTertiary),
decorationBox = { innerTextField ->
@@ -165,7 +175,12 @@ fun TextComposer(
singleLine = false,
visualTransformation = VisualTransformation.None,
shape = roundedCorners,
- contentPadding = PaddingValues(top = 10.dp, bottom = 10.dp, start = 12.dp, end = 42.dp),
+ contentPadding = PaddingValues(
+ top = 10.dp.applyScaleUp(),
+ bottom = 10.dp.applyScaleUp(),
+ start = 12.dp.applyScaleUp(),
+ end = 42.dp.applyScaleUp(),
+ ),
interactionSource = remember { MutableInteractionSource() },
placeholder = {
Text(stringResource(CommonStrings.common_message), style = defaultTypography)
@@ -193,7 +208,7 @@ fun TextComposer(
canSendMessage = composerCanSendMessage,
onSendMessage = onSendMessage,
composerMode = composerMode,
- modifier = Modifier.padding(end = 6.dp, bottom = 6.dp)
+ modifier = Modifier.padding(end = 6.dp.applyScaleUp(), bottom = 6.dp.applyScaleUp())
)
}
}
@@ -253,7 +268,7 @@ private fun EditingModeView(
tint = ElementTheme.materialColors.secondary,
modifier = Modifier
.padding(vertical = 8.dp)
- .size(16.dp),
+ .size(16.dp.applyScaleUp()),
)
Text(
stringResource(CommonStrings.common_editing),
@@ -270,11 +285,11 @@ private fun EditingModeView(
tint = ElementTheme.materialColors.secondary,
modifier = Modifier
.padding(top = 8.dp, bottom = 8.dp, start = 16.dp, end = 12.dp)
- .size(16.dp)
+ .size(16.dp.applyScaleUp())
.clickable(
enabled = true,
onClick = onResetComposerMode,
- interactionSource = MutableInteractionSource(),
+ interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false)
),
)
@@ -333,11 +348,11 @@ private fun ReplyToModeView(
tint = MaterialTheme.colorScheme.secondary,
modifier = Modifier
.padding(end = 4.dp, top = 4.dp, start = 16.dp, bottom = 16.dp)
- .size(16.dp)
+ .size(16.dp.applyScaleUp())
.clickable(
enabled = true,
onClick = onResetComposerMode,
- interactionSource = MutableInteractionSource(),
+ interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false)
),
)
@@ -351,13 +366,13 @@ private fun AttachmentButton(
) {
Surface(
modifier
- .size(30.dp)
+ .size(30.dp.applyScaleUp())
.clickable(onClick = onClick),
shape = CircleShape,
color = ElementTheme.colors.iconPrimary
) {
Image(
- modifier = Modifier.size(12.5f.dp),
+ modifier = Modifier.size(12.5f.dp.applyScaleUp()),
painter = painterResource(R.drawable.ic_add_attachment),
contentDescription = stringResource(R.string.rich_text_editor_a11y_add_attachment),
contentScale = ContentScale.Inside,
@@ -376,15 +391,15 @@ private fun BoxScope.SendButton(
composerMode: MessageComposerMode,
modifier: Modifier = Modifier,
) {
- val interactionSource = MutableInteractionSource()
+ val interactionSource = remember { MutableInteractionSource() }
Box(
modifier = modifier
.clip(CircleShape)
.background(if (canSendMessage) ElementTheme.colors.iconAccentTertiary else Color.Transparent)
- .size(30.dp)
+ .size(30.dp.applyScaleUp())
.align(Alignment.BottomEnd)
.applyIf(composerMode !is MessageComposerMode.Edit, ifTrue = {
- padding(start = 1.dp) // Center the arrow in the circle
+ padding(start = 1.dp.applyScaleUp()) // Center the arrow in the circle
})
.clickable(
enabled = canSendMessage,
@@ -404,7 +419,7 @@ private fun BoxScope.SendButton(
else -> stringResource(CommonStrings.action_send)
}
Icon(
- modifier = Modifier.size(16.dp),
+ modifier = Modifier.size(16.dp.applyScaleUp()),
resourceId = iconId,
contentDescription = contentDescription,
// Exception here, we use Color.White instead of ElementTheme.colors.iconOnSolidPrimary
@@ -415,7 +430,7 @@ private fun BoxScope.SendButton(
@DayNightPreviews
@Composable
-fun TextComposerSimplePreview() = ElementPreview {
+internal fun TextComposerSimplePreview() = ElementPreview {
Column {
TextComposer(
onSendMessage = {},
@@ -446,7 +461,7 @@ fun TextComposerSimplePreview() = ElementPreview {
@DayNightPreviews
@Composable
-fun TextComposerEditPreview() = ElementPreview {
+internal fun TextComposerEditPreview() = ElementPreview {
TextComposer(
onSendMessage = {},
onComposerTextChange = {},
@@ -459,7 +474,7 @@ fun TextComposerEditPreview() = ElementPreview {
@DayNightPreviews
@Composable
-fun TextComposerReplyPreview() = ElementPreview {
+internal fun TextComposerReplyPreview() = ElementPreview {
Column {
TextComposer(
onSendMessage = {},
diff --git a/libraries/textcomposer/src/main/res/values-ru/translations.xml b/libraries/textcomposer/src/main/res/values-ru/translations.xml
new file mode 100644
index 0000000000..9f7324f086
--- /dev/null
+++ b/libraries/textcomposer/src/main/res/values-ru/translations.xml
@@ -0,0 +1,18 @@
+
+
+ "Прикрепить файл"
+ "Переключить список маркеров"
+ "Переключить блок кода"
+ "Сообщение"
+ "Применить жирный шрифт"
+ "Применить курсивный формат"
+ "Применить формат зачеркивания"
+ "Применить формат подчеркивания"
+ "Переключение полноэкранного режима"
+ "Отступ"
+ "Применить встроенный формат кода"
+ "Установить ссылку"
+ "Переключить нумерованный список"
+ "Переключить цитату"
+ "Без отступа"
+
diff --git a/libraries/textcomposer/src/main/res/values-zh-rTW/translations.xml b/libraries/textcomposer/src/main/res/values-zh-rTW/translations.xml
new file mode 100644
index 0000000000..93777d4ca5
--- /dev/null
+++ b/libraries/textcomposer/src/main/res/values-zh-rTW/translations.xml
@@ -0,0 +1,17 @@
+
+
+ "新增附件"
+ "切換項目編號"
+ "切換程式碼區塊"
+ "訊息"
+ "套用粗體"
+ "套用斜體"
+ "套用刪除線"
+ "套用底線"
+ "切換全螢幕模式"
+ "增加縮排"
+ "設定連結"
+ "切換數字編號"
+ "切換引用"
+ "減少縮排"
+
diff --git a/libraries/theme/build.gradle.kts b/libraries/theme/build.gradle.kts
index 9488565c80..0c5c0b9548 100644
--- a/libraries/theme/build.gradle.kts
+++ b/libraries/theme/build.gradle.kts
@@ -23,6 +23,8 @@ android {
namespace = "io.element.android.libraries.theme"
dependencies {
+ api(libs.androidx.compose.material3)
+
ksp(libs.showkase.processor)
kspTest(libs.showkase.processor)
diff --git a/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/ElementTheme.kt b/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/ElementTheme.kt
index f273c2dd64..ae73f72f97 100644
--- a/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/ElementTheme.kt
+++ b/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/ElementTheme.kt
@@ -68,6 +68,14 @@ object ElementTheme {
*/
val typography: TypographyTokens = TypographyTokens
+ /**
+ * Material 3 [Typography] tokens. In Figma, these have the `M3 Typography/` prefix.
+ */
+ val materialTypography: Typography
+ @Composable
+ @ReadOnlyComposable
+ get() = MaterialTheme.typography
+
/**
* Returns whether the theme version used is the light or the dark one.
*/
diff --git a/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/LegacyColors.kt b/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/LegacyColors.kt
index b797dab86a..2e705c8c79 100644
--- a/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/LegacyColors.kt
+++ b/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/LegacyColors.kt
@@ -17,6 +17,8 @@
package io.element.android.libraries.theme
import androidx.compose.ui.graphics.Color
+import io.element.android.libraries.theme.compound.generated.internal.DarkDesignTokens
+import io.element.android.libraries.theme.compound.generated.internal.LightDesignTokens
// =================================================================================================
// IMPORTANT!
@@ -26,3 +28,6 @@ import androidx.compose.ui.graphics.Color
// =================================================================================================
val LinkColor = Color(0xFF0086E6)
+
+val SnackBarLabelColorLight = LightDesignTokens.colorGray700
+val SnackBarLabelColorDark = DarkDesignTokens.colorGray700
diff --git a/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/MaterialThemeColors.kt b/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/MaterialThemeColors.kt
index d211869f71..3d359594e1 100644
--- a/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/MaterialThemeColors.kt
+++ b/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/MaterialThemeColors.kt
@@ -91,7 +91,7 @@ internal val materialColorSchemeDark = darkColorScheme(
@Preview
@Composable
-fun ColorsSchemePreviewLight() = ColorsSchemePreview(
+internal fun ColorsSchemePreviewLight() = ColorsSchemePreview(
Color.Black,
Color.White,
materialColorSchemeLight,
@@ -99,7 +99,7 @@ fun ColorsSchemePreviewLight() = ColorsSchemePreview(
@Preview
@Composable
-fun ColorsSchemePreviewDark() = ColorsSchemePreview(
+internal fun ColorsSchemePreviewDark() = ColorsSchemePreview(
Color.White,
Color.Black,
materialColorSchemeDark,
diff --git a/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/compound/CompoundTypography.kt b/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/compound/CompoundTypography.kt
index fe72a5effe..2da86f7882 100644
--- a/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/compound/CompoundTypography.kt
+++ b/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/compound/CompoundTypography.kt
@@ -17,9 +17,11 @@
package io.element.android.libraries.theme.compound
import androidx.compose.material3.Typography
+import androidx.compose.ui.text.PlatformTextStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
import io.element.android.libraries.theme.compound.generated.TypographyTokens
@@ -41,6 +43,8 @@ internal val defaultHeadlineSmall = TextStyle(
lineHeight = 32.sp,
fontSize = 24.sp,
letterSpacing = 0.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
)
// 22px (Material) vs 20px, it's the closest one
diff --git a/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/compound/generated/TypographyTokens.kt b/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/compound/generated/TypographyTokens.kt
index 00a0b82fd7..68ff1d2e03 100644
--- a/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/compound/generated/TypographyTokens.kt
+++ b/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/compound/generated/TypographyTokens.kt
@@ -1,21 +1,7 @@
-/*
- * 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.
- */
+
// Do not edit directly
-// Generated on Tue, 27 Jun 2023 13:31:52 GMT
+// Generated on Fri, 28 Jul 2023 10:11:16 GMT
@@ -27,6 +13,8 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
+import androidx.compose.ui.text.PlatformTextStyle
+import androidx.compose.ui.text.style.LineHeightStyle
object TypographyTokens {
val fontBodyLgMedium = TextStyle(
@@ -35,6 +23,8 @@ object TypographyTokens {
lineHeight = 22.sp,
fontSize = 16.sp,
letterSpacing = 0.015629999999999998.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
)
val fontBodyLgRegular = TextStyle(
fontFamily = FontFamily.Default,
@@ -42,6 +32,8 @@ object TypographyTokens {
lineHeight = 22.sp,
fontSize = 16.sp,
letterSpacing = 0.015629999999999998.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
)
val fontBodyMdMedium = TextStyle(
fontFamily = FontFamily.Default,
@@ -49,6 +41,8 @@ object TypographyTokens {
lineHeight = 20.sp,
fontSize = 14.sp,
letterSpacing = 0.01786.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
)
val fontBodyMdRegular = TextStyle(
fontFamily = FontFamily.Default,
@@ -56,6 +50,8 @@ object TypographyTokens {
lineHeight = 20.sp,
fontSize = 14.sp,
letterSpacing = 0.01786.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
)
val fontBodySmMedium = TextStyle(
fontFamily = FontFamily.Default,
@@ -63,6 +59,8 @@ object TypographyTokens {
lineHeight = 17.sp,
fontSize = 12.sp,
letterSpacing = 0.03333.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
)
val fontBodySmRegular = TextStyle(
fontFamily = FontFamily.Default,
@@ -70,6 +68,8 @@ object TypographyTokens {
lineHeight = 17.sp,
fontSize = 12.sp,
letterSpacing = 0.03333.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
)
val fontBodyXsMedium = TextStyle(
fontFamily = FontFamily.Default,
@@ -77,6 +77,8 @@ object TypographyTokens {
lineHeight = 15.sp,
fontSize = 11.sp,
letterSpacing = 0.04545.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
)
val fontBodyXsRegular = TextStyle(
fontFamily = FontFamily.Default,
@@ -84,6 +86,8 @@ object TypographyTokens {
lineHeight = 15.sp,
fontSize = 11.sp,
letterSpacing = 0.04545.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
)
val fontHeadingLgBold = TextStyle(
fontFamily = FontFamily.Default,
@@ -91,6 +95,8 @@ object TypographyTokens {
lineHeight = 34.sp,
fontSize = 28.sp,
letterSpacing = 0.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
)
val fontHeadingLgRegular = TextStyle(
fontFamily = FontFamily.Default,
@@ -98,6 +104,8 @@ object TypographyTokens {
lineHeight = 34.sp,
fontSize = 28.sp,
letterSpacing = 0.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
)
val fontHeadingMdBold = TextStyle(
fontFamily = FontFamily.Default,
@@ -105,6 +113,8 @@ object TypographyTokens {
lineHeight = 27.sp,
fontSize = 22.sp,
letterSpacing = 0.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
)
val fontHeadingMdRegular = TextStyle(
fontFamily = FontFamily.Default,
@@ -112,6 +122,8 @@ object TypographyTokens {
lineHeight = 27.sp,
fontSize = 22.sp,
letterSpacing = 0.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
)
val fontHeadingSmMedium = TextStyle(
fontFamily = FontFamily.Default,
@@ -119,6 +131,8 @@ object TypographyTokens {
lineHeight = 25.sp,
fontSize = 20.sp,
letterSpacing = 0.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
)
val fontHeadingSmRegular = TextStyle(
fontFamily = FontFamily.Default,
@@ -126,6 +140,8 @@ object TypographyTokens {
lineHeight = 25.sp,
fontSize = 20.sp,
letterSpacing = 0.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
)
val fontHeadingXlBold = TextStyle(
fontFamily = FontFamily.Default,
@@ -133,6 +149,8 @@ object TypographyTokens {
lineHeight = 41.sp,
fontSize = 34.sp,
letterSpacing = 0.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
)
val fontHeadingXlRegular = TextStyle(
fontFamily = FontFamily.Default,
@@ -140,5 +158,7 @@ object TypographyTokens {
lineHeight = 41.sp,
fontSize = 34.sp,
letterSpacing = 0.em,
+ platformStyle = PlatformTextStyle(includeFontPadding = false),
+ lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None)
)
}
diff --git a/libraries/theme/src/main/res/drawable/ic_chat.xml b/libraries/theme/src/main/res/drawable/ic_chat.xml
deleted file mode 100644
index 1fef824a1d..0000000000
--- a/libraries/theme/src/main/res/drawable/ic_chat.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/libraries/theme/src/main/res/drawable/ic_check.xml b/libraries/theme/src/main/res/drawable/ic_check.xml
deleted file mode 100644
index e92733095b..0000000000
--- a/libraries/theme/src/main/res/drawable/ic_check.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/libraries/theme/src/main/res/drawable/ic_check_circle.xml b/libraries/theme/src/main/res/drawable/ic_check_circle.xml
deleted file mode 100644
index ad3aacbe28..0000000000
--- a/libraries/theme/src/main/res/drawable/ic_check_circle.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/libraries/theme/src/main/res/drawable/ic_chevron.xml b/libraries/theme/src/main/res/drawable/ic_chevron.xml
deleted file mode 100644
index 4ecd3f16b0..0000000000
--- a/libraries/theme/src/main/res/drawable/ic_chevron.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/libraries/theme/src/main/res/drawable/ic_close.xml b/libraries/theme/src/main/res/drawable/ic_close.xml
deleted file mode 100644
index f334767b67..0000000000
--- a/libraries/theme/src/main/res/drawable/ic_close.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/libraries/theme/src/main/res/drawable/ic_computer.xml b/libraries/theme/src/main/res/drawable/ic_computer.xml
deleted file mode 100644
index e2748c2d4a..0000000000
--- a/libraries/theme/src/main/res/drawable/ic_computer.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/libraries/theme/src/main/res/drawable/ic_delete.xml b/libraries/theme/src/main/res/drawable/ic_delete.xml
deleted file mode 100644
index 413a570210..0000000000
--- a/libraries/theme/src/main/res/drawable/ic_delete.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/libraries/theme/src/main/res/drawable/ic_error.xml b/libraries/theme/src/main/res/drawable/ic_error.xml
deleted file mode 100644
index d978824039..0000000000
--- a/libraries/theme/src/main/res/drawable/ic_error.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/libraries/theme/src/main/res/drawable/ic_info.xml b/libraries/theme/src/main/res/drawable/ic_info.xml
deleted file mode 100644
index 69865e325a..0000000000
--- a/libraries/theme/src/main/res/drawable/ic_info.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/libraries/theme/src/main/res/drawable/ic_lock.xml b/libraries/theme/src/main/res/drawable/ic_lock.xml
deleted file mode 100644
index 2ada59e82f..0000000000
--- a/libraries/theme/src/main/res/drawable/ic_lock.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/libraries/theme/src/main/res/drawable/ic_mobile.xml b/libraries/theme/src/main/res/drawable/ic_mobile.xml
deleted file mode 100644
index f2c46be357..0000000000
--- a/libraries/theme/src/main/res/drawable/ic_mobile.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/libraries/theme/src/main/res/drawable/ic_thread.xml b/libraries/theme/src/main/res/drawable/ic_thread.xml
deleted file mode 100644
index d3293fab5a..0000000000
--- a/libraries/theme/src/main/res/drawable/ic_thread.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
diff --git a/libraries/theme/src/main/res/drawable/ic_user.xml b/libraries/theme/src/main/res/drawable/ic_user.xml
deleted file mode 100644
index 5f61985ce4..0000000000
--- a/libraries/theme/src/main/res/drawable/ic_user.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
diff --git a/libraries/theme/src/main/res/drawable/ic_visibility_invisible.xml b/libraries/theme/src/main/res/drawable/ic_visibility_invisible.xml
deleted file mode 100644
index 3f20783ee4..0000000000
--- a/libraries/theme/src/main/res/drawable/ic_visibility_invisible.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/libraries/theme/src/main/res/drawable/ic_visibility_visible.xml b/libraries/theme/src/main/res/drawable/ic_visibility_visible.xml
deleted file mode 100644
index 1283a1512b..0000000000
--- a/libraries/theme/src/main/res/drawable/ic_visibility_visible.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/libraries/theme/src/main/res/drawable/ic_web_browser.xml b/libraries/theme/src/main/res/drawable/ic_web_browser.xml
deleted file mode 100644
index 080ce75905..0000000000
--- a/libraries/theme/src/main/res/drawable/ic_web_browser.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/libraries/ui-strings/src/main/res/values-de/translations.xml b/libraries/ui-strings/src/main/res/values-de/translations.xml
index 10694181da..0f5259e663 100644
--- a/libraries/ui-strings/src/main/res/values-de/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-de/translations.xml
@@ -94,7 +94,7 @@
"Passwort"
"Personen"
"Permalink"
- "Datenschutzerklärung"
+ "Datenschutzerklärung"
"Reaktionen"
"Aktualisiere…"
"Auf %1$s antworten"
@@ -145,7 +145,7 @@
"%1$s konnte nicht auf deinen Standort zugreifen. Bitte versuche es später erneut."
"Einige Nachrichten wurden nicht gesendet"
"Entschuldigung, ein Fehler ist aufgetreten."
- "🔐️ Besuchen Sie mich auf %1$s"
+ "🔐️ Besuche mich auf %1$s"
"Hey, sprich mit mir auf %1$s: %2$s"
"Bist du sicher, dass du diesen Raum verlassen willst? Du bist die einzige Person hier. Wenn du gehst, kann in Zukunft niemand mehr beitreten, auch du nicht."
"Bist du dir sicher, dass du den Raum verlassen möchtest? Dieser Raum ist nicht öffentlich und du kannst ihm ohne eine Einladung nicht mehr beitreten."
@@ -164,10 +164,10 @@
"Neu"
"Teile Analyse-Daten"
"Medienauswahl fehlgeschlagen, bitte versuche es erneut."
- "Fehler bei der Verarbeitung von Medien zum Hochladen, bitte versuchen Sie es erneut."
+ "Fehler bei der Verarbeitung von Medien zum Hochladen, bitte versuche es erneut."
"Hochladen von Medien fehlgeschlagen, bitte versuchen Sie es erneut."
"Dies ist ein einmaliger Vorgang, danke fürs Warten."
- "Deinen Account einrichten"
+ "Dein Konto einrichten"
"Prüfe, ob du alle aktuellen und zukünftigen Nachrichten dieses Benutzers ausblenden möchtest"
"Standort teilen"
"Meinen Standort teilen"
@@ -184,7 +184,7 @@
"Fehler"
"Erfolg"
"Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."
- "Sie können alle unsere Nutzerbedingungen %1$s lesen."
+ "Du kannst alle unsere Nutzerbedingungen %1$s lesen."
"hier"
"Nutzer blockieren"
diff --git a/libraries/ui-strings/src/main/res/values-ru/translations.xml b/libraries/ui-strings/src/main/res/values-ru/translations.xml
new file mode 100644
index 0000000000..d4830e9127
--- /dev/null
+++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml
@@ -0,0 +1,213 @@
+
+
+ "Скрыть пароль"
+ "Отправить файлы"
+ "Показать пароль"
+ "Меню пользователя"
+ "Разрешить"
+ "Назад"
+ "Отмена"
+ "Выбрать фото"
+ "Очистить"
+ "Закрыть"
+ "Полная проверка"
+ "Подтвердить"
+ "Продолжить"
+ "Копировать"
+ "Скопировать ссылку"
+ "Скопировать ссылку в сообщение"
+ "Создать"
+ "Создать комнату"
+ "Отклонить"
+ "Отключить"
+ "Готово"
+ "Редактировать"
+ "Включить"
+ "Забыли пароль?"
+ "Переслать"
+ "Пригласить"
+ "Пригласить друзей"
+ "Пригласить друзей в %1$s"
+ "Пригласите пользователей в %1$s"
+ "Приглашения"
+ "Подробнее"
+ "Выйти"
+ "Покинуть комнату"
+ "Далее"
+ "Нет"
+ "Не сейчас"
+ "Ок"
+ "Открыть с помощью"
+ "Быстрый ответ"
+ "Цитата"
+ "Реакция"
+ "Удалить"
+ "Ответить"
+ "Сообщить об ошибке"
+ "Пожаловаться на содержание"
+ "Повторить"
+ "Повторите расшифровку"
+ "Сохранить"
+ "Поиск"
+ "Отправить"
+ "Отправить сообщение"
+ "Поделиться"
+ "Поделиться ссылкой"
+ "Пропустить"
+ "Начать"
+ "Начать чат "
+ "Начать подтверждение"
+ "Нажмите, чтобы загрузить карту"
+ "Сделать фото"
+ "Показать источник"
+ "Да"
+ "О приложении"
+ "Политика допустимого использования"
+ "Аналитика"
+ "Аудио"
+ "Пузыри"
+ "Авторское право"
+ "Создание комнаты…"
+ "Покинул комнату"
+ "Ошибка расшифровки"
+ "Для разработчика"
+ "(изменено)"
+ "Редактирование"
+ "%1$s%2$s"
+ "Шифрование включено"
+ "Ошибка"
+ "Файл"
+ "Файл сохранен в «Загрузки»"
+ "Переслать сообщение"
+ "GIF"
+ "Изображения"
+ "Идентификатор Matrix ID не найден, приглашение может быть не получено."
+ "Покинуть комнату"
+ "Ссылка скопирована в буфер обмена"
+ "Загрузка…"
+ "Сообщение"
+ "Оформление сообщений"
+ "Сообщение удалено"
+ "Современный"
+ "Без звука"
+ "Ничего не найдено"
+ "Не в сети"
+ "Пароль"
+ "Пользователи"
+ "Постоянная ссылка"
+ "Политика конфиденциальности"
+ "Реакции"
+ "Обновление…"
+ "Отвечает на %1$s"
+ "Сообщить об ошибке"
+ "Отчет отправлен"
+ "Название комнаты"
+ "например, название вашего проекта"
+ "Поиск человека"
+ "Результаты поиска"
+ "Безопасность"
+ "Выберите свой сервер"
+ "Отправка…"
+ "Сервер не поддерживается"
+ "Адрес сервера"
+ "Настройки"
+ "Делится местонахождением"
+ "Начало чата…"
+ "Стикер"
+ "Успешно"
+ "Рекомендации"
+ "Синхронизация"
+ "Уведомление о третьей стороне"
+ "Тема"
+ "О чем эта комната?"
+ "Невозможно расшифровать"
+ "Не удалось отправить приглашения одному или нескольким пользователям."
+ "Не удалось отправить приглашение(я)"
+ "Включить звук"
+ "Неподдерживаемое событие"
+ "Имя пользователя"
+ "Проверка отменена"
+ "Проверка завершена"
+ "Видео"
+ "Ожидание…"
+ "Подтверждение"
+ "Предупреждение"
+ "Деятельность"
+ "Флаги"
+ "Еда и напитки"
+ "Животные и природа"
+ "Объекты"
+ "Смайлы и люди"
+ "Путешествия и места"
+ "Символы"
+ "Не удалось создать постоянную ссылку"
+ "Не удалось загрузить карту %1$s. Пожалуйста, повторите попытку позже."
+ "Не удалось загрузить сообщения"
+ "%1$s не удалось получить доступ к вашему местоположению. Пожалуйста, повторите попытку позже."
+ "У %1$s нет разрешения на доступ к вашему местоположению. Вы можете разрешить доступ в Настройках."
+ "У %1$s нет разрешения на доступ к вашему местоположению. Разрешите доступ ниже."
+ "Некоторые сообщения не были отправлены"
+ "Извините, произошла ошибка"
+ "🔐️ Присоединяйтесь ко мне в %1$s"
+ "Привет, поговори со мной по %1$s: %2$s"
+ "Вы уверены, что хотите покинуть эту комнату? Вы здесь единственный человек. Если вы уйдете, никто не сможет присоединиться в будущем, включая вас."
+ "Вы уверены, что хотите покинуть эту комнату? Эта комната не является публичной, и Вы не сможете присоединиться к ней без приглашения."
+ "Вы уверены, что хотите покинуть комнату?"
+ "%1$s Android"
+
+ - "%1$d участник"
+ - "%1$d участников"
+ - "%1$d участников"
+
+
+ - "%d голос"
+ - "%d голоса"
+ - "%d голосов"
+
+ "Rageshake сообщит об ошибке"
+ "Кажется, вы трясли телефон. Хотите открыть экран отчета об ошибке?"
+ "Это сообщение будет передано администратору вашего домашнего сервера. Они не смогут прочитать зашифрованные сообщения."
+ "Причина, по которой вы пожаловались на этот контент"
+ "Это начало %1$s."
+ "Это начало разговора."
+ "Новый"
+ "Делитесь данными аналитики"
+ "Не удалось выбрать носитель, попробуйте еще раз."
+ "Не удалось обработать медиафайл для загрузки, попробуйте еще раз."
+ "Не удалось загрузить медиафайлы, попробуйте еще раз."
+ "Это одноразовый процесс, спасибо, что подождали."
+ "Настройка учетной записи."
+ "Дополнительные параметры"
+ "Аудио и видео звонки"
+ "Прямые чаты"
+ "Включить уведомления на данном устройстве"
+ "Групповые чаты"
+ "Упоминания"
+ "Все"
+ "Упоминания"
+ "Уведомить меня"
+ "Уведомить меня в @room"
+ "Чтобы получать уведомления, измените свой %1$s."
+ "Настройки системы"
+ "Системные уведомления выключены"
+ "Уведомления"
+ "Отметьте, хотите ли вы скрыть все текущие и будущие сообщения от этого пользователя"
+ "Поделиться местоположением"
+ "Поделиться моим местоположением"
+ "Открыть в Apple Maps"
+ "Открыть в Google Картах"
+ "Открыть в OpenStreetMap"
+ "Поделиться этим местоположением"
+ "Местоположение"
+ "Rageshake"
+ "Порог обнаружения"
+ "Основные"
+ "Версия: %1$s (%2$s)"
+ "en"
+ "Ошибка"
+ "Успешно"
+ "Предоставлять анонимные данные об использовании, чтобы помочь нам выявить проблемы."
+ "Вы можете ознакомиться со всеми нашими условиями %1$s."
+ "здесь"
+ "Заблокировать пользователя"
+
diff --git a/libraries/ui-strings/src/main/res/values-sk/translations.xml b/libraries/ui-strings/src/main/res/values-sk/translations.xml
index 726d1b1505..14dd5626c8 100644
--- a/libraries/ui-strings/src/main/res/values-sk/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml
@@ -40,6 +40,7 @@
"Otvoriť pomocou"
"Rýchla odpoveď"
"Citovať"
+ "Reagovať"
"Odstrániť"
"Odpovedať"
"Nahlásiť chybu"
@@ -94,6 +95,9 @@
"Heslo"
"Ľudia"
"Trvalý odkaz"
+ "Výsledné hlasovanie: %1$s"
+ "Celkový počet hlasov: %1$s"
+ "Výsledky sa zobrazia po ukončení ankety"
"Zásady ochrany osobných údajov"
"Reakcie"
"Obnovuje sa…"
@@ -143,8 +147,8 @@
"%1$s nedokázal načítať mapu. Skúste to prosím neskôr."
"Načítanie správ zlyhalo"
"%1$s nemohol získať prístup k vašej polohe. Skúste to prosím neskôr."
- "Ak chcete odoslať polohu, povoľte %1$s prístup k vašej polohe z obrazovky nastavení."
- "Ak chcete odoslať polohu, povoľte %1$s prístup k vašej polohe v nasledujúcom dialógovom okne."
+ "%1$s nemá povolenie na prístup k vašej polohe. Prístup môžete zapnúť v Nastaveniach."
+ "%1$s nemá povolenie na prístup k vašej polohe. Povoľte prístup nižšie."
"Niektoré správy neboli odoslané"
"Prepáčte, vyskytla sa chyba"
"🔐️ Pripojte sa ku mne na %1$s"
@@ -158,6 +162,11 @@
- "%1$d členovia"
- "%1$d členov"
+
+ - "1 hlas"
+ - "%d hlasy"
+ - "%d hlasov"
+
"Zúrivo potriasť pre nahlásenie chyby"
"Zdá sa, že zúrivo trasiete telefónom. Chcete otvoriť obrazovku s nahlásením chýb?"
"Táto správa bude nahlásená správcovi vášho domovského servera. Nebude môcť prečítať žiadne šifrované správy."
@@ -171,7 +180,18 @@
"Nepodarilo sa nahrať médiá, skúste to prosím znova."
"Ide o jednorazový proces, ďakujeme za trpezlivosť."
"Nastavenie vášho účtu."
+ "Ďalšie nastavenia"
+ "Audio a video hovory"
+ "Priame konverzácie"
+ "Pri priamych rozhovoroch ma upozorniť na"
+ "Pri skupinových rozhovoroch ma upozorniť na"
"Povoliť oznámenia na tomto zariadení"
+ "Skupinové rozhovory"
+ "Zmienky"
+ "Všetky"
+ "Zmienky"
+ "Upozorniť ma na"
+ "Upozorniť ma na @miestnosť"
"Ak chcete dostávať oznámenia, zmeňte prosím svoje %1$s."
"nastavenia systému"
"Systémové oznámenia sú vypnuté"
diff --git a/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml b/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml
new file mode 100644
index 0000000000..701a6243ac
--- /dev/null
+++ b/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml
@@ -0,0 +1,169 @@
+
+
+ "隱藏密碼"
+ "傳送檔案"
+ "顯示密碼"
+ "使用者選單"
+ "接受"
+ "返回"
+ "取消"
+ "選擇照片"
+ "清除"
+ "關閉"
+ "完成驗證"
+ "確認"
+ "繼續"
+ "複製"
+ "複製連結"
+ "建立"
+ "建立聊天室"
+ "停用"
+ "完成"
+ "編輯"
+ "啟用"
+ "忘記密碼?"
+ "轉寄"
+ "邀請"
+ "邀請朋友"
+ "邀請朋友使用%1$s"
+ "邀請夥伴使用%1$s"
+ "邀請"
+ "了解更多"
+ "離開"
+ "離開聊天室"
+ "下一個"
+ "否"
+ "以後再說"
+ "OK"
+ "用其他方式開啟"
+ "快速回覆"
+ "引用"
+ "回應"
+ "移除"
+ "回覆"
+ "檢舉內容"
+ "再試一次"
+ "再次嘗試解密"
+ "儲存"
+ "搜尋"
+ "傳送"
+ "傳送訊息"
+ "分享"
+ "分享連結"
+ "跳過"
+ "開始"
+ "開始聊天"
+ "開始驗證"
+ "點擊以載入地圖"
+ "拍照"
+ "檢視原始碼"
+ "是"
+ "關於"
+ "分析"
+ "音訊"
+ "著作權"
+ "正在建立聊天室…"
+ "離開聊天室"
+ "解密錯誤"
+ "開發者選項"
+ "(已編輯)"
+ "編輯中"
+ "已啟用加密"
+ "錯誤"
+ "檔案"
+ "檔案已儲存至 Downloads"
+ "訊息轉寄"
+ "GIF"
+ "圖片"
+ "找不到此 Matrix ID,因此可能沒有人會收到邀請。"
+ "正在離開聊天室"
+ "連結已複製到剪貼簿"
+ "載入中…"
+ "訊息"
+ "訊息布局"
+ "訊息已移除"
+ "現代"
+ "關閉通知"
+ "查無結果"
+ "離線"
+ "密碼"
+ "夥伴"
+ "永久連結"
+ "結果將在投票結束後公佈"
+ "隱私權政策"
+ "回應"
+ "重新整理…"
+ "正在回覆%1$s"
+ "聊天室名稱"
+ "範例:您的計畫名稱"
+ "搜尋結果"
+ "選擇您的伺服器"
+ "傳送中…"
+ "伺服器 URL"
+ "設定"
+ "貼圖"
+ "成功"
+ "建議"
+ "同步中"
+ "主題"
+ "無法解密"
+ "無法發送邀請給一或多個使用者。"
+ "無法發送邀請"
+ "開啟通知"
+ "使用者名稱"
+ "驗證已取消"
+ "驗證完成"
+ "影片"
+ "等待中…"
+ "確認"
+ "警告"
+ "活動"
+ "旗幟"
+ "食物與飲料"
+ "動物與大自然"
+ "物品"
+ "表情與人物"
+ "旅行與景點"
+ "標誌"
+ "無法建立永久連結"
+ "%1$s無法載入地圖。請稍後再試。"
+ "無法載入訊息"
+ "%1$s無法取得您的位置。請稍後再試。"
+ "有些訊息尚未傳送"
+ "您確定要離開聊天室嗎?這裡只有您一個人。如果您離開了,包含您在內的所有人都無法再進入此聊天室。"
+ "您確定要離開聊天室嗎?此聊天室不是公開的,如果沒有收到邀請,您無法重新加入。"
+ "您確定要離開聊天室嗎?"
+ "%1$s Android"
+
+ - "%1$d 位成員"
+
+
+ - "%d 票"
+
+ "檢舉這個內容的原因"
+ "新訊息"
+ "無法上傳媒體檔案,請稍後再試。"
+ "設定您的帳號"
+ "其他設定"
+ "私訊"
+ "在這個裝置上開啟通知"
+ "群組聊天"
+ "提及"
+ "提及"
+ "系統設定"
+ "已關閉系統通知"
+ "通知"
+ "分享位置"
+ "分享我的位置"
+ "在 Apple 地圖中開啟"
+ "在 Google 地圖中開啟"
+ "在開放街圖(OpenStreetMap) 中開啟"
+ "分享這個位置"
+ "位置"
+ "一般"
+ "版本:%1$s(%2$s)"
+ "zh-tw"
+ "錯誤"
+ "成功"
+ "封鎖使用者"
+
diff --git a/libraries/ui-strings/src/main/res/values/donottranslate.xml b/libraries/ui-strings/src/main/res/values/donottranslate.xml
deleted file mode 100755
index 910ce31c41..0000000000
--- a/libraries/ui-strings/src/main/res/values/donottranslate.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
- …
- –
-
-
- Not implemented yet in ${app_name}
-
-
- Cut the slack from teams.
-
- Crash the application.
-
-
- © MapTiler © OpenStreetMap contributors
-
diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml
index b17d8f337f..3b4c305ffc 100644
--- a/libraries/ui-strings/src/main/res/values/localazy.xml
+++ b/libraries/ui-strings/src/main/res/values/localazy.xml
@@ -40,6 +40,7 @@
"Open with"
"Quick reply"
"Quote"
+ "React"
"Remove"
"Reply"
"Report bug"
@@ -94,6 +95,9 @@
"Password"
"People"
"Permalink"
+ "Final votes: %1$s"
+ "Total votes: %1$s"
+ "Results will show after the poll has ended"
"Privacy policy"
"Reactions"
"Refreshing…"
@@ -157,6 +161,10 @@
- "%1$d member"
- "%1$d members"
+
+ - "%d vote"
+ - "%d votes"
+
"Rageshake to report bug"
"You seem to be shaking the phone in frustration. Would you like to open the bug report screen?"
"This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages."
@@ -170,7 +178,19 @@
"Failed uploading media, please try again."
"This is a one time process, thanks for waiting."
"Setting up your account."
+ "Additional settings"
+ "Audio and video calls"
+ "Direct chats"
+ "An error occurred while updating the notification setting."
+ "On direct chats, notify me for"
+ "On group chats, notify me for"
"Enable notifications on this device"
+ "Group chats"
+ "Mentions"
+ "All"
+ "Mentions"
+ "Notify me for"
+ "Notify me on @room"
"To receive notifications, please change your %1$s."
"system settings"
"System notifications turned off"
diff --git a/plugins/settings.gradle.kts b/plugins/settings.gradle.kts
index defcb6f17b..7e2ce1ea50 100644
--- a/plugins/settings.gradle.kts
+++ b/plugins/settings.gradle.kts
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+rootProject.name = "ElementX_plugins"
+
dependencyResolutionManagement {
repositories {
mavenCentral()
diff --git a/plugins/src/main/kotlin/Versions.kt b/plugins/src/main/kotlin/Versions.kt
index 2e89afa70f..658603cb56 100644
--- a/plugins/src/main/kotlin/Versions.kt
+++ b/plugins/src/main/kotlin/Versions.kt
@@ -56,12 +56,12 @@ private const val versionMinor = 1
// Note: even values are reserved for regular release, odd values for hotfix release.
// When creating a hotfix, you should decrease the value, since the current value
// is the value for the next regular release.
-private const val versionPatch = 2
+private const val versionPatch = 4
object Versions {
val versionCode = 4_000_000 + versionMajor * 1_00_00 + versionMinor * 1_00 + versionPatch
val versionName = "$versionMajor.$versionMinor.$versionPatch"
- const val compileSdk = 33
+ const val compileSdk = 34
const val targetSdk = 33
const val minSdk = 23
val javaCompileVersion = JavaVersion.VERSION_17
diff --git a/plugins/src/main/kotlin/extension/CommonExtension.kt b/plugins/src/main/kotlin/extension/CommonExtension.kt
index 6b9523d437..e3f7b3682e 100644
--- a/plugins/src/main/kotlin/extension/CommonExtension.kt
+++ b/plugins/src/main/kotlin/extension/CommonExtension.kt
@@ -45,6 +45,7 @@ fun CommonExtension<*, *, *, *, *>.androidConfig(project: Project) {
checkDependencies = true
abortOnError = true
ignoreTestFixturesSources = true
+ checkGeneratedSources = false
}
}
@@ -71,6 +72,7 @@ fun CommonExtension<*, *, *, *, *>.composeConfig(libs: LibrariesForLibs) {
// error.add("ComposableLambdaParameterNaming")
error.add("ComposableLambdaParameterPosition")
ignoreTestFixturesSources = true
+ checkGeneratedSources = false
}
}
diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt
index 21d6648a41..a915e70046 100644
--- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt
+++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt
@@ -26,11 +26,12 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.core.view.WindowCompat
-import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
+import io.element.android.libraries.matrix.impl.RustMatrixClientFactory
import io.element.android.libraries.matrix.impl.auth.RustMatrixAuthenticationService
import io.element.android.libraries.network.useragent.SimpleUserAgentProvider
import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore
+import io.element.android.libraries.theme.ElementTheme
import io.element.android.services.toolbox.impl.systemclock.DefaultSystemClock
import kotlinx.coroutines.runBlocking
import java.io.File
@@ -39,15 +40,22 @@ class MainActivity : ComponentActivity() {
private val matrixAuthenticationService: MatrixAuthenticationService by lazy {
val baseDirectory = File(applicationContext.filesDir, "sessions")
-
+ val userAgentProvider = SimpleUserAgentProvider("MinimalSample")
+ val sessionStore = InMemorySessionStore()
RustMatrixAuthenticationService(
- context = applicationContext,
baseDirectory = baseDirectory,
- appCoroutineScope = Singleton.appScope,
coroutineDispatchers = Singleton.coroutineDispatchers,
- sessionStore = InMemorySessionStore(),
- clock = DefaultSystemClock(),
- userAgentProvider = SimpleUserAgentProvider("MinimalSample")
+ sessionStore = sessionStore,
+ userAgentProvider = userAgentProvider,
+ rustMatrixClientFactory = RustMatrixClientFactory(
+ context = applicationContext,
+ baseDirectory = baseDirectory,
+ appCoroutineScope = Singleton.appScope,
+ coroutineDispatchers = Singleton.coroutineDispatchers,
+ sessionStore = sessionStore,
+ userAgentProvider = userAgentProvider,
+ clock = DefaultSystemClock()
+ )
)
}
diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt
index 41dbc8bfe2..faaccc9b8e 100644
--- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt
+++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt
@@ -68,7 +68,7 @@ class RoomListScreen(
inviteStateDataSource = DefaultInviteStateDataSource(matrixClient, DefaultSeenInvitesStore(context), coroutineDispatchers),
leaveRoomPresenter = LeaveRoomPresenterImpl(matrixClient, RoomMembershipObserver(), coroutineDispatchers),
roomListDataSource = RoomListDataSource(
- roomSummaryDataSource = matrixClient.roomSummaryDataSource,
+ roomListService = matrixClient.roomListService,
lastMessageTimestampFormatter = DefaultLastMessageTimestampFormatter(dateTimeProvider, dateFormatters),
roomLastMessageFormatter = DefaultRoomLastMessageFormatter(
sp = stringProvider,
@@ -113,7 +113,9 @@ class RoomListScreen(
}
onDispose {
Timber.w("Stop sync!")
- matrixClient.syncService().stopSync()
+ runBlocking {
+ matrixClient.syncService().stopSync()
+ }
}
}
}
diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/Singleton.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/Singleton.kt
index 5f8c6555a5..027da552fa 100644
--- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/Singleton.kt
+++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/Singleton.kt
@@ -17,19 +17,41 @@
package io.element.android.samples.minimal
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
-import io.element.android.libraries.matrix.impl.tracing.setupTracing
-import io.element.android.libraries.matrix.api.tracing.TracingConfigurations
+import io.element.android.libraries.core.meta.BuildMeta
+import io.element.android.libraries.core.meta.BuildType
+import io.element.android.libraries.matrix.api.tracing.TracingConfiguration
+import io.element.android.libraries.matrix.api.tracing.TracingFilterConfigurations
+import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration
+import io.element.android.libraries.matrix.impl.tracing.RustTracingService
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.plus
-import timber.log.Timber
object Singleton {
+ private val buildMeta = BuildMeta(
+ isDebuggable = true,
+ buildType = BuildType.DEBUG,
+ applicationName = "EAX-Minimal",
+ applicationId = "io.element.android.samples.minimal",
+ lowPrivacyLoggingEnabled = false,
+ versionName = "0.1.0",
+ versionCode = 1,
+ gitRevision = "TODO", // BuildConfig.GIT_REVISION,
+ gitRevisionDate = "TODO", // BuildConfig.GIT_REVISION_DATE,
+ gitBranchName = "TODO", // BuildConfig.GIT_BRANCH_NAME,
+ flavorDescription = "TODO", // BuildConfig.FLAVOR_DESCRIPTION,
+ flavorShortDescription = "TODO", // BuildConfig.SHORT_FLAVOR_DESCRIPTION,
+ )
+
init {
- Timber.plant(Timber.DebugTree())
- setupTracing(TracingConfigurations.debug)
+ val tracingConfiguration = TracingConfiguration(
+ filterConfiguration = TracingFilterConfigurations.debug,
+ writesToLogcat = true,
+ writesToFilesConfiguration = WriteToFilesConfiguration.Disabled
+ )
+ RustTracingService(buildMeta).setupTracing(tracingConfiguration)
}
val appScope = MainScope() + CoroutineName("Minimal Scope")
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 408c9e2934..751c65d388 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -36,6 +36,10 @@ dependencyResolutionManagement {
includeModule("com.github.matrix-org", "matrix-analytics-events")
}
}
+ // To have immediate access to Rust SDK versions
+ maven {
+ url = URI("https://s01.oss.sonatype.org/content/repositories/releases")
+ }
flatDir {
dirs("libraries/matrix/libs")
}
diff --git a/tests/testutils/build.gradle.kts b/tests/testutils/build.gradle.kts
index d7c17c7895..184bbc418a 100644
--- a/tests/testutils/build.gradle.kts
+++ b/tests/testutils/build.gradle.kts
@@ -30,4 +30,5 @@ dependencies {
implementation(libs.test.junit)
implementation(libs.coroutines.test)
implementation(projects.libraries.core)
+ implementation(libs.test.turbine)
}
diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/ReceiveTurbine.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/ReceiveTurbine.kt
new file mode 100644
index 0000000000..06b6b3d3ea
--- /dev/null
+++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/ReceiveTurbine.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.tests.testutils
+
+import app.cash.turbine.Event
+import app.cash.turbine.ReceiveTurbine
+import app.cash.turbine.withTurbineTimeout
+import io.element.android.libraries.core.data.tryOrNull
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
+
+/**
+ * Consume all items until timeout is reached waiting for an event or we receive terminal event.
+ * The timeout is applied for each event.
+ * @return the list of consumed items.
+ */
+suspend fun ReceiveTurbine.consumeItemsUntilTimeout(timeout: Duration = 100.milliseconds): List {
+ return consumeItemsUntilPredicate(timeout) { false }
+}
+
+/**
+ * Consume items until predicate is true, or timeout is reached waiting for an event, or we receive terminal event.
+ * The timeout is applied for each event.
+ * @return the list of consumed items.
+ */
+suspend fun ReceiveTurbine.consumeItemsUntilPredicate(
+ timeout: Duration = 100.milliseconds,
+ predicate: (T) -> Boolean,
+): List