Merge branch 'develop' of https://github.com/vector-im/element-x-android into dla/feature/custom_room_notification_settings_list
35
.github/ISSUE_TEMPLATE/story.yml
vendored
|
|
@ -1,35 +0,0 @@
|
||||||
name: User story issue
|
|
||||||
description: Second-level planning issue template. A story should take about a week or a sprint to finish.
|
|
||||||
title: "[Story] <title>"
|
|
||||||
labels: [T-Story]
|
|
||||||
|
|
||||||
body:
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Story
|
|
||||||
description: A story should take roughly a week or a sprint to finish. Each story is usually made up of a number of tasks that take half to a full day.
|
|
||||||
value: |
|
|
||||||
As a user…
|
|
||||||
I want to…
|
|
||||||
so that I can…
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
<!--These should be a list of technical tasks which take ½-1 day to complete-->
|
|
||||||
```[tasklist]
|
|
||||||
### Tasklist
|
|
||||||
- [ ] Task 1
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] QA signoff on completion
|
|
||||||
- [ ] Design signoff on completion
|
|
||||||
- [ ] Product signoff on completion
|
|
||||||
|
|
||||||
|
|
||||||
## Stretch goals
|
|
||||||
None at this time
|
|
||||||
<!--or add a tasklist-->
|
|
||||||
|
|
||||||
## Out of scope
|
|
||||||
-
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
4
.github/workflows/build.yml
vendored
|
|
@ -38,7 +38,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/gradle-build-action@v2.8.0
|
uses: gradle/gradle-build-action@v2.9.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Assemble debug APK
|
- name: Assemble debug APK
|
||||||
|
|
@ -55,7 +55,7 @@ jobs:
|
||||||
name: elementx-debug
|
name: elementx-debug
|
||||||
path: |
|
path: |
|
||||||
app/build/outputs/apk/debug/*.apk
|
app/build/outputs/apk/debug/*.apk
|
||||||
- uses: rnkdsh/action-upload-diawi@v1.5.1
|
- uses: rnkdsh/action-upload-diawi@v1.5.3
|
||||||
id: diawi
|
id: diawi
|
||||||
# Do not fail the whole build if Diawi upload fails
|
# Do not fail the whole build if Diawi upload fails
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
|
||||||
2
.github/workflows/danger.yml
vendored
|
|
@ -11,7 +11,7 @@ jobs:
|
||||||
- run: |
|
- run: |
|
||||||
npm install --save-dev @babel/plugin-transform-flow-strip-types
|
npm install --save-dev @babel/plugin-transform-flow-strip-types
|
||||||
- name: Danger
|
- name: Danger
|
||||||
uses: danger/danger-js@11.2.8
|
uses: danger/danger-js@11.3.0
|
||||||
with:
|
with:
|
||||||
args: "--dangerfile ./tools/danger/dangerfile.js"
|
args: "--dangerfile ./tools/danger/dangerfile.js"
|
||||||
env:
|
env:
|
||||||
|
|
|
||||||
2
.github/workflows/maestro.yml
vendored
|
|
@ -40,7 +40,7 @@ jobs:
|
||||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||||
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
||||||
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
||||||
- uses: mobile-dev-inc/action-maestro-cloud@v1.5.0
|
- uses: mobile-dev-inc/action-maestro-cloud@v1.6.0
|
||||||
with:
|
with:
|
||||||
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
|
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
|
||||||
# Doc says (https://github.com/mobile-dev-inc/action-maestro-cloud#android):
|
# Doc says (https://github.com/mobile-dev-inc/action-maestro-cloud#android):
|
||||||
|
|
|
||||||
2
.github/workflows/nightlyReports.yml
vendored
|
|
@ -62,7 +62,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/gradle-build-action@v2.8.0
|
uses: gradle/gradle-build-action@v2.9.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Dependency analysis
|
- name: Dependency analysis
|
||||||
|
|
|
||||||
4
.github/workflows/quality.yml
vendored
|
|
@ -40,7 +40,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/gradle-build-action@v2.8.0
|
uses: gradle/gradle-build-action@v2.9.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Run code quality check suite
|
- name: Run code quality check suite
|
||||||
|
|
@ -60,7 +60,7 @@ jobs:
|
||||||
yarn add danger-plugin-lint-report --dev
|
yarn add danger-plugin-lint-report --dev
|
||||||
- name: Danger lint
|
- name: Danger lint
|
||||||
if: always()
|
if: always()
|
||||||
uses: danger/danger-js@11.2.8
|
uses: danger/danger-js@11.3.0
|
||||||
with:
|
with:
|
||||||
args: "--dangerfile ./tools/danger/dangerfile-lint.js"
|
args: "--dangerfile ./tools/danger/dangerfile-lint.js"
|
||||||
env:
|
env:
|
||||||
|
|
|
||||||
2
.github/workflows/recordScreenshots.yml
vendored
|
|
@ -24,7 +24,7 @@ jobs:
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
# Add gradle cache, this should speed up the process
|
# Add gradle cache, this should speed up the process
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/gradle-build-action@v2.8.0
|
uses: gradle/gradle-build-action@v2.9.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Record screenshots
|
- name: Record screenshots
|
||||||
|
|
|
||||||
2
.github/workflows/release.yml
vendored
|
|
@ -25,7 +25,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/gradle-build-action@v2.8.0
|
uses: gradle/gradle-build-action@v2.9.0
|
||||||
- name: Create app bundle
|
- name: Create app bundle
|
||||||
env:
|
env:
|
||||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||||
|
|
|
||||||
2
.github/workflows/sonar.yml
vendored
|
|
@ -32,7 +32,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/gradle-build-action@v2.8.0
|
uses: gradle/gradle-build-action@v2.9.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: 🔊 Publish results to Sonar
|
- name: 🔊 Publish results to Sonar
|
||||||
|
|
|
||||||
2
.github/workflows/tests.yml
vendored
|
|
@ -44,7 +44,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/gradle-build-action@v2.8.0
|
uses: gradle/gradle-build-action@v2.9.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
|
|
||||||
|
|
|
||||||
1
.idea/dictionaries/shared.xml
generated
|
|
@ -4,6 +4,7 @@
|
||||||
<w>backstack</w>
|
<w>backstack</w>
|
||||||
<w>ftue</w>
|
<w>ftue</w>
|
||||||
<w>homeserver</w>
|
<w>homeserver</w>
|
||||||
|
<w>konsist</w>
|
||||||
<w>kover</w>
|
<w>kover</w>
|
||||||
<w>measurables</w>
|
<w>measurables</w>
|
||||||
<w>onboarding</w>
|
<w>onboarding</w>
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,9 @@ appId: ${APP_ID}
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "change_server-server"
|
id: "change_server-server"
|
||||||
# Test server that does not support sliding sync.
|
# Test server that does not support sliding sync.
|
||||||
- inputText: "gnuradio"
|
- inputText: "https://kieranml.ems-support.element.dev"
|
||||||
- hideKeyboard
|
- hideKeyboard
|
||||||
- tapOn: "gnuradio.org"
|
- tapOn: "kieranml.ems-support.element.dev"
|
||||||
- extendedWaitUntil:
|
- extendedWaitUntil:
|
||||||
visible: "This server currently doesn’t support sliding sync."
|
visible: "This server currently doesn’t support sliding sync."
|
||||||
timeout: 10000
|
timeout: 10000
|
||||||
|
|
|
||||||
63
CHANGES.md
|
|
@ -1,3 +1,66 @@
|
||||||
|
Changes in Element X v0.2.4 (2023-10-12)
|
||||||
|
========================================
|
||||||
|
|
||||||
|
Features ✨
|
||||||
|
----------
|
||||||
|
- [Rich text editor] Add full screen mode ([#1447](https://github.com/vector-im/element-x-android/issues/1447))
|
||||||
|
- Improve rendering of m.emote. ([#1497](https://github.com/vector-im/element-x-android/issues/1497))
|
||||||
|
- Improve deleted session behavior. ([#1520](https://github.com/vector-im/element-x-android/issues/1520))
|
||||||
|
|
||||||
|
Bugfixes 🐛
|
||||||
|
----------
|
||||||
|
- WebP images can't be sent as media. ([#1483](https://github.com/vector-im/element-x-android/issues/1483))
|
||||||
|
- Fix back button not working in bottom sheets. ([#1517](https://github.com/vector-im/element-x-android/issues/1517))
|
||||||
|
- Render body of unknown msgtype in the timeline and in the room list ([#1539](https://github.com/vector-im/element-x-android/issues/1539))
|
||||||
|
|
||||||
|
Other changes
|
||||||
|
-------------
|
||||||
|
- Room : makes subscribeToSync/unsubscribeFromSync suspendable. ([#1457](https://github.com/vector-im/element-x-android/issues/1457))
|
||||||
|
- Add some Konsist tests. ([#1526](https://github.com/vector-im/element-x-android/issues/1526))
|
||||||
|
|
||||||
|
|
||||||
|
Changes in Element X v0.2.3 (2023-09-27)
|
||||||
|
========================================
|
||||||
|
|
||||||
|
Features ✨
|
||||||
|
----------
|
||||||
|
- Handle installation of Apks from the media viewer. ([#1432](https://github.com/vector-im/element-x-android/pull/1432))
|
||||||
|
- Integrate SDK 0.1.58 ([#1437](https://github.com/vector-im/element-x-android/pull/1437))
|
||||||
|
|
||||||
|
Other changes
|
||||||
|
-------------
|
||||||
|
- Element call: add custom parameters to Element Call urls. ([#1434](https://github.com/vector-im/element-x-android/issues/1434))
|
||||||
|
|
||||||
|
|
||||||
|
Changes in Element X v0.2.2 (2023-09-21)
|
||||||
|
========================================
|
||||||
|
|
||||||
|
Bugfixes 🐛
|
||||||
|
----------
|
||||||
|
- Add animation when rendering the timeline to avoid glitches. ([#1323](https://github.com/vector-im/element-x-android/issues/1323))
|
||||||
|
- Fix crash when trying to take a photo or record a video. ([#1395](https://github.com/vector-im/element-x-android/issues/1395))
|
||||||
|
|
||||||
|
|
||||||
|
Changes in Element X v0.2.1 (2023-09-20)
|
||||||
|
========================================
|
||||||
|
|
||||||
|
Features ✨
|
||||||
|
----------
|
||||||
|
- Bump Rust SDK to `v0.1.56`
|
||||||
|
- [Rich text editor] Add link support to rich text editor ([#1309](https://github.com/vector-im/element-x-android/issues/1309))
|
||||||
|
- Let the SDK figure the best scheme given an homeserver URL (thus allowing HTTP homeservers) ([#1382](https://github.com/vector-im/element-x-android/issues/1382))
|
||||||
|
|
||||||
|
Bugfixes 🐛
|
||||||
|
----------
|
||||||
|
- Fix ANR on RoomList when notification settings change. ([#1370](https://github.com/vector-im/element-x-android/issues/1370))
|
||||||
|
|
||||||
|
Other changes
|
||||||
|
-------------
|
||||||
|
- Element Call: support scheme `io.element.call` ([#1377](https://github.com/vector-im/element-x-android/issues/1377))
|
||||||
|
- [DI] Rework how dagger components are created and provided. ([#1378](https://github.com/vector-im/element-x-android/issues/1378))
|
||||||
|
- Remove usage of async-uniffi as it leads to a deadlocks and memory leaks. ([#1381](https://github.com/vector-im/element-x-android/issues/1381))
|
||||||
|
|
||||||
|
|
||||||
Changes in Element X v0.2.0 (2023-09-18)
|
Changes in Element X v0.2.0 (2023-09-18)
|
||||||
========================================
|
========================================
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
* [knit](#knit)
|
* [knit](#knit)
|
||||||
* [lint](#lint)
|
* [lint](#lint)
|
||||||
* [Unit tests](#unit-tests)
|
* [Unit tests](#unit-tests)
|
||||||
|
* [konsist](#konsist)
|
||||||
* [Tests](#tests)
|
* [Tests](#tests)
|
||||||
* [Accessibility](#accessibility)
|
* [Accessibility](#accessibility)
|
||||||
* [Jetpack Compose](#jetpack-compose)
|
* [Jetpack Compose](#jetpack-compose)
|
||||||
|
|
@ -156,6 +157,10 @@ Make sure the following commands execute without any error:
|
||||||
./gradlew test
|
./gradlew test
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
|
#### konsist
|
||||||
|
|
||||||
|
[konsist](https://github.com/LemonAppDev/konsist) is setup in the project to check that the architecture and the naming rules are followed. Konsist tests are classical Unit tests.
|
||||||
|
|
||||||
### Tests
|
### Tests
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
@ -171,11 +176,11 @@ For instance, when updating the image `src` of an ImageView, please also conside
|
||||||
|
|
||||||
### Jetpack Compose
|
### Jetpack Compose
|
||||||
|
|
||||||
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.
|
When adding or editing `@Composable`, make sure that you create an internal function annotated with `@PreviewsDayNight`, with a name suffixed by `Preview`, and having `ElementPreview` as the root composable.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```kotlin
|
```kotlin
|
||||||
@DayNightPreviews
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PinIconPreview() = ElementPreview {
|
internal fun PinIconPreview() = ElementPreview {
|
||||||
PinIcon()
|
PinIcon()
|
||||||
|
|
|
||||||
|
|
@ -20,14 +20,16 @@
|
||||||
<!-- To be able to install APK from the application -->
|
<!-- To be able to install APK from the application -->
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
|
||||||
|
<!-- Do not enable enableOnBackInvokedCallback until https://issuetracker.google.com/issues/271303558 is fixed -->
|
||||||
<application
|
<application
|
||||||
android:name=".ElementXApplication"
|
android:name=".ElementXApplication"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:enableOnBackInvokedCallback="true"
|
android:enableOnBackInvokedCallback="false"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.ElementX"
|
android:theme="@style/Theme.ElementX"
|
||||||
|
|
@ -81,11 +83,12 @@
|
||||||
tools:node="remove" />
|
tools:node="remove" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:authorities="${applicationId}.fileprovider"
|
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:grantUriPermissions="true"
|
android:authorities="${applicationId}.fileprovider"
|
||||||
android:exported="false">
|
android:exported="false"
|
||||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS"
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:resource="@xml/file_providers" />
|
android:resource="@xml/file_providers" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 260 KiB After Width: | Height: | Size: 263 KiB |
|
|
@ -34,7 +34,7 @@ import com.bumble.appyx.core.integrationpoint.NodeComponentActivity
|
||||||
import com.bumble.appyx.core.plugin.NodeReadyObserver
|
import com.bumble.appyx.core.plugin.NodeReadyObserver
|
||||||
import io.element.android.libraries.architecture.bindings
|
import io.element.android.libraries.architecture.bindings
|
||||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
import io.element.android.libraries.designsystem.utils.LocalSnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher
|
||||||
import io.element.android.libraries.theme.ElementTheme
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
import io.element.android.x.di.AppBindings
|
import io.element.android.x.di.AppBindings
|
||||||
import io.element.android.x.intent.SafeUriHandler
|
import io.element.android.x.intent.SafeUriHandler
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ package io.element.android.x.di
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesTo
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
import io.element.android.features.rageshake.api.reporter.BugReporter
|
import io.element.android.features.rageshake.api.reporter.BugReporter
|
||||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.matrix.api.tracing.TracingService
|
import io.element.android.libraries.matrix.api.tracing.TracingService
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ import io.element.android.features.messages.impl.timeline.components.customreact
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.core.meta.BuildType
|
import io.element.android.libraries.core.meta.BuildType
|
||||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
import io.element.android.libraries.di.DefaultPreferences
|
import io.element.android.libraries.di.DefaultPreferences
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 4 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 6 KiB After Width: | Height: | Size: 6 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
35
app/src/main/res/xml/network_security_config.xml
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<network-security-config>
|
||||||
|
<!-- Ref: https://developer.android.com/training/articles/security-config.html -->
|
||||||
|
<!-- By default, do not allow clearText traffic -->
|
||||||
|
<base-config cleartextTrafficPermitted="false" />
|
||||||
|
|
||||||
|
<!-- Allow clearText traffic on some specified host -->
|
||||||
|
<domain-config cleartextTrafficPermitted="true">
|
||||||
|
<!-- Localhost -->
|
||||||
|
<domain includeSubdomains="true">localhost</domain>
|
||||||
|
<domain includeSubdomains="true">127.0.0.1</domain>
|
||||||
|
<!-- Localhost for Android emulator -->
|
||||||
|
<domain includeSubdomains="true">10.0.2.2</domain>
|
||||||
|
<!-- Onion services -->
|
||||||
|
<domain includeSubdomains="true">onion</domain>
|
||||||
|
|
||||||
|
<!-- Domains that are used for LANs -->
|
||||||
|
<!-- These are IANA recognized special use domain names, see https://www.iana.org/assignments/special-use-domain-names/special-use-domain-names.xhtml -->
|
||||||
|
<domain includeSubdomains="true">home.arpa</domain>
|
||||||
|
<domain includeSubdomains="true">local</domain> <!-- Note this has been reserved for use with mDNS -->
|
||||||
|
<domain includeSubdomains="true">test</domain>
|
||||||
|
<!-- These are observed in the wild either by convention or RFCs that have not been accepted, and are not currently TLDs -->
|
||||||
|
<domain includeSubdomains="true">home</domain>
|
||||||
|
<domain includeSubdomains="true">lan</domain>
|
||||||
|
<domain includeSubdomains="true">localdomain</domain>
|
||||||
|
</domain-config>
|
||||||
|
|
||||||
|
<debug-overrides>
|
||||||
|
<trust-anchors>
|
||||||
|
<certificates src="system" />
|
||||||
|
<certificates src="user" />
|
||||||
|
</trust-anchors>
|
||||||
|
</debug-overrides>
|
||||||
|
|
||||||
|
</network-security-config>
|
||||||
|
|
@ -16,8 +16,8 @@
|
||||||
|
|
||||||
package io.element.android.appnav
|
package io.element.android.appnav
|
||||||
|
|
||||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
import io.element.android.libraries.designsystem.utils.SnackbarMessage
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,10 @@ import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.bumble.appyx.core.composable.Children
|
import com.bumble.appyx.core.composable.Children
|
||||||
|
import com.bumble.appyx.core.composable.PermanentChild
|
||||||
import com.bumble.appyx.core.lifecycle.subscribe
|
import com.bumble.appyx.core.lifecycle.subscribe
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import com.bumble.appyx.core.plugin.plugins
|
import com.bumble.appyx.core.plugin.plugins
|
||||||
|
|
@ -48,14 +50,18 @@ import io.element.android.features.ftue.api.state.FtueState
|
||||||
import io.element.android.features.invitelist.api.InviteListEntryPoint
|
import io.element.android.features.invitelist.api.InviteListEntryPoint
|
||||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
|
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
||||||
|
import io.element.android.features.lockscreen.api.LockScreenState
|
||||||
|
import io.element.android.features.lockscreen.api.LockScreenStateService
|
||||||
import io.element.android.features.preferences.api.PreferencesEntryPoint
|
import io.element.android.features.preferences.api.PreferencesEntryPoint
|
||||||
import io.element.android.features.roomlist.api.RoomListEntryPoint
|
import io.element.android.features.roomlist.api.RoomListEntryPoint
|
||||||
import io.element.android.features.verifysession.api.VerifySessionEntryPoint
|
import io.element.android.features.verifysession.api.VerifySessionEntryPoint
|
||||||
import io.element.android.libraries.architecture.BackstackNode
|
import io.element.android.libraries.architecture.BackstackNode
|
||||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
|
import io.element.android.libraries.architecture.waitForChildAttached
|
||||||
import io.element.android.libraries.deeplink.DeeplinkData
|
import io.element.android.libraries.deeplink.DeeplinkData
|
||||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
|
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
|
||||||
|
|
@ -68,6 +74,7 @@ import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|
@ -86,6 +93,8 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
private val notificationDrawerManager: NotificationDrawerManager,
|
private val notificationDrawerManager: NotificationDrawerManager,
|
||||||
private val ftueState: FtueState,
|
private val ftueState: FtueState,
|
||||||
|
private val lockScreenEntryPoint: LockScreenEntryPoint,
|
||||||
|
private val lockScreenStateService: LockScreenStateService,
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
snackbarDispatcher: SnackbarDispatcher,
|
snackbarDispatcher: SnackbarDispatcher,
|
||||||
) : BackstackNode<LoggedInFlowNode.NavTarget>(
|
) : BackstackNode<LoggedInFlowNode.NavTarget>(
|
||||||
|
|
@ -93,6 +102,10 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
initialElement = NavTarget.RoomList,
|
initialElement = NavTarget.RoomList,
|
||||||
savedStateMap = buildContext.savedStateMap,
|
savedStateMap = buildContext.savedStateMap,
|
||||||
),
|
),
|
||||||
|
permanentNavModel = PermanentNavModel(
|
||||||
|
navTargets = setOf(NavTarget.LoggedInPermanent, NavTarget.LockPermanent),
|
||||||
|
savedStateMap = buildContext.savedStateMap,
|
||||||
|
),
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
plugins = plugins
|
plugins = plugins
|
||||||
) {
|
) {
|
||||||
|
|
@ -121,9 +134,19 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
backstack.push(NavTarget.Ftue)
|
backstack.push(NavTarget.Ftue)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onStop = {
|
onResume = {
|
||||||
//Counterpart startSync is done in observeSyncStateAndNetworkStatus method.
|
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
|
lockScreenStateService.entersForeground()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPause = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
lockScreenStateService.entersBackground()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onStop = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
//Counterpart startSync is done in observeSyncStateAndNetworkStatus method.
|
||||||
syncService.stopSync()
|
syncService.stopSync()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -159,7 +182,10 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object Permanent : NavTarget
|
data object LoggedInPermanent : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object LockPermanent : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object RoomList : NavTarget
|
data object RoomList : NavTarget
|
||||||
|
|
@ -188,9 +214,12 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
return when (navTarget) {
|
return when (navTarget) {
|
||||||
NavTarget.Permanent -> {
|
NavTarget.LoggedInPermanent -> {
|
||||||
createNode<LoggedInNode>(buildContext)
|
createNode<LoggedInNode>(buildContext)
|
||||||
}
|
}
|
||||||
|
NavTarget.LockPermanent -> {
|
||||||
|
lockScreenEntryPoint.createNode(this, buildContext)
|
||||||
|
}
|
||||||
NavTarget.RoomList -> {
|
NavTarget.RoomList -> {
|
||||||
val callback = object : RoomListEntryPoint.Callback {
|
val callback = object : RoomListEntryPoint.Callback {
|
||||||
override fun onRoomClicked(roomId: RoomId) {
|
override fun onRoomClicked(roomId: RoomId) {
|
||||||
|
|
@ -308,30 +337,37 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
internal suspend fun attachInviteList(deeplinkData: DeeplinkData.InviteList) = withContext(lifecycleScope.coroutineContext) {
|
||||||
override fun View(modifier: Modifier) {
|
notificationDrawerManager.clearMembershipNotificationForSession(deeplinkData.sessionId)
|
||||||
Box(modifier = modifier) {
|
backstack.singleTop(NavTarget.RoomList)
|
||||||
Children(
|
backstack.push(NavTarget.InviteList)
|
||||||
navModel = backstack,
|
waitForChildAttached<Node, NavTarget> { navTarget ->
|
||||||
modifier = Modifier,
|
navTarget is NavTarget.InviteList
|
||||||
// Animate navigation to settings and to a room
|
|
||||||
transitionHandler = rememberDefaultTransitionHandler(),
|
|
||||||
)
|
|
||||||
|
|
||||||
val isFtueDisplayed by ftueState.shouldDisplayFlow.collectAsState()
|
|
||||||
|
|
||||||
if (!isFtueDisplayed) {
|
|
||||||
PermanentChild(navTarget = NavTarget.Permanent)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal suspend fun attachRoom(deeplinkData: DeeplinkData.Room) {
|
@Composable
|
||||||
backstack.push(NavTarget.Room(deeplinkData.roomId))
|
override fun View(modifier: Modifier) {
|
||||||
}
|
Box(modifier = modifier) {
|
||||||
|
val lockScreenState by lockScreenStateService.state.collectAsState()
|
||||||
internal suspend fun attachInviteList(deeplinkData: DeeplinkData.InviteList) {
|
when (lockScreenState) {
|
||||||
notificationDrawerManager.clearMembershipNotificationForSession(deeplinkData.sessionId)
|
LockScreenState.Unlocked -> {
|
||||||
backstack.push(NavTarget.InviteList)
|
Children(
|
||||||
|
navModel = backstack,
|
||||||
|
modifier = Modifier,
|
||||||
|
// Animate navigation to settings and to a room
|
||||||
|
transitionHandler = rememberDefaultTransitionHandler(),
|
||||||
|
)
|
||||||
|
val isFtueDisplayed by ftueState.shouldDisplayFlow.collectAsState()
|
||||||
|
if (!isFtueDisplayed) {
|
||||||
|
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LockScreenState.Locked -> {
|
||||||
|
MoveActivityToBackgroundBackHandler()
|
||||||
|
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LockPermanent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.appnav
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.ContextWrapper
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MoveActivityToBackgroundBackHandler(enabled: Boolean = true) {
|
||||||
|
|
||||||
|
fun Context.findActivity(): ComponentActivity? = when (this) {
|
||||||
|
is ComponentActivity -> this
|
||||||
|
is ContextWrapper -> baseContext.findActivity()
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
BackHandler(enabled = enabled) {
|
||||||
|
context.findActivity()?.moveTaskToBack(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -45,6 +45,7 @@ import io.element.android.appnav.root.RootView
|
||||||
import io.element.android.features.login.api.oidc.OidcAction
|
import io.element.android.features.login.api.oidc.OidcAction
|
||||||
import io.element.android.features.login.api.oidc.OidcActionFlow
|
import io.element.android.features.login.api.oidc.OidcActionFlow
|
||||||
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
|
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
|
||||||
|
import io.element.android.features.signedout.api.SignedOutEntryPoint
|
||||||
import io.element.android.libraries.architecture.BackstackNode
|
import io.element.android.libraries.architecture.BackstackNode
|
||||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
|
|
@ -54,6 +55,7 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
@ -69,6 +71,7 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
private val matrixClientsHolder: MatrixClientsHolder,
|
private val matrixClientsHolder: MatrixClientsHolder,
|
||||||
private val presenter: RootPresenter,
|
private val presenter: RootPresenter,
|
||||||
private val bugReportEntryPoint: BugReportEntryPoint,
|
private val bugReportEntryPoint: BugReportEntryPoint,
|
||||||
|
private val signedOutEntryPoint: SignedOutEntryPoint,
|
||||||
private val intentResolver: IntentResolver,
|
private val intentResolver: IntentResolver,
|
||||||
private val oidcActionFlow: OidcActionFlow,
|
private val oidcActionFlow: OidcActionFlow,
|
||||||
) : BackstackNode<RootFlowNode.NavTarget>(
|
) : BackstackNode<RootFlowNode.NavTarget>(
|
||||||
|
|
@ -97,13 +100,20 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.onEach { navState ->
|
.onEach { navState ->
|
||||||
Timber.v("navState=$navState")
|
Timber.v("navState=$navState")
|
||||||
if (navState.isLoggedIn) {
|
when (navState.loggedInState) {
|
||||||
tryToRestoreLatestSession(
|
is LoggedInState.LoggedIn -> {
|
||||||
onSuccess = { sessionId -> switchToLoggedInFlow(sessionId, navState.cacheIndex) },
|
if (navState.loggedInState.isTokenValid) {
|
||||||
onFailure = { switchToNotLoggedInFlow() }
|
tryToRestoreLatestSession(
|
||||||
)
|
onSuccess = { sessionId -> switchToLoggedInFlow(sessionId, navState.cacheIndex) },
|
||||||
} else {
|
onFailure = { switchToNotLoggedInFlow() }
|
||||||
switchToNotLoggedInFlow()
|
)
|
||||||
|
} else {
|
||||||
|
switchToSignedOutFlow(SessionId(navState.loggedInState.sessionId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LoggedInState.NotLoggedIn -> {
|
||||||
|
switchToNotLoggedInFlow()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.launchIn(lifecycleScope)
|
.launchIn(lifecycleScope)
|
||||||
|
|
@ -118,6 +128,10 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
backstack.safeRoot(NavTarget.NotLoggedInFlow)
|
backstack.safeRoot(NavTarget.NotLoggedInFlow)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun switchToSignedOutFlow(sessionId: SessionId) {
|
||||||
|
backstack.safeRoot(NavTarget.SignedOutFlow(sessionId))
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun restoreSessionIfNeeded(
|
private suspend fun restoreSessionIfNeeded(
|
||||||
sessionId: SessionId,
|
sessionId: SessionId,
|
||||||
onFailure: () -> Unit = {},
|
onFailure: () -> Unit = {},
|
||||||
|
|
@ -179,6 +193,11 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
val navId: Int
|
val navId: Int
|
||||||
) : NavTarget
|
) : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class SignedOutFlow(
|
||||||
|
val sessionId: SessionId
|
||||||
|
) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object BugReport : NavTarget
|
data object BugReport : NavTarget
|
||||||
}
|
}
|
||||||
|
|
@ -198,6 +217,15 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
createNode<LoggedInAppScopeFlowNode>(buildContext, plugins = listOf(inputs, callback))
|
createNode<LoggedInAppScopeFlowNode>(buildContext, plugins = listOf(inputs, callback))
|
||||||
}
|
}
|
||||||
NavTarget.NotLoggedInFlow -> createNode<NotLoggedInFlowNode>(buildContext)
|
NavTarget.NotLoggedInFlow -> createNode<NotLoggedInFlowNode>(buildContext)
|
||||||
|
is NavTarget.SignedOutFlow -> {
|
||||||
|
signedOutEntryPoint.nodeBuilder(this, buildContext)
|
||||||
|
.params(
|
||||||
|
SignedOutEntryPoint.Params(
|
||||||
|
sessionId = navTarget.sessionId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
NavTarget.SplashScreen -> splashNode(buildContext)
|
NavTarget.SplashScreen -> splashNode(buildContext)
|
||||||
NavTarget.BugReport -> {
|
NavTarget.BugReport -> {
|
||||||
val callback = object : BugReportEntryPoint.Callback {
|
val callback = object : BugReportEntryPoint.Callback {
|
||||||
|
|
@ -234,7 +262,7 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
.apply {
|
.apply {
|
||||||
when (deeplinkData) {
|
when (deeplinkData) {
|
||||||
is DeeplinkData.Root -> attachRoot()
|
is DeeplinkData.Root -> attachRoot()
|
||||||
is DeeplinkData.Room -> attachRoom(deeplinkData)
|
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId)
|
||||||
is DeeplinkData.InviteList -> attachInviteList(deeplinkData)
|
is DeeplinkData.InviteList -> attachInviteList(deeplinkData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,10 @@ class LoggedInNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
private val loggedInPresenter: LoggedInPresenter,
|
private val loggedInPresenter: LoggedInPresenter,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(
|
||||||
|
buildContext = buildContext,
|
||||||
|
plugins = plugins
|
||||||
|
) {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -47,7 +47,7 @@ fun LoggedInView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@DayNightPreviews
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun LoggedInViewPreview(@PreviewParameter(LoggedInStateProvider::class) state: LoggedInState) = ElementPreview {
|
internal fun LoggedInViewPreview(@PreviewParameter(LoggedInStateProvider::class) state: LoggedInState) = ElementPreview {
|
||||||
LoggedInView(
|
LoggedInView(
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
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.CircularProgressIndicator
|
||||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||||
|
|
@ -81,7 +81,7 @@ fun SyncStateView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@DayNightPreviews
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SyncStateViewPreview() = ElementPreview {
|
internal fun SyncStateViewPreview() = ElementPreview {
|
||||||
// Add a box to see the shadow
|
// Add a box to see the shadow
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorVi
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule
|
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.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
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.CircularProgressIndicator
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
|
|
@ -100,7 +100,7 @@ private fun LoadingRoomTopBar(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@DayNightPreviews
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun LoadingRoomNodeViewPreview(@PreviewParameter(LoadingRoomStateProvider::class) state: LoadingRoomState) = ElementPreview {
|
internal fun LoadingRoomNodeViewPreview(@PreviewParameter(LoadingRoomStateProvider::class) state: LoadingRoomState) = ElementPreview {
|
||||||
LoadingRoomNodeView(
|
LoadingRoomNodeView(
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
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.RoomMembershipObserver
|
||||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
@ -60,6 +61,7 @@ class RoomLoadedFlowNode @AssistedInject constructor(
|
||||||
private val messagesEntryPoint: MessagesEntryPoint,
|
private val messagesEntryPoint: MessagesEntryPoint,
|
||||||
private val roomDetailsEntryPoint: RoomDetailsEntryPoint,
|
private val roomDetailsEntryPoint: RoomDetailsEntryPoint,
|
||||||
private val appNavigationStateService: AppNavigationStateService,
|
private val appNavigationStateService: AppNavigationStateService,
|
||||||
|
private val appCoroutineScope: CoroutineScope,
|
||||||
roomComponentFactory: RoomComponentFactory,
|
roomComponentFactory: RoomComponentFactory,
|
||||||
roomMembershipObserver: RoomMembershipObserver,
|
roomMembershipObserver: RoomMembershipObserver,
|
||||||
) : BackstackNode<RoomLoadedFlowNode.NavTarget>(
|
) : BackstackNode<RoomLoadedFlowNode.NavTarget>(
|
||||||
|
|
@ -91,6 +93,16 @@ class RoomLoadedFlowNode @AssistedInject constructor(
|
||||||
appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId)
|
appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId)
|
||||||
fetchRoomMembers()
|
fetchRoomMembers()
|
||||||
},
|
},
|
||||||
|
onResume = {
|
||||||
|
appCoroutineScope.launch {
|
||||||
|
inputs.room.subscribeToSync()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPause = {
|
||||||
|
appCoroutineScope.launch {
|
||||||
|
inputs.room.unsubscribeFromSync()
|
||||||
|
}
|
||||||
|
},
|
||||||
onDestroy = {
|
onDestroy = {
|
||||||
Timber.v("OnDestroy")
|
Timber.v("OnDestroy")
|
||||||
appNavigationStateService.onLeavingRoom(id)
|
appNavigationStateService.onLeavingRoom(id)
|
||||||
|
|
@ -169,9 +181,7 @@ class RoomLoadedFlowNode @AssistedInject constructor(
|
||||||
// because this node enters 'onDestroy' before his children, so it can leads to
|
// because this node enters 'onDestroy' before his children, so it can leads to
|
||||||
// using the room in a child node where it's already closed.
|
// using the room in a child node where it's already closed.
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
inputs.room.subscribeToSync()
|
|
||||||
onDispose {
|
onDispose {
|
||||||
inputs.room.unsubscribeFromSync()
|
|
||||||
if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
|
if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
|
||||||
inputs.room.destroy()
|
inputs.room.destroy()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
package io.element.android.appnav.root
|
package io.element.android.appnav.root
|
||||||
|
|
||||||
|
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [RootNavState] produced by [RootNavStateFlowFactory].
|
* [RootNavState] produced by [RootNavStateFlowFactory].
|
||||||
*/
|
*/
|
||||||
|
|
@ -26,7 +28,7 @@ data class RootNavState(
|
||||||
*/
|
*/
|
||||||
val cacheIndex: Int,
|
val cacheIndex: Int,
|
||||||
/**
|
/**
|
||||||
* true if we are currently loggedIn.
|
* LoggedInState.
|
||||||
*/
|
*/
|
||||||
val isLoggedIn: Boolean
|
val loggedInState: LoggedInState,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,9 @@ import io.element.android.appnav.di.MatrixClientsHolder
|
||||||
import io.element.android.features.login.api.LoginUserStory
|
import io.element.android.features.login.api.LoginUserStory
|
||||||
import io.element.android.features.preferences.api.CacheService
|
import io.element.android.features.preferences.api.CacheService
|
||||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||||
|
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -47,9 +47,14 @@ class RootNavStateFlowFactory @Inject constructor(
|
||||||
fun create(savedStateMap: SavedStateMap?): Flow<RootNavState> {
|
fun create(savedStateMap: SavedStateMap?): Flow<RootNavState> {
|
||||||
return combine(
|
return combine(
|
||||||
cacheIndexFlow(savedStateMap),
|
cacheIndexFlow(savedStateMap),
|
||||||
isUserLoggedInFlow(),
|
authenticationService.loggedInStateFlow(),
|
||||||
) { cacheIndex, isLoggedIn ->
|
loginUserStory.loginFlowIsDone,
|
||||||
RootNavState(cacheIndex = cacheIndex, isLoggedIn = isLoggedIn)
|
) { cacheIndex, loggedInState, loginFlowIsDone ->
|
||||||
|
if (loginFlowIsDone) {
|
||||||
|
RootNavState(cacheIndex = cacheIndex, loggedInState = loggedInState)
|
||||||
|
} else {
|
||||||
|
RootNavState(cacheIndex = cacheIndex, loggedInState = LoggedInState.NotLoggedIn)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,16 +77,6 @@ class RootNavStateFlowFactory @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isUserLoggedInFlow(): Flow<Boolean> {
|
|
||||||
return combine(
|
|
||||||
authenticationService.isLoggedIn(),
|
|
||||||
loginUserStory.loginFlowIsDone
|
|
||||||
) { isLoggedIn, loginFlowIsDone ->
|
|
||||||
isLoggedIn && loginFlowIsDone
|
|
||||||
}
|
|
||||||
.distinctUntilChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return a flow of integer that increments the value by one each time a new element is emitted upstream.
|
* @return a flow of integer that increments the value by one each time a new element is emitted upstream.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ import io.element.android.features.rageshake.api.crash.CrashDetectionEvents
|
||||||
import io.element.android.features.rageshake.api.crash.CrashDetectionView
|
import io.element.android.features.rageshake.api.crash.CrashDetectionView
|
||||||
import io.element.android.features.rageshake.api.detection.RageshakeDetectionEvents
|
import io.element.android.features.rageshake.api.detection.RageshakeDetectionEvents
|
||||||
import io.element.android.features.rageshake.api.detection.RageshakeDetectionView
|
import io.element.android.features.rageshake.api.detection.RageshakeDetectionView
|
||||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.services.apperror.impl.AppErrorView
|
import io.element.android.services.apperror.impl.AppErrorView
|
||||||
|
|
@ -66,7 +66,7 @@ fun RootView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@DayNightPreviews
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun RootPreview(@PreviewParameter(RootStateProvider::class) rootState: RootState) = ElementPreview {
|
internal fun RootPreview(@PreviewParameter(RootStateProvider::class) rootState: RootState) = ElementPreview {
|
||||||
RootView(rootState) {
|
RootView(rootState) {
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,8 @@ 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.RoomMembershipObserver
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
@ -85,6 +87,7 @@ class RoomFlowNodeTest {
|
||||||
plugins: List<Plugin>,
|
plugins: List<Plugin>,
|
||||||
messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(),
|
messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(),
|
||||||
roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(),
|
roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(),
|
||||||
|
coroutineScope: CoroutineScope,
|
||||||
) = RoomLoadedFlowNode(
|
) = RoomLoadedFlowNode(
|
||||||
buildContext = BuildContext.root(savedStateMap = null),
|
buildContext = BuildContext.root(savedStateMap = null),
|
||||||
plugins = plugins,
|
plugins = plugins,
|
||||||
|
|
@ -92,16 +95,21 @@ class RoomFlowNodeTest {
|
||||||
roomDetailsEntryPoint = roomDetailsEntryPoint,
|
roomDetailsEntryPoint = roomDetailsEntryPoint,
|
||||||
appNavigationStateService = FakeAppNavigationStateService(),
|
appNavigationStateService = FakeAppNavigationStateService(),
|
||||||
roomMembershipObserver = RoomMembershipObserver(),
|
roomMembershipObserver = RoomMembershipObserver(),
|
||||||
|
appCoroutineScope = coroutineScope,
|
||||||
roomComponentFactory = FakeRoomComponentFactory(),
|
roomComponentFactory = FakeRoomComponentFactory(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given a room flow node when initialized then it loads messages entry point`() {
|
fun `given a room flow node when initialized then it loads messages entry point`() = runTest {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
val room = FakeMatrixRoom()
|
val room = FakeMatrixRoom()
|
||||||
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||||
val inputs = RoomLoadedFlowNode.Inputs(room)
|
val inputs = RoomLoadedFlowNode.Inputs(room)
|
||||||
val roomFlowNode = aRoomFlowNode(listOf(inputs), fakeMessagesEntryPoint)
|
val roomFlowNode = aRoomFlowNode(
|
||||||
|
plugins = listOf(inputs),
|
||||||
|
messagesEntryPoint = fakeMessagesEntryPoint,
|
||||||
|
coroutineScope = this
|
||||||
|
)
|
||||||
// WHEN
|
// WHEN
|
||||||
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
||||||
|
|
||||||
|
|
@ -113,13 +121,18 @@ class RoomFlowNodeTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given a room flow node when callback on room details is triggered then it loads room details entry point`() {
|
fun `given a room flow node when callback on room details is triggered then it loads room details entry point`() = runTest {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
val room = FakeMatrixRoom()
|
val room = FakeMatrixRoom()
|
||||||
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||||
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
|
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
|
||||||
val inputs = RoomLoadedFlowNode.Inputs(room)
|
val inputs = RoomLoadedFlowNode.Inputs(room)
|
||||||
val roomFlowNode = aRoomFlowNode(listOf(inputs), fakeMessagesEntryPoint, fakeRoomDetailsEntryPoint)
|
val roomFlowNode = aRoomFlowNode(
|
||||||
|
plugins = listOf(inputs),
|
||||||
|
messagesEntryPoint = fakeMessagesEntryPoint,
|
||||||
|
roomDetailsEntryPoint = fakeRoomDetailsEntryPoint,
|
||||||
|
coroutineScope = this
|
||||||
|
)
|
||||||
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
||||||
// WHEN
|
// WHEN
|
||||||
fakeMessagesEntryPoint.callback?.onRoomDetailsClicked()
|
fakeMessagesEntryPoint.callback?.onRoomDetailsClicked()
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ class RootPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val presenter = createPresenter()
|
val presenter = createRootPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -54,7 +54,7 @@ class RootPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - passes app error state`() = runTest {
|
fun `present - passes app error state`() = runTest {
|
||||||
val presenter = createPresenter(
|
val presenter = createRootPresenter(
|
||||||
appErrorService = DefaultAppErrorStateService().apply {
|
appErrorService = DefaultAppErrorStateService().apply {
|
||||||
showError("Bad news", "Something bad happened")
|
showError("Bad news", "Something bad happened")
|
||||||
}
|
}
|
||||||
|
|
@ -75,7 +75,7 @@ class RootPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createPresenter(
|
private fun createRootPresenter(
|
||||||
appErrorService: AppErrorStateService = DefaultAppErrorStateService()
|
appErrorService: AppErrorStateService = DefaultAppErrorStateService()
|
||||||
): RootPresenter {
|
): RootPresenter {
|
||||||
val crashDataStore = FakeCrashDataStore()
|
val crashDataStore = FakeCrashDataStore()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.appnav.di
|
||||||
|
|
||||||
|
import com.bumble.appyx.core.state.MutableSavedStateMapImpl
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
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.auth.FakeAuthenticationService
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class MatrixClientsHolderTest {
|
||||||
|
@Test
|
||||||
|
fun `test getOrNull`() {
|
||||||
|
val fakeAuthenticationService = FakeAuthenticationService()
|
||||||
|
val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService)
|
||||||
|
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test getOrRestore`() = runTest {
|
||||||
|
val fakeAuthenticationService = FakeAuthenticationService()
|
||||||
|
val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService)
|
||||||
|
val fakeMatrixClient = FakeMatrixClient()
|
||||||
|
fakeAuthenticationService.givenMatrixClient(fakeMatrixClient)
|
||||||
|
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNull()
|
||||||
|
assertThat(matrixClientsHolder.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient)
|
||||||
|
// Do it again to it the cache
|
||||||
|
assertThat(matrixClientsHolder.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient)
|
||||||
|
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test remove`() = runTest {
|
||||||
|
val fakeAuthenticationService = FakeAuthenticationService()
|
||||||
|
val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService)
|
||||||
|
val fakeMatrixClient = FakeMatrixClient()
|
||||||
|
fakeAuthenticationService.givenMatrixClient(fakeMatrixClient)
|
||||||
|
assertThat(matrixClientsHolder.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient)
|
||||||
|
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient)
|
||||||
|
// Remove
|
||||||
|
matrixClientsHolder.remove(A_SESSION_ID)
|
||||||
|
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test remove all`() = runTest {
|
||||||
|
val fakeAuthenticationService = FakeAuthenticationService()
|
||||||
|
val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService)
|
||||||
|
val fakeMatrixClient = FakeMatrixClient()
|
||||||
|
fakeAuthenticationService.givenMatrixClient(fakeMatrixClient)
|
||||||
|
assertThat(matrixClientsHolder.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient)
|
||||||
|
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient)
|
||||||
|
// Remove all
|
||||||
|
matrixClientsHolder.removeAll()
|
||||||
|
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test save and restore`() = runTest {
|
||||||
|
val fakeAuthenticationService = FakeAuthenticationService()
|
||||||
|
val matrixClientsHolder = MatrixClientsHolder(fakeAuthenticationService)
|
||||||
|
val fakeMatrixClient = FakeMatrixClient()
|
||||||
|
fakeAuthenticationService.givenMatrixClient(fakeMatrixClient)
|
||||||
|
matrixClientsHolder.getOrRestore(A_SESSION_ID)
|
||||||
|
val savedStateMap = MutableSavedStateMapImpl { true }
|
||||||
|
matrixClientsHolder.saveIntoSavedState(savedStateMap)
|
||||||
|
assertThat(savedStateMap.size).isEqualTo(1)
|
||||||
|
// Test Restore with non-empty map
|
||||||
|
matrixClientsHolder.restoreWithSavedState(savedStateMap)
|
||||||
|
// Empty the map
|
||||||
|
matrixClientsHolder.removeAll()
|
||||||
|
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isNull()
|
||||||
|
// Restore again
|
||||||
|
matrixClientsHolder.restoreWithSavedState(savedStateMap)
|
||||||
|
assertThat(matrixClientsHolder.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -42,7 +42,7 @@ class LoggedInPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val presenter = createPresenter()
|
val presenter = createLoggedInPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -54,7 +54,7 @@ class LoggedInPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - show sync spinner`() = runTest {
|
fun `present - show sync spinner`() = runTest {
|
||||||
val roomListService = FakeRoomListService()
|
val roomListService = FakeRoomListService()
|
||||||
val presenter = createPresenter(roomListService, NetworkStatus.Online)
|
val presenter = createLoggedInPresenter(roomListService, NetworkStatus.Online)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -66,7 +66,7 @@ class LoggedInPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createPresenter(
|
private fun createLoggedInPresenter(
|
||||||
roomListService: RoomListService = FakeRoomListService(),
|
roomListService: RoomListService = FakeRoomListService(),
|
||||||
networkStatus: NetworkStatus = NetworkStatus.Offline
|
networkStatus: NetworkStatus = NetworkStatus.Offline
|
||||||
): LoggedInPresenter {
|
): LoggedInPresenter {
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<Delete>("clean").configure {
|
tasks.register<Delete>("clean").configure {
|
||||||
delete(rootProject.buildDir)
|
delete(rootProject.layout.buildDirectory)
|
||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
|
|
@ -62,7 +62,7 @@ allprojects {
|
||||||
config.from(files("$rootDir/tools/detekt/detekt.yml"))
|
config.from(files("$rootDir/tools/detekt/detekt.yml"))
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
detektPlugins("io.nlopez.compose.rules:detekt:0.2.3")
|
detektPlugins("io.nlopez.compose.rules:detekt:0.3.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
// KtLint
|
// KtLint
|
||||||
|
|
@ -86,7 +86,7 @@ allprojects {
|
||||||
reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.CHECKSTYLE)
|
reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.CHECKSTYLE)
|
||||||
}
|
}
|
||||||
filter {
|
filter {
|
||||||
exclude { element -> element.file.path.contains("$buildDir/generated/") }
|
exclude { element -> element.file.path.contains("${layout.buildDirectory.asFile.get()}/generated/") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Dependency check
|
// Dependency check
|
||||||
|
|
@ -176,10 +176,13 @@ koverMerged {
|
||||||
"*_ModuleKt",
|
"*_ModuleKt",
|
||||||
"anvil.hint.binding.io.element.*",
|
"anvil.hint.binding.io.element.*",
|
||||||
"anvil.hint.merge.*",
|
"anvil.hint.merge.*",
|
||||||
|
"anvil.hint.multibinding.io.element.*",
|
||||||
"anvil.module.*",
|
"anvil.module.*",
|
||||||
"com.airbnb.android.showkase*",
|
"com.airbnb.android.showkase*",
|
||||||
"io.element.android.libraries.designsystem.showkase.*",
|
"io.element.android.libraries.designsystem.showkase.*",
|
||||||
|
"io.element.android.x.di.DaggerAppComponent*",
|
||||||
"*_Factory",
|
"*_Factory",
|
||||||
|
"*_Factory_Impl",
|
||||||
"*_Factory$*",
|
"*_Factory$*",
|
||||||
"*_Module",
|
"*_Module",
|
||||||
"*_Module$*",
|
"*_Module$*",
|
||||||
|
|
@ -228,11 +231,11 @@ koverMerged {
|
||||||
name = "Global minimum code coverage."
|
name = "Global minimum code coverage."
|
||||||
target = kotlinx.kover.api.VerificationTarget.ALL
|
target = kotlinx.kover.api.VerificationTarget.ALL
|
||||||
bound {
|
bound {
|
||||||
minValue = 55
|
minValue = 60
|
||||||
// Setting a max value, so that if coverage is bigger, it means that we have to change minValue.
|
// Setting a max value, so that if coverage is bigger, it means that we have to change minValue.
|
||||||
// For instance if we have minValue = 20 and maxValue = 30, and current code coverage is now 31.32%, update
|
// For instance if we have minValue = 20 and maxValue = 30, and current code coverage is now 31.32%, update
|
||||||
// minValue to 25 and maxValue to 35.
|
// minValue to 25 and maxValue to 35.
|
||||||
maxValue = 65
|
maxValue = 70
|
||||||
counter = kotlinx.kover.api.CounterType.INSTRUCTION
|
counter = kotlinx.kover.api.CounterType.INSTRUCTION
|
||||||
valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE
|
valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE
|
||||||
}
|
}
|
||||||
|
|
@ -247,6 +250,10 @@ koverMerged {
|
||||||
excludes += "io.element.android.appnav.loggedin.LoggedInPresenter$*"
|
excludes += "io.element.android.appnav.loggedin.LoggedInPresenter$*"
|
||||||
// Some options can't be tested at the moment
|
// Some options can't be tested at the moment
|
||||||
excludes += "io.element.android.features.preferences.impl.developer.DeveloperSettingsPresenter$*"
|
excludes += "io.element.android.features.preferences.impl.developer.DeveloperSettingsPresenter$*"
|
||||||
|
// Temporary until we have actually something to test.
|
||||||
|
excludes += "io.element.android.features.lockscreen.impl.auth.PinAuthenticationPresenter"
|
||||||
|
excludes += "io.element.android.features.lockscreen.impl.auth.PinAuthenticationPresenter$*"
|
||||||
|
excludes += "io.element.android.features.lockscreen.impl.create.CreatePinPresenter"
|
||||||
}
|
}
|
||||||
bound {
|
bound {
|
||||||
minValue = 85
|
minValue = 85
|
||||||
|
|
@ -354,7 +361,7 @@ subprojects {
|
||||||
subprojects {
|
subprojects {
|
||||||
tasks.withType<KspTask>() {
|
tasks.withType<KspTask>() {
|
||||||
doLast {
|
doLast {
|
||||||
fileTree(buildDir).apply { include("**/*ShowkaseExtension*.kt") }.files.forEach { file ->
|
fileTree(layout.buildDirectory).apply { include("**/*ShowkaseExtension*.kt") }.files.forEach { file ->
|
||||||
ReplaceRegExp().apply {
|
ReplaceRegExp().apply {
|
||||||
setMatch("^public fun Showkase.getMetadata")
|
setMatch("^public fun Showkase.getMetadata")
|
||||||
setReplace("@Suppress(\"DEPRECATION\") public fun Showkase.getMetadata")
|
setReplace("@Suppress(\"DEPRECATION\") public fun Showkase.getMetadata")
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
[Rich text editor] Add link support to rich text editor
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Fix ANR on RoomList when notification settings change.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
[DI] Rework how dagger components are created and provided.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Let the SDK figure the best scheme given an homeserver URL (thus allowing HTTP homeservers)
|
|
||||||
1
changelog.d/1481.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Always register the pusher when application starts
|
||||||
1
changelog.d/1519.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Ensure screen does not turn off when playing a video
|
||||||
1
changelog.d/1563.misc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Remove usage of blocking methods.
|
||||||
|
|
@ -117,21 +117,16 @@ You can also have access to the aars through the [release](https://github.com/ma
|
||||||
|
|
||||||
#### Build the SDK locally
|
#### Build the SDK locally
|
||||||
|
|
||||||
|
Easiest way: run the script [./tools/sdk/build_rust_sdk.sh](./tools/sdk/build_rust_sdk.sh) and just answer the questions.
|
||||||
|
|
||||||
|
Legacy way:
|
||||||
|
|
||||||
If you need to locally build the sdk-android you can use
|
If you need to locally build the sdk-android you can use
|
||||||
the [build](https://github.com/matrix-org/matrix-rust-components-kotlin/blob/main/scripts/build.sh) script.
|
the [build](https://github.com/matrix-org/matrix-rust-components-kotlin/blob/main/scripts/build.sh) script.
|
||||||
|
|
||||||
For this, you first need to ensure to setup :
|
For this please check the [prerequisites](https://github.com/matrix-org/matrix-rust-components-kotlin/blob/main/README.md#prerequisites) from the repo.
|
||||||
|
|
||||||
- rust environment (check https://rust-lang.github.io/rustup/ if needed)
|
Checkout both [matrix-rust-sdk](https://github.com/matrix-org/matrix-rust-sdk) and [matrix-rust-components-kotlin](https://github.com/matrix-org/matrix-rust-components-kotlin) repositories
|
||||||
- cargo-ndk < 2.12.0
|
|
||||||
```shell
|
|
||||||
cargo install cargo-ndk --version 2.11.0
|
|
||||||
```
|
|
||||||
- android targets
|
|
||||||
```shell
|
|
||||||
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android
|
|
||||||
```
|
|
||||||
- checkout both [matrix-rust-sdk](https://github.com/matrix-org/matrix-rust-sdk) and [matrix-rust-components-kotlin](https://github.com/matrix-org/matrix-rust-components-kotlin) repositories
|
|
||||||
```shell
|
```shell
|
||||||
git clone git@github.com:matrix-org/matrix-rust-sdk.git
|
git clone git@github.com:matrix-org/matrix-rust-sdk.git
|
||||||
git clone git@github.com:matrix-org/matrix-rust-components-kotlin.git
|
git clone git@github.com:matrix-org/matrix-rust-components-kotlin.git
|
||||||
|
|
@ -151,14 +146,10 @@ So for example to build the sdk against aarch64-linux-android target and copy th
|
||||||
./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
|
./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
|
||||||
```
|
```
|
||||||
|
|
||||||
Finally let the `matrix/impl` module use this aar by changing the dependencies from `libs.matrix.sdk` to `projects.libraries.rustsdk`:
|
Troubleshooting:
|
||||||
|
- You may need to set `ANDROID_NDK_HOME` e.g `export ANDROID_NDK_HOME=~/Library/Android/sdk/ndk`.
|
||||||
```groovy
|
- If you get the error `thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', .cargo/registry/src/index.crates.io-6f17d22bba15001f/cargo-ndk-2.11.0/src/cli.rs:345:18` try updating your Cargo NDK version. In this case, 2.11.0 is too old so `cargo install cargo-ndk` to install a newer version.
|
||||||
dependencies {
|
- If you get the error `Unsupported class file major version 64` try changing your JVM version. In this case, Java 20 is not supported in Gradle yet, so downgrade to an earlier version (Java 17 worked in this case).
|
||||||
api(projects.libraries.rustsdk) // <- use the local version of the sdk. Uncomment this line.
|
|
||||||
//implementation(libs.matrix.sdk) // <- use the released version. Comment this line.
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
You are good to test your local rust development now!
|
You are good to test your local rust development now!
|
||||||
|
|
||||||
|
|
@ -280,11 +271,12 @@ Follow these steps to install and configure the plugin and templates:
|
||||||
|
|
||||||
1. Install the AS plugin for generating modules :
|
1. Install the AS plugin for generating modules :
|
||||||
[Generate Module from Template](https://plugins.jetbrains.com/plugin/13586-generate-module-from-template)
|
[Generate Module from Template](https://plugins.jetbrains.com/plugin/13586-generate-module-from-template)
|
||||||
2. Import file templates in AS :
|
2. Run the script `tools/templates/generate_templates.sh` to generate the template zip file
|
||||||
|
3. Import file templates in AS :
|
||||||
- Navigate to File/Manage IDE Settings/Import Settings
|
- Navigate to File/Manage IDE Settings/Import Settings
|
||||||
- Pick the `tools/templates/file_templates.zip` files
|
- Pick the `tmp/file_templates.zip` files
|
||||||
- Click on OK
|
- Click on OK
|
||||||
3. Configure generate-module-from-template plugin :
|
4. Configure generate-module-from-template plugin :
|
||||||
- Navigate to AS/Settings/Tools/Module Template Settings
|
- Navigate to AS/Settings/Tools/Module Template Settings
|
||||||
- Click on + / Import From File
|
- Click on + / Import From File
|
||||||
- Pick the `tools/templates/FeatureModule.json`
|
- Pick the `tools/templates/FeatureModule.json`
|
||||||
|
|
|
||||||
22
docs/debug_proxying.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Setup a debug mitm proxy to inspect all the app's network traffic
|
||||||
|
|
||||||
|
1) Install mitmproxy: `brew install mitmproxy`.
|
||||||
|
1) Launch `mitmweb` from a terminal. It will pop up mitmproxy's web interface in a web browser.
|
||||||
|
1) Configure Android Emulator.
|
||||||
|
1) Launch your android emulator.
|
||||||
|
1) Open its settings page and go to Settings -> Proxy (nb this tab isn't visible when running the emu inside the Android Studio window, you need to set it so it runs in its own window).
|
||||||
|
1) Disable "Use Android Studio HTTP proxy settings" and pick "Manual proxy configuration".
|
||||||
|
1) Set `127.0.0.1` as "Host name" and `8080` as "Port number".
|
||||||
|
1) Click "Apply" and verify that "Proxy status" is "Success" and close the settings window.
|
||||||
|
<img width="932" alt="Screenshot 2023-10-04 at 14 48 47" src="https://github.com/vector-im/element-x-android/assets/1273124/bf99a053-20b0-42a4-91d3-9602f709f684">
|
||||||
|
1) Install the mitmproxy CA cert (this is needed to see traffic from java/kotlin code, it's not needed for traffic coming from native code e.g. the matrix-rust-sdk).
|
||||||
|
1) Open the emulator Chrome browser app
|
||||||
|
1) Go to the url `mitm.it`
|
||||||
|
1) Follow the instructions to install the CA cert on Android devices.
|
||||||
|
<img width="606" alt="Screenshot 2023-10-04 at 14 51 27" src="https://github.com/vector-im/element-x-android/assets/1273124/5f2b6f27-6958-4ea7-97fe-c7f06d105da5">
|
||||||
|
1) Slightly modify the Element X app source code.
|
||||||
|
1) Go to the `RustMatrixClientFactory.create()` method.
|
||||||
|
1) Add `.disableSslVerification()` in the `ClientBuilder` method chain.
|
||||||
|
1) Build and run the Element X app.
|
||||||
|
1) Enjoy, you will see all the traffic in mitmproxy's web interface.
|
||||||
|
<img width="1110" alt="Screenshot 2023-10-04 at 14 50 03" src="https://github.com/vector-im/element-x-android/assets/1273124/5d039efd-448d-426c-a384-dbbceb9f33ac">
|
||||||
2
fastlane/metadata/android/en-US/changelogs/40002010.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
Main changes in this version: Element Call, design update, bugfixes
|
||||||
|
Full changelog: https://github.com/vector-im/element-x-android/releases
|
||||||
2
fastlane/metadata/android/en-US/changelogs/40002020.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
Main changes in this version: bugfixes
|
||||||
|
Full changelog: https://github.com/vector-im/element-x-android/releases
|
||||||
2
fastlane/metadata/android/en-US/changelogs/40002030.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
Main changes in this version: bugfixes.
|
||||||
|
Full changelog: https://github.com/vector-im/element-x-android/releases
|
||||||
2
fastlane/metadata/android/en-US/changelogs/40002040.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
Main changes in this version: bugfixes.
|
||||||
|
Full changelog: https://github.com/vector-im/element-x-android/releases
|
||||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 391 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 221 KiB |
|
Before Width: | Height: | Size: 694 KiB After Width: | Height: | Size: 2 MiB |
|
Before Width: | Height: | Size: 1,024 KiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 934 KiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 675 KiB After Width: | Height: | Size: 1.3 MiB |
|
|
@ -24,7 +24,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
||||||
import io.element.android.libraries.designsystem.components.LINK_TAG
|
import io.element.android.libraries.designsystem.components.LINK_TAG
|
||||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart
|
import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart
|
||||||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||||
|
|
@ -70,7 +70,7 @@ fun AnalyticsPreferencesView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@DayNightPreviews
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AnalyticsPreferencesViewPreview(@PreviewParameter(AnalyticsPreferencesStateProvider::class) state: AnalyticsPreferencesState) =
|
internal fun AnalyticsPreferencesViewPreview(@PreviewParameter(AnalyticsPreferencesStateProvider::class) state: AnalyticsPreferencesState) =
|
||||||
ElementPreview {
|
ElementPreview {
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.text.ClickableText
|
import androidx.compose.foundation.text.ClickableText
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Poll
|
import androidx.compose.material.icons.filled.Poll
|
||||||
import androidx.compose.material.icons.rounded.Check
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
|
@ -45,10 +44,10 @@ import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
||||||
import io.element.android.features.analytics.api.Config
|
import io.element.android.features.analytics.api.Config
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
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.molecules.IconTitleSubtitleMolecule
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.InfoListItem
|
import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.InfoListOrganism
|
import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism
|
||||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart
|
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.Button
|
||||||
|
|
@ -56,6 +55,7 @@ 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.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.designsystem.theme.temporaryColorBgSpecial
|
import io.element.android.libraries.designsystem.theme.temporaryColorBgSpecial
|
||||||
|
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||||
import io.element.android.libraries.designsystem.utils.LogCompositions
|
import io.element.android.libraries.designsystem.utils.LogCompositions
|
||||||
import io.element.android.libraries.theme.ElementTheme
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
@ -147,7 +147,7 @@ private fun CheckIcon(modifier: Modifier = Modifier) {
|
||||||
.size(20.dp)
|
.size(20.dp)
|
||||||
.background(color = MaterialTheme.colorScheme.background, shape = CircleShape)
|
.background(color = MaterialTheme.colorScheme.background, shape = CircleShape)
|
||||||
.padding(2.dp),
|
.padding(2.dp),
|
||||||
imageVector = Icons.Rounded.Check,
|
resourceId = CommonDrawables.ic_compound_check,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = ElementTheme.colors.textActionAccent,
|
tint = ElementTheme.colors.textActionAccent,
|
||||||
)
|
)
|
||||||
|
|
@ -209,7 +209,7 @@ private fun AnalyticsOptInFooter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@DayNightPreviews
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AnalyticsOptInViewPreview(@PreviewParameter(AnalyticsOptInStateProvider::class) state: AnalyticsOptInState) = ElementPreview {
|
internal fun AnalyticsOptInViewPreview(@PreviewParameter(AnalyticsOptInStateProvider::class) state: AnalyticsOptInState) = ElementPreview {
|
||||||
AnalyticsOptInView(
|
AnalyticsOptInView(
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_analytics_prompt_data_usage">"Wir zeichnen keine persönlichen Daten auf und erstellen keine Profile."</string>
|
<string name="screen_analytics_prompt_data_usage">"Wir zeichnen keine persönlichen Daten auf und erstellen keine Profile."</string>
|
||||||
<string name="screen_analytics_prompt_help_us_improve">"Teilen Sie anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."</string>
|
<string name="screen_analytics_prompt_help_us_improve">"Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."</string>
|
||||||
<string name="screen_analytics_prompt_read_terms">"Sie können alle unsere Bedingungen lesen%1$s."</string>
|
<string name="screen_analytics_prompt_read_terms">"Du kannst alle unsere Bedingungen lesen %1$s."</string>
|
||||||
<string name="screen_analytics_prompt_read_terms_content_link">"hier"</string>
|
<string name="screen_analytics_prompt_read_terms_content_link">"hier"</string>
|
||||||
<string name="screen_analytics_prompt_settings">"Du kannst diese Funktion jederzeit deaktivieren"</string>
|
<string name="screen_analytics_prompt_settings">"Du kannst diese Funktion jederzeit deaktivieren"</string>
|
||||||
<string name="screen_analytics_prompt_third_party_sharing">"Wir geben deine Daten nicht an Dritte weiter"</string>
|
<string name="screen_analytics_prompt_third_party_sharing">"Wir geben deine Daten nicht an Dritte weiter"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_analytics_prompt_data_usage">"我們不會紀錄或剖繪您的個人資料"</string>
|
<string name="screen_analytics_prompt_data_usage">"我們不會紀錄或剖繪您的個人資料"</string>
|
||||||
<string name="screen_analytics_prompt_help_us_improve">"分享匿名的使用數據以協助我們釐清問題"</string>
|
<string name="screen_analytics_prompt_help_us_improve">"分享匿名的使用數據以協助我們釐清問題。"</string>
|
||||||
<string name="screen_analytics_prompt_read_terms">"您可以到 %1$s 閱讀我們的條款。"</string>
|
<string name="screen_analytics_prompt_read_terms">"您可以到%1$s閱讀我們的條款。"</string>
|
||||||
<string name="screen_analytics_prompt_read_terms_content_link">"這裡"</string>
|
<string name="screen_analytics_prompt_read_terms_content_link">"這裡"</string>
|
||||||
<string name="screen_analytics_prompt_settings">"您可以在任何時候關閉它"</string>
|
<string name="screen_analytics_prompt_settings">"您可以在任何時候關閉它"</string>
|
||||||
<string name="screen_analytics_prompt_third_party_sharing">"我們不會和第三方分享您的資料"</string>
|
<string name="screen_analytics_prompt_third_party_sharing">"我們不會和第三方分享您的資料"</string>
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,6 @@
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data android:scheme="http" />
|
|
||||||
<data android:scheme="https" />
|
<data android:scheme="https" />
|
||||||
|
|
||||||
<data android:host="call.element.io" />
|
<data android:host="call.element.io" />
|
||||||
|
|
@ -53,6 +52,14 @@
|
||||||
<data android:scheme="element" />
|
<data android:scheme="element" />
|
||||||
<data android:host="call" />
|
<data android:host="call" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<!-- Custom scheme to handle urls from other domains in the format: io.element.call:/?url=https%3A%2F%2Felement.io -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="io.element.call" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
<service android:name=".CallForegroundService" android:enabled="true" android:foregroundServiceType="mediaPlayback" />
|
<service android:name=".CallForegroundService" android:enabled="true" android:foregroundServiceType="mediaPlayback" />
|
||||||
|
|
|
||||||
|
|
@ -17,29 +17,88 @@
|
||||||
package io.element.android.features.call
|
package io.element.android.features.call
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import java.net.URLDecoder
|
import javax.inject.Inject
|
||||||
|
|
||||||
object CallIntentDataParser {
|
class CallIntentDataParser @Inject constructor() {
|
||||||
|
|
||||||
private val validHttpSchemes = sequenceOf("http", "https")
|
private val validHttpSchemes = sequenceOf("https")
|
||||||
|
|
||||||
fun parse(data: String?): String? {
|
fun parse(data: String?): String? {
|
||||||
val parsedUrl = data?.let { Uri.parse(data) } ?: return null
|
val parsedUrl = data?.let { Uri.parse(data) } ?: return null
|
||||||
val scheme = parsedUrl.scheme
|
val scheme = parsedUrl.scheme
|
||||||
return when {
|
return when {
|
||||||
scheme in validHttpSchemes && parsedUrl.host == "call.element.io" -> data
|
scheme in validHttpSchemes && parsedUrl.host == "call.element.io" -> parsedUrl
|
||||||
scheme == "element" && parsedUrl.host == "call" -> {
|
scheme == "element" && parsedUrl.host == "call" -> {
|
||||||
// We use this custom scheme to load arbitrary URLs for other instances of Element Call,
|
// We use this custom scheme to load arbitrary URLs for other instances of Element Call,
|
||||||
// so we can only verify it's an HTTP/HTTPs URL with a non-empty host
|
// so we can only verify it's an HTTP/HTTPs URL with a non-empty host
|
||||||
parsedUrl.getQueryParameter("url")
|
parsedUrl.getUrlParameter()
|
||||||
?.let { URLDecoder.decode(it, "utf-8") }
|
}
|
||||||
?.takeIf {
|
scheme == "io.element.call" && parsedUrl.host == null -> {
|
||||||
val internalUri = Uri.parse(it)
|
// We use this custom scheme to load arbitrary URLs for other instances of Element Call,
|
||||||
internalUri.scheme in validHttpSchemes && !internalUri.host.isNullOrBlank()
|
// so we can only verify it's an HTTP/HTTPs URL with a non-empty host
|
||||||
}
|
parsedUrl.getUrlParameter()
|
||||||
}
|
}
|
||||||
// This should never be possible, but we still need to take into account the possibility
|
// This should never be possible, but we still need to take into account the possibility
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}?.withCustomParameters()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Uri.getUrlParameter(): Uri? {
|
||||||
|
return getQueryParameter("url")
|
||||||
|
?.let { urlParameter ->
|
||||||
|
Uri.parse(urlParameter).takeIf { uri ->
|
||||||
|
uri.scheme in validHttpSchemes && !uri.host.isNullOrBlank()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the uri has the following parameters and value in the fragment:
|
||||||
|
* - appPrompt=false
|
||||||
|
* - confineToRoom=true
|
||||||
|
* to ensure that the rendering will bo correct on the embedded Webview.
|
||||||
|
*/
|
||||||
|
private fun Uri.withCustomParameters(): String {
|
||||||
|
val builder = buildUpon()
|
||||||
|
// Remove the existing query parameters
|
||||||
|
builder.clearQuery()
|
||||||
|
queryParameterNames.forEach {
|
||||||
|
if (it == APP_PROMPT_PARAMETER || it == CONFINE_TO_ROOM_PARAMETER) return@forEach
|
||||||
|
builder.appendQueryParameter(it, getQueryParameter(it))
|
||||||
|
}
|
||||||
|
// Remove the existing fragment parameters, and build the new fragment
|
||||||
|
val currentFragment = fragment ?: ""
|
||||||
|
// Reset the current fragment
|
||||||
|
builder.fragment("")
|
||||||
|
val queryFragmentPosition = currentFragment.lastIndexOf("?")
|
||||||
|
val newFragment = if (queryFragmentPosition == -1) {
|
||||||
|
// No existing query, build it.
|
||||||
|
"$currentFragment?$APP_PROMPT_PARAMETER=false&$CONFINE_TO_ROOM_PARAMETER=true"
|
||||||
|
} else {
|
||||||
|
buildString {
|
||||||
|
append(currentFragment.substring(0, queryFragmentPosition + 1))
|
||||||
|
val queryFragment = currentFragment.substring(queryFragmentPosition + 1)
|
||||||
|
// Replace the existing parameters
|
||||||
|
val newQueryFragment = queryFragment
|
||||||
|
.replace("$APP_PROMPT_PARAMETER=true", "$APP_PROMPT_PARAMETER=false")
|
||||||
|
.replace("$CONFINE_TO_ROOM_PARAMETER=false", "$CONFINE_TO_ROOM_PARAMETER=true")
|
||||||
|
append(newQueryFragment)
|
||||||
|
// Ensure the parameters are there
|
||||||
|
if (!newQueryFragment.contains("$APP_PROMPT_PARAMETER=false")) {
|
||||||
|
if (newQueryFragment.isNotEmpty()) {
|
||||||
|
append("&")
|
||||||
|
}
|
||||||
|
append("$APP_PROMPT_PARAMETER=false")
|
||||||
|
}
|
||||||
|
if (!newQueryFragment.contains("$CONFINE_TO_ROOM_PARAMETER=true")) {
|
||||||
|
append("&$CONFINE_TO_ROOM_PARAMETER=true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// We do not want to encode the Fragment part, so append it manually
|
||||||
|
return builder.build().toString() + "#" + newFragment
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val APP_PROMPT_PARAMETER = "appPrompt"
|
||||||
|
private const val CONFINE_TO_ROOM_PARAMETER = "confineToRoom"
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,6 @@ import android.webkit.WebView
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Close
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
@ -33,10 +31,11 @@ import androidx.compose.ui.platform.LocalInspectionMode
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
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.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||||
import io.element.android.libraries.theme.ElementTheme
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
|
|
||||||
typealias RequestPermissionCallback = (Array<String>) -> Unit
|
typealias RequestPermissionCallback = (Array<String>) -> Unit
|
||||||
|
|
@ -58,7 +57,7 @@ internal fun CallScreenView(
|
||||||
title = { Text(stringResource(R.string.element_call)) },
|
title = { Text(stringResource(R.string.element_call)) },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
BackButton(
|
BackButton(
|
||||||
imageVector = Icons.Default.Close,
|
resourceId = CommonDrawables.ic_compound_close,
|
||||||
onClick = onClose
|
onClick = onClose
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -138,7 +137,7 @@ private fun WebView.setup(userAgent: String, onPermissionsRequested: (Permission
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@DayNightPreviews
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun CallScreenViewPreview() {
|
internal fun CallScreenViewPreview() {
|
||||||
ElementTheme {
|
ElementTheme {
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ import javax.inject.Inject
|
||||||
class ElementCallActivity : ComponentActivity() {
|
class ElementCallActivity : ComponentActivity() {
|
||||||
|
|
||||||
@Inject lateinit var userAgentProvider: UserAgentProvider
|
@Inject lateinit var userAgentProvider: UserAgentProvider
|
||||||
|
@Inject lateinit var callIntentDataParser: CallIntentDataParser
|
||||||
|
|
||||||
private lateinit var audioManager: AudioManager
|
private lateinit var audioManager: AudioManager
|
||||||
|
|
||||||
|
|
@ -129,7 +130,7 @@ class ElementCallActivity : ComponentActivity() {
|
||||||
finishAndRemoveTask()
|
finishAndRemoveTask()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseUrl(url: String?): String? = CallIntentDataParser.parse(url)
|
private fun parseUrl(url: String?): String? = callIntentDataParser.parse(url)
|
||||||
|
|
||||||
private fun registerPermissionResultLauncher(): ActivityResultLauncher<Array<String>> {
|
private fun registerPermissionResultLauncher(): ActivityResultLauncher<Array<String>> {
|
||||||
return registerForActivityResult(
|
return registerForActivityResult(
|
||||||
|
|
|
||||||
6
features/call/src/main/res/values-cs/translations.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="call_foreground_service_channel_title_android">"Probíhající hovor"</string>
|
||||||
|
<string name="call_foreground_service_message_android">"Klepněte pro návrat k hovoru"</string>
|
||||||
|
<string name="call_foreground_service_title_android">"☎️ Probíhá hovor"</string>
|
||||||
|
</resources>
|
||||||
6
features/call/src/main/res/values-ru/translations.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="call_foreground_service_channel_title_android">"Текущий вызов"</string>
|
||||||
|
<string name="call_foreground_service_message_android">"Коснитесь, чтобы вернуться к вызову"</string>
|
||||||
|
<string name="call_foreground_service_title_android">"Идёт вызов"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="call_foreground_service_message_android">"點擊以返回到通話頁面"</string>
|
||||||
|
<string name="call_foreground_service_title_android">"☎️ 通話中"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,225 @@
|
||||||
|
/*
|
||||||
|
* 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.call
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
import java.net.URLEncoder
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
class CallIntentDataParserTest {
|
||||||
|
|
||||||
|
private val callIntentDataParser = CallIntentDataParser()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a null data returns null`() {
|
||||||
|
val url: String? = null
|
||||||
|
assertThat(callIntentDataParser.parse(url)).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `empty data returns null`() {
|
||||||
|
doTest("", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `invalid data returns null`() {
|
||||||
|
doTest("!", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `data with no scheme returns null`() {
|
||||||
|
doTest("test", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Element Call http urls returns null`() {
|
||||||
|
doTest("http://call.element.io", null)
|
||||||
|
doTest("http://call.element.io/some-actual-call?with=parameters", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Element Call urls will be returned as is`() {
|
||||||
|
doTest(
|
||||||
|
url = "https://call.element.io",
|
||||||
|
expectedResult = "https://call.element.io#?$EXTRA_PARAMS"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Element Call url with url param gets url extracted`() {
|
||||||
|
doTest(
|
||||||
|
url = VALID_CALL_URL_WITH_PARAM,
|
||||||
|
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `HTTP and HTTPS urls that don't come from EC return null`() {
|
||||||
|
doTest("http://app.element.io", null)
|
||||||
|
doTest("https://app.element.io", null, testEmbedded = false)
|
||||||
|
doTest("http://", null)
|
||||||
|
doTest("https://", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Element Call url with no url returns null`() {
|
||||||
|
val embeddedUrl = VALID_CALL_URL_WITH_PARAM
|
||||||
|
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
|
||||||
|
val url = "io.element.call:/?no_url=$encodedUrl"
|
||||||
|
assertThat(callIntentDataParser.parse(url)).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `element scheme with no call host returns null`() {
|
||||||
|
val embeddedUrl = VALID_CALL_URL_WITH_PARAM
|
||||||
|
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
|
||||||
|
val url = "element://no-call?url=$encodedUrl"
|
||||||
|
assertThat(callIntentDataParser.parse(url)).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `element scheme with no data returns null`() {
|
||||||
|
val url = "element://call?url="
|
||||||
|
assertThat(callIntentDataParser.parse(url)).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Element Call url with no data returns null`() {
|
||||||
|
val url = "io.element.call:/?url="
|
||||||
|
assertThat(callIntentDataParser.parse(url)).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `element invalid scheme returns null`() {
|
||||||
|
val embeddedUrl = VALID_CALL_URL_WITH_PARAM
|
||||||
|
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
|
||||||
|
val url = "bad.scheme:/?url=$encodedUrl"
|
||||||
|
assertThat(callIntentDataParser.parse(url)).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Element Call url with url extra param appPrompt gets url extracted`() {
|
||||||
|
doTest(
|
||||||
|
url = "${VALID_CALL_URL_WITH_PARAM}&appPrompt=true",
|
||||||
|
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Element Call url with url extra param in fragment appPrompt gets url extracted`() {
|
||||||
|
doTest(
|
||||||
|
url = "${VALID_CALL_URL_WITH_PARAM}#?appPrompt=true",
|
||||||
|
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?appPrompt=false&confineToRoom=true"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Element Call url with url extra param in fragment appPrompt and other gets url extracted`() {
|
||||||
|
doTest(
|
||||||
|
url = "${VALID_CALL_URL_WITH_PARAM}#?appPrompt=true&otherParam=maybe",
|
||||||
|
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?appPrompt=false&otherParam=maybe&confineToRoom=true"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Element Call url with url extra param confineToRoom gets url extracted`() {
|
||||||
|
doTest(
|
||||||
|
url = "${VALID_CALL_URL_WITH_PARAM}&confineToRoom=false",
|
||||||
|
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Element Call url with url extra param in fragment confineToRoom gets url extracted`() {
|
||||||
|
doTest(
|
||||||
|
url = "${VALID_CALL_URL_WITH_PARAM}#?confineToRoom=false",
|
||||||
|
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?confineToRoom=true&appPrompt=false"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Element Call url with url extra param in fragment confineToRoom and more gets url extracted`() {
|
||||||
|
doTest(
|
||||||
|
url = "${VALID_CALL_URL_WITH_PARAM}#?confineToRoom=false&otherParam=maybe",
|
||||||
|
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?confineToRoom=true&otherParam=maybe&appPrompt=false"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Element Call url with url fragment gets url extracted`() {
|
||||||
|
doTest(
|
||||||
|
url = "${VALID_CALL_URL_WITH_PARAM}#fragment",
|
||||||
|
expectedResult = "$VALID_CALL_URL_WITH_PARAM#fragment?$EXTRA_PARAMS"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Element Call url with url fragment with params gets url extracted`() {
|
||||||
|
doTest(
|
||||||
|
url = "${VALID_CALL_URL_WITH_PARAM}#fragment?otherParam=maybe",
|
||||||
|
expectedResult = "$VALID_CALL_URL_WITH_PARAM#fragment?otherParam=maybe&$EXTRA_PARAMS"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Element Call url with url fragment with other params gets url extracted`() {
|
||||||
|
doTest(
|
||||||
|
url = "${VALID_CALL_URL_WITH_PARAM}#?otherParam=maybe",
|
||||||
|
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?otherParam=maybe&$EXTRA_PARAMS"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Element Call url with empty fragment`() {
|
||||||
|
doTest(
|
||||||
|
url = "${VALID_CALL_URL_WITH_PARAM}#",
|
||||||
|
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Element Call url with empty fragment query`() {
|
||||||
|
doTest(
|
||||||
|
url = "${VALID_CALL_URL_WITH_PARAM}#?",
|
||||||
|
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun doTest(url: String, expectedResult: String?, testEmbedded: Boolean = true) {
|
||||||
|
// Test direct parsing
|
||||||
|
assertThat(callIntentDataParser.parse(url)).isEqualTo(expectedResult)
|
||||||
|
|
||||||
|
if (testEmbedded) {
|
||||||
|
// Test embedded url, scheme 1
|
||||||
|
val encodedUrl = URLEncoder.encode(url, "utf-8")
|
||||||
|
val urlScheme1 = "element://call?url=$encodedUrl"
|
||||||
|
assertThat(callIntentDataParser.parse(urlScheme1)).isEqualTo(expectedResult)
|
||||||
|
|
||||||
|
// Test embedded url, scheme 2
|
||||||
|
val urlScheme2 = "io.element.call:/?url=$encodedUrl"
|
||||||
|
assertThat(callIntentDataParser.parse(urlScheme2)).isEqualTo(expectedResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val VALID_CALL_URL_WITH_PARAM = "https://call.element.io/some-actual-call?with=parameters"
|
||||||
|
const val EXTRA_PARAMS = "appPrompt=false&confineToRoom=true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,105 +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.call
|
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.robolectric.RobolectricTestRunner
|
|
||||||
import java.net.URLEncoder
|
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner::class)
|
|
||||||
class CallIntentDataParserTests {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `a null data returns null`() {
|
|
||||||
val url: String? = null
|
|
||||||
assertThat(CallIntentDataParser.parse(url)).isNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `empty data returns null`() {
|
|
||||||
val url = ""
|
|
||||||
assertThat(CallIntentDataParser.parse(url)).isNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `invalid data returns null`() {
|
|
||||||
val url = "!"
|
|
||||||
assertThat(CallIntentDataParser.parse(url)).isNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `data with no scheme returns null`() {
|
|
||||||
val url = "test"
|
|
||||||
assertThat(CallIntentDataParser.parse(url)).isNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Element Call urls will be returned as is`() {
|
|
||||||
val httpBaseUrl = "http://call.element.io"
|
|
||||||
val httpCallUrl = "http://call.element.io/some-actual-call?with=parameters"
|
|
||||||
val httpsBaseUrl = "https://call.element.io"
|
|
||||||
val httpsCallUrl = "https://call.element.io/some-actual-call?with=parameters"
|
|
||||||
assertThat(CallIntentDataParser.parse(httpBaseUrl)).isEqualTo(httpBaseUrl)
|
|
||||||
assertThat(CallIntentDataParser.parse(httpCallUrl)).isEqualTo(httpCallUrl)
|
|
||||||
assertThat(CallIntentDataParser.parse(httpsBaseUrl)).isEqualTo(httpsBaseUrl)
|
|
||||||
assertThat(CallIntentDataParser.parse(httpsCallUrl)).isEqualTo(httpsCallUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `HTTP and HTTPS urls that don't come from EC return null`() {
|
|
||||||
val httpBaseUrl = "http://app.element.io"
|
|
||||||
val httpsBaseUrl = "https://app.element.io"
|
|
||||||
val httpInvalidUrl = "http://"
|
|
||||||
val httpsInvalidUrl = "http://"
|
|
||||||
assertThat(CallIntentDataParser.parse(httpBaseUrl)).isNull()
|
|
||||||
assertThat(CallIntentDataParser.parse(httpsBaseUrl)).isNull()
|
|
||||||
assertThat(CallIntentDataParser.parse(httpInvalidUrl)).isNull()
|
|
||||||
assertThat(CallIntentDataParser.parse(httpsInvalidUrl)).isNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `element scheme with call host and url param gets url extracted`() {
|
|
||||||
val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters"
|
|
||||||
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
|
|
||||||
val url = "element://call?url=$encodedUrl"
|
|
||||||
assertThat(CallIntentDataParser.parse(url)).isEqualTo(embeddedUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `element scheme with call host and no url param returns null`() {
|
|
||||||
val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters"
|
|
||||||
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
|
|
||||||
val url = "element://call?no-url=$encodedUrl"
|
|
||||||
assertThat(CallIntentDataParser.parse(url)).isNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `element scheme with no call host returns null`() {
|
|
||||||
val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters"
|
|
||||||
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
|
|
||||||
val url = "element://no-call?url=$encodedUrl"
|
|
||||||
assertThat(CallIntentDataParser.parse(url)).isNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `element scheme with no data returns null`() {
|
|
||||||
val url = "element://call?url="
|
|
||||||
assertThat(CallIntentDataParser.parse(url)).isNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -48,6 +48,7 @@ dependencies {
|
||||||
implementation(projects.libraries.deeplink)
|
implementation(projects.libraries.deeplink)
|
||||||
implementation(projects.libraries.mediapickers.api)
|
implementation(projects.libraries.mediapickers.api)
|
||||||
implementation(projects.libraries.mediaupload.api)
|
implementation(projects.libraries.mediaupload.api)
|
||||||
|
implementation(projects.libraries.permissions.api)
|
||||||
implementation(projects.libraries.usersearch.impl)
|
implementation(projects.libraries.usersearch.impl)
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
|
|
@ -64,6 +65,7 @@ dependencies {
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.libraries.mediapickers.test)
|
testImplementation(projects.libraries.mediapickers.test)
|
||||||
testImplementation(projects.libraries.mediaupload.test)
|
testImplementation(projects.libraries.mediaupload.test)
|
||||||
|
testImplementation(projects.libraries.permissions.test)
|
||||||
testImplementation(projects.libraries.usersearch.test)
|
testImplementation(projects.libraries.usersearch.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ import io.element.android.features.createroom.impl.components.UserListView
|
||||||
import io.element.android.features.createroom.impl.userlist.UserListEvents
|
import io.element.android.features.createroom.impl.userlist.UserListEvents
|
||||||
import io.element.android.features.createroom.impl.userlist.UserListState
|
import io.element.android.features.createroom.impl.userlist.UserListState
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
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.Scaffold
|
||||||
|
|
@ -84,7 +84,7 @@ fun AddPeopleView(
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AddPeopleViewTopBar(
|
private fun AddPeopleViewTopBar(
|
||||||
hasSelectedUsers: Boolean,
|
hasSelectedUsers: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onBackPressed: () -> Unit = {},
|
onBackPressed: () -> Unit = {},
|
||||||
|
|
@ -109,7 +109,7 @@ fun AddPeopleViewTopBar(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@DayNightPreviews
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AddPeopleViewPreview(@PreviewParameter(AddPeopleUserListStateProvider::class) state: UserListState) = ElementPreview {
|
internal fun AddPeopleViewPreview(@PreviewParameter(AddPeopleUserListStateProvider::class) state: UserListState) = ElementPreview {
|
||||||
AddPeopleView(state = state)
|
AddPeopleView(state = state)
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.features.createroom.impl.configureroom.RoomPrivacyItem
|
import io.element.android.features.createroom.impl.configureroom.RoomPrivacyItem
|
||||||
import io.element.android.features.createroom.impl.configureroom.roomPrivacyItems
|
import io.element.android.features.createroom.impl.configureroom.roomPrivacyItems
|
||||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
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.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.RadioButton
|
import io.element.android.libraries.designsystem.theme.components.RadioButton
|
||||||
|
|
@ -57,7 +57,7 @@ fun RoomPrivacyOption(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier.padding(horizontal = 8.dp),
|
modifier = Modifier.padding(horizontal = 8.dp),
|
||||||
imageVector = roomPrivacyItem.icon,
|
resourceId = roomPrivacyItem.icon,
|
||||||
contentDescription = "",
|
contentDescription = "",
|
||||||
tint = MaterialTheme.colorScheme.secondary,
|
tint = MaterialTheme.colorScheme.secondary,
|
||||||
)
|
)
|
||||||
|
|
@ -90,7 +90,7 @@ fun RoomPrivacyOption(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@DayNightPreviews
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun RoomPrivacyOptionPreview() = ElementPreview {
|
internal fun RoomPrivacyOptionPreview() = ElementPreview {
|
||||||
val aRoomPrivacyItem = roomPrivacyItems().first()
|
val aRoomPrivacyItem = roomPrivacyItems().first()
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.features.createroom.impl.userlist.UserListEvents
|
import io.element.android.features.createroom.impl.userlist.UserListEvents
|
||||||
import io.element.android.features.createroom.impl.userlist.UserListState
|
import io.element.android.features.createroom.impl.userlist.UserListState
|
||||||
import io.element.android.features.createroom.impl.userlist.UserListStateProvider
|
import io.element.android.features.createroom.impl.userlist.UserListStateProvider
|
||||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.components.SelectedUsersList
|
import io.element.android.libraries.matrix.ui.components.SelectedUsersList
|
||||||
|
|
@ -76,7 +76,7 @@ fun UserListView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@DayNightPreviews
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun UserListViewPreview(@PreviewParameter(UserListStateProvider::class) state: UserListState) = ElementPreview {
|
internal fun UserListViewPreview(@PreviewParameter(UserListStateProvider::class) state: UserListState) = ElementPreview {
|
||||||
UserListView(state = state)
|
UserListView(state = state)
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package io.element.android.features.createroom.impl.configureroom
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
|
@ -40,6 +41,8 @@ import io.element.android.libraries.matrix.api.createroom.RoomVisibility
|
||||||
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||||
import io.element.android.libraries.mediapickers.api.PickerProvider
|
import io.element.android.libraries.mediapickers.api.PickerProvider
|
||||||
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||||
|
import io.element.android.libraries.permissions.api.PermissionsEvents
|
||||||
|
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -52,10 +55,15 @@ class ConfigureRoomPresenter @Inject constructor(
|
||||||
private val mediaPickerProvider: PickerProvider,
|
private val mediaPickerProvider: PickerProvider,
|
||||||
private val mediaPreProcessor: MediaPreProcessor,
|
private val mediaPreProcessor: MediaPreProcessor,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
|
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||||
) : Presenter<ConfigureRoomState> {
|
) : Presenter<ConfigureRoomState> {
|
||||||
|
|
||||||
|
private val cameraPermissionPresenter: PermissionsPresenter = permissionsPresenterFactory.create(android.Manifest.permission.CAMERA)
|
||||||
|
private var pendingPermissionRequest = false
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): ConfigureRoomState {
|
override fun present(): ConfigureRoomState {
|
||||||
|
val cameraPermissionState = cameraPermissionPresenter.present()
|
||||||
val createRoomConfig = dataStore.getCreateRoomConfig().collectAsState(CreateRoomConfig())
|
val createRoomConfig = dataStore.getCreateRoomConfig().collectAsState(CreateRoomConfig())
|
||||||
|
|
||||||
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(
|
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(
|
||||||
|
|
@ -75,6 +83,13 @@ class ConfigureRoomPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(cameraPermissionState.permissionGranted) {
|
||||||
|
if (cameraPermissionState.permissionGranted && pendingPermissionRequest) {
|
||||||
|
pendingPermissionRequest = false
|
||||||
|
cameraPhotoPicker.launch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
val createRoomAction: MutableState<Async<RoomId>> = remember { mutableStateOf(Async.Uninitialized) }
|
val createRoomAction: MutableState<Async<RoomId>> = remember { mutableStateOf(Async.Uninitialized) }
|
||||||
|
|
||||||
|
|
@ -93,7 +108,12 @@ class ConfigureRoomPresenter @Inject constructor(
|
||||||
is ConfigureRoomEvents.HandleAvatarAction -> {
|
is ConfigureRoomEvents.HandleAvatarAction -> {
|
||||||
when (event.action) {
|
when (event.action) {
|
||||||
AvatarAction.ChoosePhoto -> galleryImagePicker.launch()
|
AvatarAction.ChoosePhoto -> galleryImagePicker.launch()
|
||||||
AvatarAction.TakePhoto -> cameraPhotoPicker.launch()
|
AvatarAction.TakePhoto -> if (cameraPermissionState.permissionGranted) {
|
||||||
|
cameraPhotoPicker.launch()
|
||||||
|
} else {
|
||||||
|
pendingPermissionRequest = true
|
||||||
|
cameraPermissionState.eventSink(PermissionsEvents.RequestPermissions)
|
||||||
|
}
|
||||||
AvatarAction.Remove -> dataStore.setAvatarUri(uri = null)
|
AvatarAction.Remove -> dataStore.setAvatarUri(uri = null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -106,6 +126,7 @@ class ConfigureRoomPresenter @Inject constructor(
|
||||||
config = createRoomConfig.value,
|
config = createRoomConfig.value,
|
||||||
avatarActions = avatarActions,
|
avatarActions = avatarActions,
|
||||||
createRoomAction = createRoomAction.value,
|
createRoomAction = createRoomAction.value,
|
||||||
|
cameraPermissionState = cameraPermissionState,
|
||||||
eventSink = ::handleEvents,
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,14 @@ import io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||||
import io.element.android.features.createroom.impl.CreateRoomConfig
|
import io.element.android.features.createroom.impl.CreateRoomConfig
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.permissions.api.PermissionsState
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class ConfigureRoomState(
|
data class ConfigureRoomState(
|
||||||
val config: CreateRoomConfig,
|
val config: CreateRoomConfig,
|
||||||
val avatarActions: ImmutableList<AvatarAction>,
|
val avatarActions: ImmutableList<AvatarAction>,
|
||||||
val createRoomAction: Async<RoomId>,
|
val createRoomAction: Async<RoomId>,
|
||||||
|
val cameraPermissionState: PermissionsState,
|
||||||
val eventSink: (ConfigureRoomEvents) -> Unit
|
val eventSink: (ConfigureRoomEvents) -> Unit
|
||||||
) {
|
) {
|
||||||
val isCreateButtonEnabled: Boolean = config.roomName.isNullOrEmpty().not()
|
val isCreateButtonEnabled: Boolean = config.roomName.isNullOrEmpty().not()
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.createroom.impl.CreateRoomConfig
|
import io.element.android.features.createroom.impl.CreateRoomConfig
|
||||||
import io.element.android.features.createroom.impl.userlist.aListOfSelectedUsers
|
import io.element.android.features.createroom.impl.userlist.aListOfSelectedUsers
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
|
import io.element.android.libraries.permissions.api.aPermissionsState
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomState> {
|
open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomState> {
|
||||||
|
|
@ -41,5 +42,6 @@ fun aConfigureRoomState() = ConfigureRoomState(
|
||||||
config = CreateRoomConfig(),
|
config = CreateRoomConfig(),
|
||||||
avatarActions = persistentListOf(),
|
avatarActions = persistentListOf(),
|
||||||
createRoomAction = Async.Uninitialized,
|
createRoomAction = Async.Uninitialized,
|
||||||
|
cameraPermissionState = aPermissionsState(showDialog = false),
|
||||||
eventSink = { },
|
eventSink = { },
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ import io.element.android.libraries.designsystem.components.LabelledTextField
|
||||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
||||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
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.Scaffold
|
||||||
|
|
@ -65,6 +65,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet
|
import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet
|
||||||
import io.element.android.libraries.matrix.ui.components.SelectedUsersList
|
import io.element.android.libraries.matrix.ui.components.SelectedUsersList
|
||||||
import io.element.android.libraries.matrix.ui.components.UnsavedAvatar
|
import io.element.android.libraries.matrix.ui.components.UnsavedAvatar
|
||||||
|
import io.element.android.libraries.permissions.api.PermissionsView
|
||||||
import io.element.android.libraries.theme.ElementTheme
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -73,9 +74,9 @@ import kotlinx.coroutines.launch
|
||||||
@Composable
|
@Composable
|
||||||
fun ConfigureRoomView(
|
fun ConfigureRoomView(
|
||||||
state: ConfigureRoomState,
|
state: ConfigureRoomState,
|
||||||
|
onBackPressed: () -> Unit,
|
||||||
|
onRoomCreated: (RoomId) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onBackPressed: () -> Unit = {},
|
|
||||||
onRoomCreated: (RoomId) -> Unit = {},
|
|
||||||
) {
|
) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
@ -172,11 +173,15 @@ fun ConfigureRoomView(
|
||||||
|
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PermissionsView(
|
||||||
|
state = state.cameraPermissionState,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ConfigureRoomToolbar(
|
private fun ConfigureRoomToolbar(
|
||||||
isNextActionEnabled: Boolean,
|
isNextActionEnabled: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onBackPressed: () -> Unit = {},
|
onBackPressed: () -> Unit = {},
|
||||||
|
|
@ -202,7 +207,7 @@ fun ConfigureRoomToolbar(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomNameWithAvatar(
|
private fun RoomNameWithAvatar(
|
||||||
avatarUri: Uri?,
|
avatarUri: Uri?,
|
||||||
roomName: String,
|
roomName: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
@ -230,7 +235,7 @@ fun RoomNameWithAvatar(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomTopic(
|
private fun RoomTopic(
|
||||||
topic: String,
|
topic: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onTopicChanged: (String) -> Unit = {},
|
onTopicChanged: (String) -> Unit = {},
|
||||||
|
|
@ -249,7 +254,7 @@ fun RoomTopic(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomPrivacyOptions(
|
private fun RoomPrivacyOptions(
|
||||||
selected: RoomPrivacy?,
|
selected: RoomPrivacy?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onOptionSelected: (RoomPrivacyItem) -> Unit = {},
|
onOptionSelected: (RoomPrivacyItem) -> Unit = {},
|
||||||
|
|
@ -273,10 +278,12 @@ private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier =
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@DayNightPreviews
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ConfigureRoomViewPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = ElementPreview {
|
internal fun ConfigureRoomViewPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = ElementPreview {
|
||||||
ConfigureRoomView(
|
ConfigureRoomView(
|
||||||
state = state,
|
state = state,
|
||||||
|
onBackPressed = {},
|
||||||
|
onRoomCreated = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,19 +16,17 @@
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl.configureroom
|
package io.element.android.features.createroom.impl.configureroom
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.compose.material.icons.outlined.Lock
|
|
||||||
import androidx.compose.material.icons.outlined.Public
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import io.element.android.features.createroom.impl.R
|
import io.element.android.features.createroom.impl.R
|
||||||
|
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
data class RoomPrivacyItem(
|
data class RoomPrivacyItem(
|
||||||
val privacy: RoomPrivacy,
|
val privacy: RoomPrivacy,
|
||||||
val icon: ImageVector,
|
@DrawableRes val icon: Int,
|
||||||
val title: String,
|
val title: String,
|
||||||
val description: String,
|
val description: String,
|
||||||
)
|
)
|
||||||
|
|
@ -40,13 +38,13 @@ fun roomPrivacyItems(): ImmutableList<RoomPrivacyItem> {
|
||||||
when (it) {
|
when (it) {
|
||||||
RoomPrivacy.Private -> RoomPrivacyItem(
|
RoomPrivacy.Private -> RoomPrivacyItem(
|
||||||
privacy = it,
|
privacy = it,
|
||||||
icon = Icons.Outlined.Lock,
|
icon = CommonDrawables.ic_compound_lock,
|
||||||
title = stringResource(R.string.screen_create_room_private_option_title),
|
title = stringResource(R.string.screen_create_room_private_option_title),
|
||||||
description = stringResource(R.string.screen_create_room_private_option_description),
|
description = stringResource(R.string.screen_create_room_private_option_description),
|
||||||
)
|
)
|
||||||
RoomPrivacy.Public -> RoomPrivacyItem(
|
RoomPrivacy.Public -> RoomPrivacyItem(
|
||||||
privacy = it,
|
privacy = it,
|
||||||
icon = Icons.Outlined.Public,
|
icon = CommonDrawables.ic_compound_public,
|
||||||
title = stringResource(R.string.screen_create_room_public_option_title),
|
title = stringResource(R.string.screen_create_room_public_option_title),
|
||||||
description = stringResource(R.string.screen_create_room_public_option_description),
|
description = stringResource(R.string.screen_create_room_public_option_description),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Close
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -41,17 +39,17 @@ import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.features.createroom.impl.R
|
import io.element.android.features.createroom.impl.R
|
||||||
import io.element.android.features.createroom.impl.components.UserListView
|
import io.element.android.features.createroom.impl.components.UserListView
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.designsystem.VectorIcons
|
|
||||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||||
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
||||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
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.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.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
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.components.TopAppBar
|
||||||
|
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.theme.ElementTheme
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
@ -128,7 +126,7 @@ fun CreateRoomRootView(
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun CreateRoomRootViewTopBar(
|
private fun CreateRoomRootViewTopBar(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onClosePressed: () -> Unit = {},
|
onClosePressed: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
|
|
@ -141,19 +139,16 @@ fun CreateRoomRootViewTopBar(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onClosePressed) {
|
BackButton(
|
||||||
Icon(
|
resourceId = CommonDrawables.ic_compound_close,
|
||||||
imageVector = Icons.Default.Close,
|
onClick = onClosePressed,
|
||||||
contentDescription = stringResource(id = CommonStrings.action_close),
|
)
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CreateRoomActionButtonsList(
|
private fun CreateRoomActionButtonsList(
|
||||||
state: CreateRoomRootState,
|
state: CreateRoomRootState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onNewRoomClicked: () -> Unit = {},
|
onNewRoomClicked: () -> Unit = {},
|
||||||
|
|
@ -161,12 +156,12 @@ fun CreateRoomActionButtonsList(
|
||||||
) {
|
) {
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
CreateRoomActionButton(
|
CreateRoomActionButton(
|
||||||
iconRes = VectorIcons.Groups,
|
iconRes = CommonDrawables.ic_groups,
|
||||||
text = stringResource(id = R.string.screen_create_room_action_create_room),
|
text = stringResource(id = R.string.screen_create_room_action_create_room),
|
||||||
onClick = onNewRoomClicked,
|
onClick = onNewRoomClicked,
|
||||||
)
|
)
|
||||||
CreateRoomActionButton(
|
CreateRoomActionButton(
|
||||||
iconRes = VectorIcons.Share,
|
iconRes = CommonDrawables.ic_compound_share_android,
|
||||||
text = stringResource(id = CommonStrings.action_invite_friends_to_app, state.applicationName),
|
text = stringResource(id = CommonStrings.action_invite_friends_to_app, state.applicationName),
|
||||||
onClick = onInvitePeopleClicked,
|
onClick = onInvitePeopleClicked,
|
||||||
)
|
)
|
||||||
|
|
@ -174,7 +169,7 @@ fun CreateRoomActionButtonsList(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CreateRoomActionButton(
|
private fun CreateRoomActionButton(
|
||||||
@DrawableRes iconRes: Int,
|
@DrawableRes iconRes: Int,
|
||||||
text: String,
|
text: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
@ -202,7 +197,7 @@ fun CreateRoomActionButton(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@DayNightPreviews
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun CreateRoomRootViewPreview(@PreviewParameter(CreateRoomRootStateProvider::class) state: CreateRoomRootState) =
|
internal fun CreateRoomRootViewPreview(@PreviewParameter(CreateRoomRootStateProvider::class) state: CreateRoomRootState) =
|
||||||
ElementPreview {
|
ElementPreview {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,10 @@
|
||||||
<string name="screen_create_room_action_invite_people">"邀請朋友使用 Element"</string>
|
<string name="screen_create_room_action_invite_people">"邀請朋友使用 Element"</string>
|
||||||
<string name="screen_create_room_add_people_title">"邀請夥伴"</string>
|
<string name="screen_create_room_add_people_title">"邀請夥伴"</string>
|
||||||
<string name="screen_create_room_error_creating_room">"建立聊天室時發生錯誤"</string>
|
<string name="screen_create_room_error_creating_room">"建立聊天室時發生錯誤"</string>
|
||||||
|
<string name="screen_create_room_private_option_description">"聊天室裡的訊息會被加密。聊天室建立後,無法停用加密功能。"</string>
|
||||||
|
<string name="screen_create_room_private_option_title">"私密聊天室(僅限邀請)"</string>
|
||||||
|
<string name="screen_create_room_public_option_description">"訊息未加密,任何人都可以查看。您可以在之後啟用加密功能。"</string>
|
||||||
|
<string name="screen_create_room_public_option_title">"公開聊天室(任何人)"</string>
|
||||||
<string name="screen_create_room_room_name_label">"聊天室名稱"</string>
|
<string name="screen_create_room_room_name_label">"聊天室名稱"</string>
|
||||||
<string name="screen_create_room_topic_label">"主題(非必填)"</string>
|
<string name="screen_create_room_topic_label">"主題(非必填)"</string>
|
||||||
<string name="screen_create_room_title">"建立聊天室"</string>
|
<string name="screen_create_room_title">"建立聊天室"</string>
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ import io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||||
import io.element.android.libraries.mediapickers.test.FakePickerProvider
|
import io.element.android.libraries.mediapickers.test.FakePickerProvider
|
||||||
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||||
|
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
|
||||||
|
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
|
|
@ -55,6 +57,7 @@ import org.robolectric.RobolectricTestRunner
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
private const val AN_URI_FROM_CAMERA = "content://uri_from_camera"
|
private const val AN_URI_FROM_CAMERA = "content://uri_from_camera"
|
||||||
|
private const val AN_URI_FROM_CAMERA_2 = "content://uri_from_camera_2"
|
||||||
private const val AN_URI_FROM_GALLERY = "content://uri_from_gallery"
|
private const val AN_URI_FROM_GALLERY = "content://uri_from_gallery"
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner::class)
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
|
@ -70,6 +73,7 @@ class ConfigureRoomPresenterTests {
|
||||||
private lateinit var fakePickerProvider: FakePickerProvider
|
private lateinit var fakePickerProvider: FakePickerProvider
|
||||||
private lateinit var fakeMediaPreProcessor: FakeMediaPreProcessor
|
private lateinit var fakeMediaPreProcessor: FakeMediaPreProcessor
|
||||||
private lateinit var fakeAnalyticsService: FakeAnalyticsService
|
private lateinit var fakeAnalyticsService: FakeAnalyticsService
|
||||||
|
private lateinit var fakePermissionsPresenter: FakePermissionsPresenter
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
|
|
@ -79,12 +83,14 @@ class ConfigureRoomPresenterTests {
|
||||||
fakePickerProvider = FakePickerProvider()
|
fakePickerProvider = FakePickerProvider()
|
||||||
fakeMediaPreProcessor = FakeMediaPreProcessor()
|
fakeMediaPreProcessor = FakeMediaPreProcessor()
|
||||||
fakeAnalyticsService = FakeAnalyticsService()
|
fakeAnalyticsService = FakeAnalyticsService()
|
||||||
|
fakePermissionsPresenter = FakePermissionsPresenter()
|
||||||
presenter = ConfigureRoomPresenter(
|
presenter = ConfigureRoomPresenter(
|
||||||
dataStore = createRoomDataStore,
|
dataStore = createRoomDataStore,
|
||||||
matrixClient = fakeMatrixClient,
|
matrixClient = fakeMatrixClient,
|
||||||
mediaPickerProvider = fakePickerProvider,
|
mediaPickerProvider = fakePickerProvider,
|
||||||
mediaPreProcessor = fakeMediaPreProcessor,
|
mediaPreProcessor = fakeMediaPreProcessor,
|
||||||
analyticsService = fakeAnalyticsService,
|
analyticsService = fakeAnalyticsService,
|
||||||
|
permissionsPresenterFactory = FakePermissionsPresenterFactory(fakePermissionsPresenter),
|
||||||
)
|
)
|
||||||
|
|
||||||
mockkStatic(File::readBytes)
|
mockkStatic(File::readBytes)
|
||||||
|
|
@ -170,8 +176,6 @@ class ConfigureRoomPresenterTests {
|
||||||
// Room avatar
|
// Room avatar
|
||||||
// Pick avatar
|
// Pick avatar
|
||||||
fakePickerProvider.givenResult(null)
|
fakePickerProvider.givenResult(null)
|
||||||
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
|
|
||||||
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto))
|
|
||||||
// From gallery
|
// From gallery
|
||||||
val uriFromGallery = Uri.parse(AN_URI_FROM_GALLERY)
|
val uriFromGallery = Uri.parse(AN_URI_FROM_GALLERY)
|
||||||
fakePickerProvider.givenResult(uriFromGallery)
|
fakePickerProvider.givenResult(uriFromGallery)
|
||||||
|
|
@ -182,10 +186,23 @@ class ConfigureRoomPresenterTests {
|
||||||
// From camera
|
// From camera
|
||||||
val uriFromCamera = Uri.parse(AN_URI_FROM_CAMERA)
|
val uriFromCamera = Uri.parse(AN_URI_FROM_CAMERA)
|
||||||
fakePickerProvider.givenResult(uriFromCamera)
|
fakePickerProvider.givenResult(uriFromCamera)
|
||||||
|
assertThat(newState.cameraPermissionState.permissionGranted).isFalse()
|
||||||
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto))
|
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto))
|
||||||
newState = awaitItem()
|
newState = awaitItem()
|
||||||
|
assertThat(newState.cameraPermissionState.showDialog).isTrue()
|
||||||
|
fakePermissionsPresenter.setPermissionGranted()
|
||||||
|
newState = awaitItem()
|
||||||
|
assertThat(newState.cameraPermissionState.permissionGranted).isTrue()
|
||||||
|
newState = awaitItem()
|
||||||
expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera)
|
expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera)
|
||||||
assertThat(newState.config).isEqualTo(expectedConfig)
|
assertThat(newState.config).isEqualTo(expectedConfig)
|
||||||
|
// Do it again, no permission is requested
|
||||||
|
val uriFromCamera2 = Uri.parse(AN_URI_FROM_CAMERA_2)
|
||||||
|
fakePickerProvider.givenResult(uriFromCamera2)
|
||||||
|
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto))
|
||||||
|
newState = awaitItem()
|
||||||
|
expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera2)
|
||||||
|
assertThat(newState.config).isEqualTo(expectedConfig)
|
||||||
// Remove
|
// Remove
|
||||||
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.Remove))
|
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.Remove))
|
||||||
newState = awaitItem()
|
newState = awaitItem()
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import io.element.android.features.ftue.impl.R
|
import io.element.android.features.ftue.impl.R
|
||||||
import io.element.android.libraries.designsystem.atomic.pages.SunsetPage
|
import io.element.android.libraries.designsystem.atomic.pages.SunsetPage
|
||||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -45,7 +45,7 @@ fun MigrationScreenView(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@DayNightPreviews
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun MigrationViewPreview() = ElementPreview {
|
internal fun MigrationViewPreview() = ElementPreview {
|
||||||
MigrationScreenView(
|
MigrationScreenView(
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ class NotificationsOptInPresenter @AssistedInject constructor(
|
||||||
if (notificationsPermissionsState.permissionGranted) {
|
if (notificationsPermissionsState.permissionGranted) {
|
||||||
callback.onNotificationsOptInFinished()
|
callback.onNotificationsOptInFinished()
|
||||||
} else {
|
} else {
|
||||||
notificationsPermissionsState.eventSink(PermissionsEvents.OpenSystemDialog)
|
notificationsPermissionsState.eventSink(PermissionsEvents.RequestPermissions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NotificationsOptInEvents.NotNowClicked -> {
|
NotificationsOptInEvents.NotNowClicked -> {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,6 @@ open class NotificationsOptInStateProvider : PreviewParameterProvider<Notificati
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aNotificationsOptInState() = NotificationsOptInState(
|
fun aNotificationsOptInState() = NotificationsOptInState(
|
||||||
notificationsPermissionState = aPermissionsState(),
|
notificationsPermissionState = aPermissionsState(showDialog = false),
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,6 @@ import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.systemBarsPadding
|
import androidx.compose.foundation.layout.systemBarsPadding
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Notifications
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
@ -45,11 +43,12 @@ import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
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.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.theme.components.Button
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
|
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||||
import io.element.android.libraries.theme.ElementTheme
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
|
@ -80,7 +79,7 @@ private fun NotificationsOptInHeader(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
title = stringResource(R.string.screen_notification_optin_title),
|
title = stringResource(R.string.screen_notification_optin_title),
|
||||||
subTitle = stringResource(R.string.screen_notification_optin_subtitle),
|
subTitle = stringResource(R.string.screen_notification_optin_subtitle),
|
||||||
iconImageVector = Icons.Default.Notifications,
|
iconResourceId = CommonDrawables.ic_compound_notifications_solid,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -183,7 +182,7 @@ private fun NotificationRow(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@DayNightPreviews
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun NotificationsOptInViewPreview(
|
internal fun NotificationsOptInViewPreview(
|
||||||
@PreviewParameter(NotificationsOptInStateProvider::class) state: NotificationsOptInState
|
@PreviewParameter(NotificationsOptInStateProvider::class) state: NotificationsOptInState
|
||||||
|
|
|
||||||