Merge branch 'release/0.2.4' into main
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.1
|
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.2
|
- 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/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.1
|
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
|
||||||
|
|
|
||||||
2
.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.1
|
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
|
||||||
|
|
|
||||||
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.1
|
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.1
|
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.1
|
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.1
|
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
|
||||||
|
|
|
||||||
21
CHANGES.md
|
|
@ -1,3 +1,24 @@
|
||||||
|
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)
|
Changes in Element X v0.2.3 (2023-09-27)
|
||||||
========================================
|
========================================
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -230,6 +230,7 @@ dependencies {
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
testImplementation(libs.test.konsist)
|
||||||
|
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
139
app/src/test/kotlin/io/element/android/app/KonsistTest.kt
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
/*
|
||||||
|
* 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.app
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import com.lemonappdev.konsist.api.KoModifier
|
||||||
|
import com.lemonappdev.konsist.api.Konsist
|
||||||
|
import com.lemonappdev.konsist.api.ext.list.constructors
|
||||||
|
import com.lemonappdev.konsist.api.ext.list.modifierprovider.withoutModifier
|
||||||
|
import com.lemonappdev.konsist.api.ext.list.modifierprovider.withoutOverrideModifier
|
||||||
|
import com.lemonappdev.konsist.api.ext.list.parameters
|
||||||
|
import com.lemonappdev.konsist.api.ext.list.properties
|
||||||
|
import com.lemonappdev.konsist.api.ext.list.withAllAnnotationsOf
|
||||||
|
import com.lemonappdev.konsist.api.ext.list.withAllParentsOf
|
||||||
|
import com.lemonappdev.konsist.api.ext.list.withNameEndingWith
|
||||||
|
import com.lemonappdev.konsist.api.ext.list.withReturnType
|
||||||
|
import com.lemonappdev.konsist.api.ext.list.withTopLevel
|
||||||
|
import com.lemonappdev.konsist.api.ext.list.withoutName
|
||||||
|
import com.lemonappdev.konsist.api.ext.list.withoutNameEndingWith
|
||||||
|
import com.lemonappdev.konsist.api.verify.assertFalse
|
||||||
|
import com.lemonappdev.konsist.api.verify.assertTrue
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class KonsistTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Classes extending 'Presenter' should have 'Presenter' suffix`() {
|
||||||
|
Konsist.scopeFromProject()
|
||||||
|
.classes()
|
||||||
|
.withAllParentsOf(Presenter::class)
|
||||||
|
.assertTrue {
|
||||||
|
it.name.endsWith("Presenter")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Functions with '@PreviewsDayNight' annotation should have 'Preview' suffix`() {
|
||||||
|
Konsist
|
||||||
|
.scopeFromProject()
|
||||||
|
.functions()
|
||||||
|
.withAllAnnotationsOf(PreviewsDayNight::class)
|
||||||
|
.assertTrue {
|
||||||
|
it.hasNameEndingWith("Preview") &&
|
||||||
|
it.hasNameEndingWith("LightPreview").not() &&
|
||||||
|
it.hasNameEndingWith("DarkPreview").not()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Top level function with '@Composable' annotation starting with a upper case should be placed in a file with the same name`() {
|
||||||
|
Konsist
|
||||||
|
.scopeFromProject()
|
||||||
|
.functions()
|
||||||
|
.withTopLevel()
|
||||||
|
.withoutModifier(KoModifier.PRIVATE)
|
||||||
|
.withoutNameEndingWith("Preview")
|
||||||
|
.withAllAnnotationsOf(Composable::class)
|
||||||
|
.withoutName(
|
||||||
|
// Add some exceptions...
|
||||||
|
"OutlinedButton",
|
||||||
|
"TextButton",
|
||||||
|
"SimpleAlertDialogContent",
|
||||||
|
)
|
||||||
|
.assertTrue(
|
||||||
|
additionalMessage =
|
||||||
|
"""
|
||||||
|
Please check the filename. It should match the top level Composable function. If the filename is correct:
|
||||||
|
- consider making the Composable private or moving it to its own file
|
||||||
|
- at last resort, you can add an exception in the Konsist test
|
||||||
|
""".trimIndent()
|
||||||
|
) {
|
||||||
|
if (it.name.first().isLowerCase()) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
val fileName = it.containingFile.name.removeSuffix(".kt")
|
||||||
|
fileName == it.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Data class state MUST not have default value`() {
|
||||||
|
Konsist
|
||||||
|
.scopeFromProject()
|
||||||
|
.classes()
|
||||||
|
.withNameEndingWith("State")
|
||||||
|
.withoutName(
|
||||||
|
"CameraPositionState",
|
||||||
|
)
|
||||||
|
.constructors
|
||||||
|
.parameters
|
||||||
|
.assertTrue { parameterDeclaration ->
|
||||||
|
parameterDeclaration.defaultValue == null &&
|
||||||
|
// Using parameterDeclaration.defaultValue == null is not enough apparently,
|
||||||
|
// Also check that the text does not contain an equal sign
|
||||||
|
parameterDeclaration.text.contains("=").not()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Function which creates Presenter in test MUST be named 'createPresenterName'`() {
|
||||||
|
Konsist
|
||||||
|
.scopeFromTest()
|
||||||
|
.functions()
|
||||||
|
.withReturnType { it.name.endsWith("Presenter") }
|
||||||
|
.withoutOverrideModifier()
|
||||||
|
.assertTrue { functionDeclaration ->
|
||||||
|
functionDeclaration.name == "create${functionDeclaration.returnType?.name}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `no field should have 'm' prefix`() {
|
||||||
|
Konsist
|
||||||
|
.scopeFromProject()
|
||||||
|
.classes()
|
||||||
|
.properties()
|
||||||
|
.assertFalse {
|
||||||
|
val secondCharacterIsUppercase = it.name.getOrNull(1)?.isUpperCase() ?: false
|
||||||
|
it.name.startsWith('m') && secondCharacterIsUppercase
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -56,7 +58,7 @@ import io.element.android.libraries.architecture.animation.rememberDefaultTransi
|
||||||
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.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
|
||||||
|
|
@ -95,6 +97,10 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
initialElement = NavTarget.RoomList,
|
initialElement = NavTarget.RoomList,
|
||||||
savedStateMap = buildContext.savedStateMap,
|
savedStateMap = buildContext.savedStateMap,
|
||||||
),
|
),
|
||||||
|
permanentNavModel = PermanentNavModel(
|
||||||
|
NavTarget.Permanent,
|
||||||
|
savedStateMap = buildContext.savedStateMap,
|
||||||
|
),
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
plugins = plugins
|
plugins = plugins
|
||||||
) {
|
) {
|
||||||
|
|
@ -328,7 +334,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
val isFtueDisplayed by ftueState.shouldDisplayFlow.collectAsState()
|
val isFtueDisplayed by ftueState.shouldDisplayFlow.collectAsState()
|
||||||
|
|
||||||
if (!isFtueDisplayed) {
|
if (!isFtueDisplayed) {
|
||||||
PermanentChild(navTarget = NavTarget.Permanent)
|
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.Permanent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -162,9 +174,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.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
@ -354,7 +357,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")
|
||||||
|
|
|
||||||
|
|
@ -120,18 +120,9 @@ You can also have access to the aars through the [release](https://github.com/ma
|
||||||
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,6 +142,11 @@ 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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Troubleshooting:
|
||||||
|
- You may need to set `ANDROID_NDK_HOME` e.g `export ANDROID_NDK_HOME=~/Library/Android/sdk/ndk`.
|
||||||
|
- 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.
|
||||||
|
- 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).
|
||||||
|
|
||||||
Finally let the `matrix/impl` module use this aar by changing the dependencies from `libs.matrix.sdk` to `projects.libraries.rustsdk`:
|
Finally let the `matrix/impl` module use this aar by changing the dependencies from `libs.matrix.sdk` to `projects.libraries.rustsdk`:
|
||||||
|
|
||||||
```groovy
|
```groovy
|
||||||
|
|
@ -280,11 +276,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/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 |
|
|
@ -44,8 +44,8 @@ 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.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<?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>
|
||||||
|
|
|
||||||
|
|
@ -54,21 +54,50 @@ class CallIntentDataParser @Inject constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure the uri has the following parameters and value:
|
* Ensure the uri has the following parameters and value in the fragment:
|
||||||
* - appPrompt=false
|
* - appPrompt=false
|
||||||
* - confineToRoom=true
|
* - confineToRoom=true
|
||||||
* to ensure that the rendering will bo correct on the embedded Webview.
|
* to ensure that the rendering will bo correct on the embedded Webview.
|
||||||
*/
|
*/
|
||||||
private fun Uri.withCustomParameters(): String {
|
private fun Uri.withCustomParameters(): String {
|
||||||
val builder = buildUpon()
|
val builder = buildUpon()
|
||||||
|
// Remove the existing query parameters
|
||||||
builder.clearQuery()
|
builder.clearQuery()
|
||||||
queryParameterNames.forEach {
|
queryParameterNames.forEach {
|
||||||
if (it == APP_PROMPT_PARAMETER || it == CONFINE_TO_ROOM_PARAMETER) return@forEach
|
if (it == APP_PROMPT_PARAMETER || it == CONFINE_TO_ROOM_PARAMETER) return@forEach
|
||||||
builder.appendQueryParameter(it, getQueryParameter(it))
|
builder.appendQueryParameter(it, getQueryParameter(it))
|
||||||
}
|
}
|
||||||
builder.appendQueryParameter(APP_PROMPT_PARAMETER, "false")
|
// Remove the existing fragment parameters, and build the new fragment
|
||||||
builder.appendQueryParameter(CONFINE_TO_ROOM_PARAMETER, "true")
|
val currentFragment = fragment ?: ""
|
||||||
return builder.build().toString()
|
// 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 APP_PROMPT_PARAMETER = "appPrompt"
|
||||||
|
|
|
||||||
|
|
@ -35,92 +35,51 @@ class CallIntentDataParserTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `empty data returns null`() {
|
fun `empty data returns null`() {
|
||||||
val url = ""
|
doTest("", null)
|
||||||
assertThat(callIntentDataParser.parse(url)).isNull()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `invalid data returns null`() {
|
fun `invalid data returns null`() {
|
||||||
val url = "!"
|
doTest("!", null)
|
||||||
assertThat(callIntentDataParser.parse(url)).isNull()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `data with no scheme returns null`() {
|
fun `data with no scheme returns null`() {
|
||||||
val url = "test"
|
doTest("test", null)
|
||||||
assertThat(callIntentDataParser.parse(url)).isNull()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Element Call http urls returns null`() {
|
fun `Element Call http urls returns null`() {
|
||||||
val httpBaseUrl = "http://call.element.io"
|
doTest("http://call.element.io", null)
|
||||||
val httpCallUrl = "http://call.element.io/some-actual-call?with=parameters"
|
doTest("http://call.element.io/some-actual-call?with=parameters", null)
|
||||||
assertThat(callIntentDataParser.parse(httpBaseUrl)).isNull()
|
|
||||||
assertThat(callIntentDataParser.parse(httpCallUrl)).isNull()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Element Call urls will be returned as is`() {
|
fun `Element Call urls will be returned as is`() {
|
||||||
val httpsBaseUrl = "https://call.element.io"
|
doTest(
|
||||||
val httpsCallUrl = VALID_CALL_URL_WITH_PARAM
|
url = "https://call.element.io",
|
||||||
assertThat(callIntentDataParser.parse(httpsBaseUrl)).isEqualTo("$httpsBaseUrl?$EXTRA_PARAMS")
|
expectedResult = "https://call.element.io#?$EXTRA_PARAMS"
|
||||||
assertThat(callIntentDataParser.parse(httpsCallUrl)).isEqualTo("$httpsCallUrl&$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
|
@Test
|
||||||
fun `HTTP and HTTPS urls that don't come from EC return null`() {
|
fun `HTTP and HTTPS urls that don't come from EC return null`() {
|
||||||
val httpBaseUrl = "http://app.element.io"
|
doTest("http://app.element.io", null)
|
||||||
val httpsBaseUrl = "https://app.element.io"
|
doTest("https://app.element.io", null, testEmbedded = false)
|
||||||
val httpInvalidUrl = "http://"
|
doTest("http://", null)
|
||||||
val httpsInvalidUrl = "http://"
|
doTest("https://", null)
|
||||||
assertThat(callIntentDataParser.parse(httpBaseUrl)).isNull()
|
|
||||||
assertThat(callIntentDataParser.parse(httpsBaseUrl)).isNull()
|
|
||||||
assertThat(callIntentDataParser.parse(httpInvalidUrl)).isNull()
|
|
||||||
assertThat(callIntentDataParser.parse(httpsInvalidUrl)).isNull()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `element scheme with call host and url with http will returns null`() {
|
fun `Element Call url with no url returns null`() {
|
||||||
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)).isNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `element scheme with call host and url param gets url extracted`() {
|
|
||||||
val embeddedUrl = VALID_CALL_URL_WITH_PARAM
|
|
||||||
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
|
|
||||||
val url = "element://call?url=$encodedUrl"
|
|
||||||
assertThat(callIntentDataParser.parse(url)).isEqualTo("$VALID_CALL_URL_WITH_PARAM&$EXTRA_PARAMS")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `element scheme 2 with url param with http returns null`() {
|
|
||||||
val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters"
|
|
||||||
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
|
|
||||||
val url = "io.element.call:/?url=$encodedUrl"
|
|
||||||
assertThat(callIntentDataParser.parse(url)).isNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `element scheme 2 with url param gets url extracted`() {
|
|
||||||
val embeddedUrl = VALID_CALL_URL_WITH_PARAM
|
|
||||||
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
|
|
||||||
val url = "io.element.call:/?url=$encodedUrl"
|
|
||||||
assertThat(callIntentDataParser.parse(url)).isEqualTo("$VALID_CALL_URL_WITH_PARAM&$EXTRA_PARAMS")
|
|
||||||
}
|
|
||||||
|
|
||||||
@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 2 with no url returns null`() {
|
|
||||||
val embeddedUrl = VALID_CALL_URL_WITH_PARAM
|
val embeddedUrl = VALID_CALL_URL_WITH_PARAM
|
||||||
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
|
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
|
||||||
val url = "io.element.call:/?no_url=$encodedUrl"
|
val url = "io.element.call:/?no_url=$encodedUrl"
|
||||||
|
|
@ -142,7 +101,7 @@ class CallIntentDataParserTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `element scheme 2 with no data returns null`() {
|
fun `Element Call url with no data returns null`() {
|
||||||
val url = "io.element.call:/?url="
|
val url = "io.element.call:/?url="
|
||||||
assertThat(callIntentDataParser.parse(url)).isNull()
|
assertThat(callIntentDataParser.parse(url)).isNull()
|
||||||
}
|
}
|
||||||
|
|
@ -156,29 +115,108 @@ class CallIntentDataParserTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `element scheme 2 with url extra param appPrompt gets url extracted`() {
|
fun `Element Call url with url extra param appPrompt gets url extracted`() {
|
||||||
val embeddedUrl = "${VALID_CALL_URL_WITH_PARAM}&appPrompt=true"
|
doTest(
|
||||||
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
|
url = "${VALID_CALL_URL_WITH_PARAM}&appPrompt=true",
|
||||||
val url = "io.element.call:/?url=$encodedUrl"
|
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS"
|
||||||
assertThat(callIntentDataParser.parse(url)).isEqualTo("$VALID_CALL_URL_WITH_PARAM&$EXTRA_PARAMS")
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `element scheme 2 with url extra param confineToRoom gets url extracted`() {
|
fun `Element Call url with url extra param in fragment appPrompt gets url extracted`() {
|
||||||
val embeddedUrl = "${VALID_CALL_URL_WITH_PARAM}&confineToRoom=false"
|
doTest(
|
||||||
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
|
url = "${VALID_CALL_URL_WITH_PARAM}#?appPrompt=true",
|
||||||
val url = "io.element.call:/?url=$encodedUrl"
|
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?appPrompt=false&confineToRoom=true"
|
||||||
assertThat(callIntentDataParser.parse(url)).isEqualTo("$VALID_CALL_URL_WITH_PARAM&$EXTRA_PARAMS")
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `element scheme 2 with url fragment gets url extracted`() {
|
fun `Element Call url with url extra param in fragment appPrompt and other gets url extracted`() {
|
||||||
val embeddedUrl = "${VALID_CALL_URL_WITH_PARAM}#fragment"
|
doTest(
|
||||||
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
|
url = "${VALID_CALL_URL_WITH_PARAM}#?appPrompt=true&otherParam=maybe",
|
||||||
val url = "io.element.call:/?url=$encodedUrl"
|
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?appPrompt=false&otherParam=maybe&confineToRoom=true"
|
||||||
assertThat(callIntentDataParser.parse(url)).isEqualTo("$VALID_CALL_URL_WITH_PARAM&$EXTRA_PARAMS#fragment")
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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 {
|
companion object {
|
||||||
const val VALID_CALL_URL_WITH_PARAM = "https://call.element.io/some-actual-call?with=parameters"
|
const val VALID_CALL_URL_WITH_PARAM = "https://call.element.io/some-actual-call?with=parameters"
|
||||||
|
|
|
||||||
|
|
@ -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 = {},
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,7 @@ fun ConfigureRoomView(
|
||||||
|
|
||||||
@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 = {},
|
||||||
|
|
@ -207,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,
|
||||||
|
|
@ -235,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 = {},
|
||||||
|
|
@ -254,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 = {},
|
||||||
|
|
|
||||||
|
|
@ -126,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 = {},
|
||||||
) {
|
) {
|
||||||
|
|
@ -148,7 +148,7 @@ fun CreateRoomRootViewTopBar(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CreateRoomActionButtonsList(
|
private fun CreateRoomActionButtonsList(
|
||||||
state: CreateRoomRootState,
|
state: CreateRoomRootState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onNewRoomClicked: () -> Unit = {},
|
onNewRoomClicked: () -> Unit = {},
|
||||||
|
|
@ -169,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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,8 @@ import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.features.ftue.impl.R
|
import io.element.android.features.ftue.impl.R
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtom
|
import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtom
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtomSize
|
import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtomSize
|
||||||
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.OnBoardingPage
|
import io.element.android.libraries.designsystem.atomic.pages.OnBoardingPage
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
<?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_migration_title">"設定您的帳號"</string>
|
<string name="screen_migration_message">"這是一次性的程序,感謝您耐心等候。"</string>
|
||||||
|
<string name="screen_migration_title">"正在設定您的帳號。"</string>
|
||||||
|
<string name="screen_welcome_bullet_1">"通話、投票、搜尋等更多功能將在今年登場。"</string>
|
||||||
|
<string name="screen_welcome_bullet_2">"在這次的更新,您無法查看聊天室內被加密的歷史訊息。"</string>
|
||||||
|
<string name="screen_welcome_bullet_3">"我們很樂意聽取您的意見,請到設定頁面告訴我們您的想法。"</string>
|
||||||
<string name="screen_welcome_button">"開始吧!"</string>
|
<string name="screen_welcome_button">"開始吧!"</string>
|
||||||
|
<string name="screen_welcome_subtitle">"我們有些事想告訴您:"</string>
|
||||||
|
<string name="screen_welcome_title">"歡迎使用 %1$s!"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed
|
|
||||||
@Suppress("DSL_SCOPE_VIOLATION")
|
|
||||||
plugins {
|
plugins {
|
||||||
id("io.element.android-compose-library")
|
id("io.element.android-compose-library")
|
||||||
alias(libs.plugins.anvil)
|
alias(libs.plugins.anvil)
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,10 @@ import kotlinx.collections.immutable.ImmutableList
|
||||||
@Immutable
|
@Immutable
|
||||||
data class InviteListState(
|
data class InviteListState(
|
||||||
val inviteList: ImmutableList<InviteListInviteSummary>,
|
val inviteList: ImmutableList<InviteListInviteSummary>,
|
||||||
val declineConfirmationDialog: InviteDeclineConfirmationDialog = InviteDeclineConfirmationDialog.Hidden,
|
val declineConfirmationDialog: InviteDeclineConfirmationDialog,
|
||||||
val acceptedAction: Async<RoomId> = Async.Uninitialized,
|
val acceptedAction: Async<RoomId>,
|
||||||
val declinedAction: Async<Unit> = Async.Uninitialized,
|
val declinedAction: Async<Unit>,
|
||||||
val eventSink: (InviteListEvents) -> Unit = {}
|
val eventSink: (InviteListEvents) -> Unit
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed interface InviteDeclineConfirmationDialog {
|
sealed interface InviteDeclineConfirmationDialog {
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,10 @@ open class InviteListStateProvider : PreviewParameterProvider<InviteListState> {
|
||||||
|
|
||||||
internal fun aInviteListState() = InviteListState(
|
internal fun aInviteListState() = InviteListState(
|
||||||
inviteList = aInviteListInviteSummaryList(),
|
inviteList = aInviteListInviteSummaryList(),
|
||||||
|
declineConfirmationDialog = InviteDeclineConfirmationDialog.Hidden,
|
||||||
|
acceptedAction = Async.Uninitialized,
|
||||||
|
declinedAction = Async.Uninitialized,
|
||||||
|
eventSink = {},
|
||||||
)
|
)
|
||||||
|
|
||||||
internal fun aInviteListInviteSummaryList(): ImmutableList<InviteListInviteSummary> {
|
internal fun aInviteListInviteSummaryList(): ImmutableList<InviteListInviteSummary> {
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ fun InviteListView(
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun InviteListContent(
|
private fun InviteListContent(
|
||||||
state: InviteListState,
|
state: InviteListState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onBackClicked: () -> Unit = {},
|
onBackClicked: () -> Unit = {},
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ internal fun InviteSummaryRow(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun DefaultInviteSummaryRow(
|
private fun DefaultInviteSummaryRow(
|
||||||
invite: InviteListInviteSummary,
|
invite: InviteListInviteSummary,
|
||||||
onAcceptClicked: () -> Unit = {},
|
onAcceptClicked: () -> Unit = {},
|
||||||
onDeclineClicked: () -> Unit = {},
|
onDeclineClicked: () -> Unit = {},
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,10 @@ package io.element.android.features.leaveroom.api
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
data class LeaveRoomState(
|
data class LeaveRoomState(
|
||||||
val confirmation: Confirmation = Confirmation.Hidden,
|
val confirmation: Confirmation,
|
||||||
val progress: Progress = Progress.Hidden,
|
val progress: Progress,
|
||||||
val error: Error = Error.Hidden,
|
val error: Error,
|
||||||
val eventSink: (LeaveRoomEvent) -> Unit = {},
|
val eventSink: (LeaveRoomEvent) -> Unit,
|
||||||
) {
|
) {
|
||||||
sealed interface Confirmation {
|
sealed interface Confirmation {
|
||||||
data object Hidden : Confirmation
|
data object Hidden : Confirmation
|
||||||
|
|
|
||||||
|
|
@ -22,32 +22,32 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
class LeaveRoomStateProvider : PreviewParameterProvider<LeaveRoomState> {
|
class LeaveRoomStateProvider : PreviewParameterProvider<LeaveRoomState> {
|
||||||
override val values: Sequence<LeaveRoomState>
|
override val values: Sequence<LeaveRoomState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
LeaveRoomState(
|
aLeaveRoomState(
|
||||||
confirmation = LeaveRoomState.Confirmation.Hidden,
|
confirmation = LeaveRoomState.Confirmation.Hidden,
|
||||||
progress = LeaveRoomState.Progress.Hidden,
|
progress = LeaveRoomState.Progress.Hidden,
|
||||||
error = LeaveRoomState.Error.Hidden,
|
error = LeaveRoomState.Error.Hidden,
|
||||||
),
|
),
|
||||||
LeaveRoomState(
|
aLeaveRoomState(
|
||||||
confirmation = LeaveRoomState.Confirmation.Generic(A_ROOM_ID),
|
confirmation = LeaveRoomState.Confirmation.Generic(A_ROOM_ID),
|
||||||
progress = LeaveRoomState.Progress.Hidden,
|
progress = LeaveRoomState.Progress.Hidden,
|
||||||
error = LeaveRoomState.Error.Hidden,
|
error = LeaveRoomState.Error.Hidden,
|
||||||
),
|
),
|
||||||
LeaveRoomState(
|
aLeaveRoomState(
|
||||||
confirmation = LeaveRoomState.Confirmation.PrivateRoom(A_ROOM_ID),
|
confirmation = LeaveRoomState.Confirmation.PrivateRoom(A_ROOM_ID),
|
||||||
progress = LeaveRoomState.Progress.Hidden,
|
progress = LeaveRoomState.Progress.Hidden,
|
||||||
error = LeaveRoomState.Error.Hidden,
|
error = LeaveRoomState.Error.Hidden,
|
||||||
),
|
),
|
||||||
LeaveRoomState(
|
aLeaveRoomState(
|
||||||
confirmation = LeaveRoomState.Confirmation.LastUserInRoom(A_ROOM_ID),
|
confirmation = LeaveRoomState.Confirmation.LastUserInRoom(A_ROOM_ID),
|
||||||
progress = LeaveRoomState.Progress.Hidden,
|
progress = LeaveRoomState.Progress.Hidden,
|
||||||
error = LeaveRoomState.Error.Hidden,
|
error = LeaveRoomState.Error.Hidden,
|
||||||
),
|
),
|
||||||
LeaveRoomState(
|
aLeaveRoomState(
|
||||||
confirmation = LeaveRoomState.Confirmation.Hidden,
|
confirmation = LeaveRoomState.Confirmation.Hidden,
|
||||||
progress = LeaveRoomState.Progress.Shown,
|
progress = LeaveRoomState.Progress.Shown,
|
||||||
error = LeaveRoomState.Error.Hidden,
|
error = LeaveRoomState.Error.Hidden,
|
||||||
),
|
),
|
||||||
LeaveRoomState(
|
aLeaveRoomState(
|
||||||
confirmation = LeaveRoomState.Confirmation.Hidden,
|
confirmation = LeaveRoomState.Confirmation.Hidden,
|
||||||
progress = LeaveRoomState.Progress.Hidden,
|
progress = LeaveRoomState.Progress.Hidden,
|
||||||
error = LeaveRoomState.Error.Shown,
|
error = LeaveRoomState.Error.Shown,
|
||||||
|
|
@ -56,3 +56,14 @@ class LeaveRoomStateProvider : PreviewParameterProvider<LeaveRoomState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private val A_ROOM_ID = RoomId("!aRoomId:aDomain")
|
private val A_ROOM_ID = RoomId("!aRoomId:aDomain")
|
||||||
|
|
||||||
|
fun aLeaveRoomState(
|
||||||
|
confirmation: LeaveRoomState.Confirmation = LeaveRoomState.Confirmation.Hidden,
|
||||||
|
progress: LeaveRoomState.Progress = LeaveRoomState.Progress.Hidden,
|
||||||
|
error: LeaveRoomState.Error = LeaveRoomState.Error.Hidden,
|
||||||
|
) = LeaveRoomState(
|
||||||
|
confirmation = confirmation,
|
||||||
|
progress = progress,
|
||||||
|
error = error,
|
||||||
|
eventSink = {},
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ class LeaveRoomPresenterImplTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state hides all dialogs`() = runTest {
|
fun `present - initial state hides all dialogs`() = runTest {
|
||||||
val presenter = createPresenter()
|
val presenter = createLeaveRoomPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -57,7 +57,7 @@ class LeaveRoomPresenterImplTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - show generic confirmation`() = runTest {
|
fun `present - show generic confirmation`() = runTest {
|
||||||
val presenter = createPresenter(
|
val presenter = createLeaveRoomPresenter(
|
||||||
client = FakeMatrixClient().apply {
|
client = FakeMatrixClient().apply {
|
||||||
givenGetRoomResult(
|
givenGetRoomResult(
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
|
|
@ -77,7 +77,7 @@ class LeaveRoomPresenterImplTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - show private room confirmation`() = runTest {
|
fun `present - show private room confirmation`() = runTest {
|
||||||
val presenter = createPresenter(
|
val presenter = createLeaveRoomPresenter(
|
||||||
client = FakeMatrixClient().apply {
|
client = FakeMatrixClient().apply {
|
||||||
givenGetRoomResult(
|
givenGetRoomResult(
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
|
|
@ -97,7 +97,7 @@ class LeaveRoomPresenterImplTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - show last user in room confirmation`() = runTest {
|
fun `present - show last user in room confirmation`() = runTest {
|
||||||
val presenter = createPresenter(
|
val presenter = createLeaveRoomPresenter(
|
||||||
client = FakeMatrixClient().apply {
|
client = FakeMatrixClient().apply {
|
||||||
givenGetRoomResult(
|
givenGetRoomResult(
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
|
|
@ -118,7 +118,7 @@ class LeaveRoomPresenterImplTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - leaving a room leaves the room`() = runTest {
|
fun `present - leaving a room leaves the room`() = runTest {
|
||||||
val roomMembershipObserver = RoomMembershipObserver()
|
val roomMembershipObserver = RoomMembershipObserver()
|
||||||
val presenter = createPresenter(
|
val presenter = createLeaveRoomPresenter(
|
||||||
client = FakeMatrixClient().apply {
|
client = FakeMatrixClient().apply {
|
||||||
givenGetRoomResult(
|
givenGetRoomResult(
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
|
|
@ -140,7 +140,7 @@ class LeaveRoomPresenterImplTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - show error if leave room fails`() = runTest {
|
fun `present - show error if leave room fails`() = runTest {
|
||||||
val presenter = createPresenter(
|
val presenter = createLeaveRoomPresenter(
|
||||||
client = FakeMatrixClient().apply {
|
client = FakeMatrixClient().apply {
|
||||||
givenGetRoomResult(
|
givenGetRoomResult(
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
|
|
@ -164,7 +164,7 @@ class LeaveRoomPresenterImplTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - show progress indicator while leaving a room`() = runTest {
|
fun `present - show progress indicator while leaving a room`() = runTest {
|
||||||
val presenter = createPresenter(
|
val presenter = createLeaveRoomPresenter(
|
||||||
client = FakeMatrixClient().apply {
|
client = FakeMatrixClient().apply {
|
||||||
givenGetRoomResult(
|
givenGetRoomResult(
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
|
|
@ -186,7 +186,7 @@ class LeaveRoomPresenterImplTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - hide error hides the error`() = runTest {
|
fun `present - hide error hides the error`() = runTest {
|
||||||
val presenter = createPresenter(
|
val presenter = createLeaveRoomPresenter(
|
||||||
client = FakeMatrixClient().apply {
|
client = FakeMatrixClient().apply {
|
||||||
givenGetRoomResult(
|
givenGetRoomResult(
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
|
|
@ -212,7 +212,7 @@ class LeaveRoomPresenterImplTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TestScope.createPresenter(
|
private fun TestScope.createLeaveRoomPresenter(
|
||||||
client: MatrixClient = FakeMatrixClient(),
|
client: MatrixClient = FakeMatrixClient(),
|
||||||
roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(),
|
roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(),
|
||||||
): LeaveRoomPresenter = LeaveRoomPresenterImpl(
|
): LeaveRoomPresenter = LeaveRoomPresenterImpl(
|
||||||
|
|
|
||||||
30
features/leaveroom/test/build.gradle.kts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("io.element.android-compose-library")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.features.leaveroom.test"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.libraries.core)
|
||||||
|
implementation(projects.libraries.architecture)
|
||||||
|
implementation(projects.libraries.matrix.api)
|
||||||
|
api(projects.features.leaveroom.api)
|
||||||
|
}
|
||||||
|
|
@ -20,9 +20,8 @@ import androidx.compose.runtime.Composable
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
|
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class LeaveRoomPresenterFake @Inject constructor() : LeaveRoomPresenter {
|
class FakeLeaveRoomPresenter : LeaveRoomPresenter {
|
||||||
|
|
||||||
val events = mutableListOf<LeaveRoomEvent>()
|
val events = mutableListOf<LeaveRoomEvent>()
|
||||||
|
|
||||||
|
|
@ -30,7 +29,12 @@ class LeaveRoomPresenterFake @Inject constructor() : LeaveRoomPresenter {
|
||||||
events += event
|
events += event
|
||||||
}
|
}
|
||||||
|
|
||||||
private var state = LeaveRoomState(eventSink = ::handleEvent)
|
private var state = LeaveRoomState(
|
||||||
|
confirmation = LeaveRoomState.Confirmation.Hidden,
|
||||||
|
progress = LeaveRoomState.Progress.Hidden,
|
||||||
|
error = LeaveRoomState.Error.Hidden,
|
||||||
|
eventSink = ::handleEvent,
|
||||||
|
)
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value.copy(eventSink = ::handleEvent)
|
field = value.copy(eventSink = ::handleEvent)
|
||||||
}
|
}
|
||||||
|
|
@ -17,9 +17,9 @@
|
||||||
package io.element.android.features.location.impl.common.permissions
|
package io.element.android.features.location.impl.common.permissions
|
||||||
|
|
||||||
data class PermissionsState(
|
data class PermissionsState(
|
||||||
val permissions: Permissions = Permissions.NoneGranted,
|
val permissions: Permissions,
|
||||||
val shouldShowRationale: Boolean = false,
|
val shouldShowRationale: Boolean,
|
||||||
val eventSink: (PermissionsEvents) -> Unit = {},
|
val eventSink: (PermissionsEvents) -> Unit,
|
||||||
) {
|
) {
|
||||||
sealed interface Permissions {
|
sealed interface Permissions {
|
||||||
data object AllGranted : Permissions
|
data object AllGranted : Permissions
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,11 @@
|
||||||
package io.element.android.features.location.impl.send
|
package io.element.android.features.location.impl.send
|
||||||
|
|
||||||
data class SendLocationState(
|
data class SendLocationState(
|
||||||
val permissionDialog: Dialog = Dialog.None,
|
val permissionDialog: Dialog,
|
||||||
val mode: Mode = Mode.PinLocation,
|
val mode: Mode,
|
||||||
val hasLocationPermission: Boolean = false,
|
val hasLocationPermission: Boolean,
|
||||||
val appName: String = "AppName",
|
val appName: String,
|
||||||
val eventSink: (SendLocationEvents) -> Unit = {},
|
val eventSink: (SendLocationEvents) -> Unit,
|
||||||
) {
|
) {
|
||||||
sealed interface Mode {
|
sealed interface Mode {
|
||||||
data object SenderLocation : Mode
|
data object SenderLocation : Mode
|
||||||
|
|
|
||||||
|
|
@ -23,35 +23,44 @@ private const val APP_NAME = "ApplicationName"
|
||||||
class SendLocationStateProvider : PreviewParameterProvider<SendLocationState> {
|
class SendLocationStateProvider : PreviewParameterProvider<SendLocationState> {
|
||||||
override val values: Sequence<SendLocationState>
|
override val values: Sequence<SendLocationState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
SendLocationState(
|
aSendLocationState(
|
||||||
permissionDialog = SendLocationState.Dialog.None,
|
permissionDialog = SendLocationState.Dialog.None,
|
||||||
mode = SendLocationState.Mode.PinLocation,
|
mode = SendLocationState.Mode.PinLocation,
|
||||||
hasLocationPermission = false,
|
hasLocationPermission = false,
|
||||||
appName = APP_NAME,
|
|
||||||
),
|
),
|
||||||
SendLocationState(
|
aSendLocationState(
|
||||||
permissionDialog = SendLocationState.Dialog.PermissionDenied,
|
permissionDialog = SendLocationState.Dialog.PermissionDenied,
|
||||||
mode = SendLocationState.Mode.PinLocation,
|
mode = SendLocationState.Mode.PinLocation,
|
||||||
hasLocationPermission = false,
|
hasLocationPermission = false,
|
||||||
appName = APP_NAME,
|
|
||||||
),
|
),
|
||||||
SendLocationState(
|
aSendLocationState(
|
||||||
permissionDialog = SendLocationState.Dialog.PermissionRationale,
|
permissionDialog = SendLocationState.Dialog.PermissionRationale,
|
||||||
mode = SendLocationState.Mode.PinLocation,
|
mode = SendLocationState.Mode.PinLocation,
|
||||||
hasLocationPermission = false,
|
hasLocationPermission = false,
|
||||||
appName = APP_NAME,
|
|
||||||
),
|
),
|
||||||
SendLocationState(
|
aSendLocationState(
|
||||||
permissionDialog = SendLocationState.Dialog.None,
|
permissionDialog = SendLocationState.Dialog.None,
|
||||||
mode = SendLocationState.Mode.PinLocation,
|
mode = SendLocationState.Mode.PinLocation,
|
||||||
hasLocationPermission = true,
|
hasLocationPermission = true,
|
||||||
appName = APP_NAME,
|
|
||||||
),
|
),
|
||||||
SendLocationState(
|
aSendLocationState(
|
||||||
permissionDialog = SendLocationState.Dialog.None,
|
permissionDialog = SendLocationState.Dialog.None,
|
||||||
mode = SendLocationState.Mode.SenderLocation,
|
mode = SendLocationState.Mode.SenderLocation,
|
||||||
hasLocationPermission = true,
|
hasLocationPermission = true,
|
||||||
appName = APP_NAME,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun aSendLocationState(
|
||||||
|
permissionDialog: SendLocationState.Dialog,
|
||||||
|
mode: SendLocationState.Mode,
|
||||||
|
hasLocationPermission: Boolean,
|
||||||
|
): SendLocationState {
|
||||||
|
return SendLocationState(
|
||||||
|
permissionDialog = permissionDialog,
|
||||||
|
mode = mode,
|
||||||
|
hasLocationPermission = hasLocationPermission,
|
||||||
|
appName = APP_NAME,
|
||||||
|
eventSink = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.location.impl
|
||||||
|
|
||||||
|
import io.element.android.features.location.impl.common.permissions.PermissionsState
|
||||||
|
|
||||||
|
fun aPermissionsState(
|
||||||
|
permissions: PermissionsState.Permissions = PermissionsState.Permissions.NoneGranted,
|
||||||
|
shouldShowRationale: Boolean = false,
|
||||||
|
): PermissionsState {
|
||||||
|
return PermissionsState(
|
||||||
|
permissions = permissions,
|
||||||
|
shouldShowRationale = shouldShowRationale,
|
||||||
|
eventSink = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -26,7 +26,11 @@ class PermissionsPresenterFake : PermissionsPresenter {
|
||||||
events += event
|
events += event
|
||||||
}
|
}
|
||||||
|
|
||||||
private var state = PermissionsState(eventSink = ::handleEvent)
|
private var state = PermissionsState(
|
||||||
|
permissions = PermissionsState.Permissions.NoneGranted,
|
||||||
|
shouldShowRationale = false,
|
||||||
|
eventSink = ::handleEvent
|
||||||
|
)
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value.copy(eventSink = ::handleEvent)
|
field = value.copy(eventSink = ::handleEvent)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth
|
import com.google.common.truth.Truth
|
||||||
import im.vector.app.features.analytics.plan.Composer
|
import im.vector.app.features.analytics.plan.Composer
|
||||||
import io.element.android.features.location.api.Location
|
import io.element.android.features.location.api.Location
|
||||||
|
import io.element.android.features.location.impl.aPermissionsState
|
||||||
import io.element.android.features.location.impl.common.actions.FakeLocationActions
|
import io.element.android.features.location.impl.common.actions.FakeLocationActions
|
||||||
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
|
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
|
||||||
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
|
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
|
||||||
|
|
@ -65,7 +66,7 @@ class SendLocationPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `initial state with permissions granted`() = runTest {
|
fun `initial state with permissions granted`() = runTest {
|
||||||
permissionsPresenterFake.givenState(
|
permissionsPresenterFake.givenState(
|
||||||
PermissionsState(
|
aPermissionsState(
|
||||||
permissions = PermissionsState.Permissions.AllGranted,
|
permissions = PermissionsState.Permissions.AllGranted,
|
||||||
shouldShowRationale = false,
|
shouldShowRationale = false,
|
||||||
)
|
)
|
||||||
|
|
@ -92,7 +93,7 @@ class SendLocationPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `initial state with permissions partially granted`() = runTest {
|
fun `initial state with permissions partially granted`() = runTest {
|
||||||
permissionsPresenterFake.givenState(
|
permissionsPresenterFake.givenState(
|
||||||
PermissionsState(
|
aPermissionsState(
|
||||||
permissions = PermissionsState.Permissions.SomeGranted,
|
permissions = PermissionsState.Permissions.SomeGranted,
|
||||||
shouldShowRationale = false,
|
shouldShowRationale = false,
|
||||||
)
|
)
|
||||||
|
|
@ -119,7 +120,7 @@ class SendLocationPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `initial state with permissions denied`() = runTest {
|
fun `initial state with permissions denied`() = runTest {
|
||||||
permissionsPresenterFake.givenState(
|
permissionsPresenterFake.givenState(
|
||||||
PermissionsState(
|
aPermissionsState(
|
||||||
permissions = PermissionsState.Permissions.NoneGranted,
|
permissions = PermissionsState.Permissions.NoneGranted,
|
||||||
shouldShowRationale = false,
|
shouldShowRationale = false,
|
||||||
)
|
)
|
||||||
|
|
@ -145,7 +146,7 @@ class SendLocationPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `initial state with permissions denied once`() = runTest {
|
fun `initial state with permissions denied once`() = runTest {
|
||||||
permissionsPresenterFake.givenState(
|
permissionsPresenterFake.givenState(
|
||||||
PermissionsState(
|
aPermissionsState(
|
||||||
permissions = PermissionsState.Permissions.NoneGranted,
|
permissions = PermissionsState.Permissions.NoneGranted,
|
||||||
shouldShowRationale = true,
|
shouldShowRationale = true,
|
||||||
)
|
)
|
||||||
|
|
@ -171,7 +172,7 @@ class SendLocationPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `rationale dialog dismiss`() = runTest {
|
fun `rationale dialog dismiss`() = runTest {
|
||||||
permissionsPresenterFake.givenState(
|
permissionsPresenterFake.givenState(
|
||||||
PermissionsState(
|
aPermissionsState(
|
||||||
permissions = PermissionsState.Permissions.NoneGranted,
|
permissions = PermissionsState.Permissions.NoneGranted,
|
||||||
shouldShowRationale = true,
|
shouldShowRationale = true,
|
||||||
)
|
)
|
||||||
|
|
@ -202,7 +203,7 @@ class SendLocationPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `rationale dialog continue`() = runTest {
|
fun `rationale dialog continue`() = runTest {
|
||||||
permissionsPresenterFake.givenState(
|
permissionsPresenterFake.givenState(
|
||||||
PermissionsState(
|
aPermissionsState(
|
||||||
permissions = PermissionsState.Permissions.NoneGranted,
|
permissions = PermissionsState.Permissions.NoneGranted,
|
||||||
shouldShowRationale = true,
|
shouldShowRationale = true,
|
||||||
)
|
)
|
||||||
|
|
@ -230,7 +231,7 @@ class SendLocationPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `permission denied dialog dismiss`() = runTest {
|
fun `permission denied dialog dismiss`() = runTest {
|
||||||
permissionsPresenterFake.givenState(
|
permissionsPresenterFake.givenState(
|
||||||
PermissionsState(
|
aPermissionsState(
|
||||||
permissions = PermissionsState.Permissions.NoneGranted,
|
permissions = PermissionsState.Permissions.NoneGranted,
|
||||||
shouldShowRationale = false,
|
shouldShowRationale = false,
|
||||||
)
|
)
|
||||||
|
|
@ -261,7 +262,7 @@ class SendLocationPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `share sender location`() = runTest {
|
fun `share sender location`() = runTest {
|
||||||
permissionsPresenterFake.givenState(
|
permissionsPresenterFake.givenState(
|
||||||
PermissionsState(
|
aPermissionsState(
|
||||||
permissions = PermissionsState.Permissions.AllGranted,
|
permissions = PermissionsState.Permissions.AllGranted,
|
||||||
shouldShowRationale = false,
|
shouldShowRationale = false,
|
||||||
)
|
)
|
||||||
|
|
@ -317,7 +318,7 @@ class SendLocationPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `share pin location`() = runTest {
|
fun `share pin location`() = runTest {
|
||||||
permissionsPresenterFake.givenState(
|
permissionsPresenterFake.givenState(
|
||||||
PermissionsState(
|
aPermissionsState(
|
||||||
permissions = PermissionsState.Permissions.NoneGranted,
|
permissions = PermissionsState.Permissions.NoneGranted,
|
||||||
shouldShowRationale = false,
|
shouldShowRationale = false,
|
||||||
)
|
)
|
||||||
|
|
@ -373,7 +374,7 @@ class SendLocationPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `composer context passes through analytics`() = runTest {
|
fun `composer context passes through analytics`() = runTest {
|
||||||
permissionsPresenterFake.givenState(
|
permissionsPresenterFake.givenState(
|
||||||
PermissionsState(
|
aPermissionsState(
|
||||||
permissions = PermissionsState.Permissions.NoneGranted,
|
permissions = PermissionsState.Permissions.NoneGranted,
|
||||||
shouldShowRationale = false,
|
shouldShowRationale = false,
|
||||||
)
|
)
|
||||||
|
|
@ -419,7 +420,7 @@ class SendLocationPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `open settings activity`() = runTest {
|
fun `open settings activity`() = runTest {
|
||||||
permissionsPresenterFake.givenState(
|
permissionsPresenterFake.givenState(
|
||||||
PermissionsState(
|
aPermissionsState(
|
||||||
permissions = PermissionsState.Permissions.NoneGranted,
|
permissions = PermissionsState.Permissions.NoneGranted,
|
||||||
shouldShowRationale = false,
|
shouldShowRationale = false,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth
|
import com.google.common.truth.Truth
|
||||||
import io.element.android.features.location.api.Location
|
import io.element.android.features.location.api.Location
|
||||||
|
import io.element.android.features.location.impl.aPermissionsState
|
||||||
import io.element.android.features.location.impl.common.actions.FakeLocationActions
|
import io.element.android.features.location.impl.common.actions.FakeLocationActions
|
||||||
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
|
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
|
||||||
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
|
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
|
||||||
|
|
@ -55,7 +56,7 @@ class ShowLocationPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `emits initial state with no location permission`() = runTest {
|
fun `emits initial state with no location permission`() = runTest {
|
||||||
permissionsPresenterFake.givenState(
|
permissionsPresenterFake.givenState(
|
||||||
PermissionsState(
|
aPermissionsState(
|
||||||
permissions = PermissionsState.Permissions.NoneGranted,
|
permissions = PermissionsState.Permissions.NoneGranted,
|
||||||
shouldShowRationale = false,
|
shouldShowRationale = false,
|
||||||
)
|
)
|
||||||
|
|
@ -75,7 +76,7 @@ class ShowLocationPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `emits initial state location permission denied once`() = runTest {
|
fun `emits initial state location permission denied once`() = runTest {
|
||||||
permissionsPresenterFake.givenState(
|
permissionsPresenterFake.givenState(
|
||||||
PermissionsState(
|
aPermissionsState(
|
||||||
permissions = PermissionsState.Permissions.NoneGranted,
|
permissions = PermissionsState.Permissions.NoneGranted,
|
||||||
shouldShowRationale = true,
|
shouldShowRationale = true,
|
||||||
)
|
)
|
||||||
|
|
@ -94,7 +95,7 @@ class ShowLocationPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `emits initial state with location permission`() = runTest {
|
fun `emits initial state with location permission`() = runTest {
|
||||||
permissionsPresenterFake.givenState(PermissionsState(permissions = PermissionsState.Permissions.AllGranted))
|
permissionsPresenterFake.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted))
|
||||||
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -109,7 +110,7 @@ class ShowLocationPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `emits initial state with partial location permission`() = runTest {
|
fun `emits initial state with partial location permission`() = runTest {
|
||||||
permissionsPresenterFake.givenState(PermissionsState(permissions = PermissionsState.Permissions.SomeGranted))
|
permissionsPresenterFake.givenState(aPermissionsState(permissions = PermissionsState.Permissions.SomeGranted))
|
||||||
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -137,7 +138,7 @@ class ShowLocationPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `centers on user location`() = runTest {
|
fun `centers on user location`() = runTest {
|
||||||
permissionsPresenterFake.givenState(PermissionsState(permissions = PermissionsState.Permissions.AllGranted))
|
permissionsPresenterFake.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted))
|
||||||
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -166,7 +167,7 @@ class ShowLocationPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `rationale dialog dismiss`() = runTest {
|
fun `rationale dialog dismiss`() = runTest {
|
||||||
permissionsPresenterFake.givenState(
|
permissionsPresenterFake.givenState(
|
||||||
PermissionsState(
|
aPermissionsState(
|
||||||
permissions = PermissionsState.Permissions.NoneGranted,
|
permissions = PermissionsState.Permissions.NoneGranted,
|
||||||
shouldShowRationale = true,
|
shouldShowRationale = true,
|
||||||
)
|
)
|
||||||
|
|
@ -197,7 +198,7 @@ class ShowLocationPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `rationale dialog continue`() = runTest {
|
fun `rationale dialog continue`() = runTest {
|
||||||
permissionsPresenterFake.givenState(
|
permissionsPresenterFake.givenState(
|
||||||
PermissionsState(
|
aPermissionsState(
|
||||||
permissions = PermissionsState.Permissions.NoneGranted,
|
permissions = PermissionsState.Permissions.NoneGranted,
|
||||||
shouldShowRationale = true,
|
shouldShowRationale = true,
|
||||||
)
|
)
|
||||||
|
|
@ -225,7 +226,7 @@ class ShowLocationPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `permission denied dialog dismiss`() = runTest {
|
fun `permission denied dialog dismiss`() = runTest {
|
||||||
permissionsPresenterFake.givenState(
|
permissionsPresenterFake.givenState(
|
||||||
PermissionsState(
|
aPermissionsState(
|
||||||
permissions = PermissionsState.Permissions.NoneGranted,
|
permissions = PermissionsState.Permissions.NoneGranted,
|
||||||
shouldShowRationale = false,
|
shouldShowRationale = false,
|
||||||
)
|
)
|
||||||
|
|
@ -256,7 +257,7 @@ class ShowLocationPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `open settings activity`() = runTest {
|
fun `open settings activity`() = runTest {
|
||||||
permissionsPresenterFake.givenState(
|
permissionsPresenterFake.givenState(
|
||||||
PermissionsState(
|
aPermissionsState(
|
||||||
permissions = PermissionsState.Permissions.NoneGranted,
|
permissions = PermissionsState.Permissions.NoneGranted,
|
||||||
shouldShowRationale = false,
|
shouldShowRationale = false,
|
||||||
)
|
)
|
||||||
|
|
@ -290,7 +291,6 @@ class ShowLocationPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val A_DESCRIPTION = "My happy place"
|
private const val A_DESCRIPTION = "My happy place"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,9 @@
|
||||||
|
|
||||||
package io.element.android.features.login.impl.accountprovider
|
package io.element.android.features.login.impl.accountprovider
|
||||||
|
|
||||||
data class AccountProvider constructor(
|
data class AccountProvider(
|
||||||
val title: String,
|
val url: String,
|
||||||
|
val title: String = url.removePrefix("https://").removePrefix("http://"),
|
||||||
val subtitle: String? = null,
|
val subtitle: String? = null,
|
||||||
val isPublic: Boolean = false,
|
val isPublic: Boolean = false,
|
||||||
val isMatrixOrg: Boolean = false,
|
val isMatrixOrg: Boolean = false,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.features.login.impl.accountprovider
|
package io.element.android.features.login.impl.accountprovider
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.features.login.impl.util.LoginConstants
|
||||||
|
|
||||||
open class AccountProviderProvider : PreviewParameterProvider<AccountProvider> {
|
open class AccountProviderProvider : PreviewParameterProvider<AccountProvider> {
|
||||||
override val values: Sequence<AccountProvider>
|
override val values: Sequence<AccountProvider>
|
||||||
|
|
@ -31,7 +32,7 @@ open class AccountProviderProvider : PreviewParameterProvider<AccountProvider> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun anAccountProvider() = AccountProvider(
|
fun anAccountProvider() = AccountProvider(
|
||||||
title = "matrix.org",
|
url = LoginConstants.MATRIX_ORG_URL,
|
||||||
subtitle = "Matrix.org is an open network for secure, decentralized communication.",
|
subtitle = "Matrix.org is an open network for secure, decentralized communication.",
|
||||||
isPublic = true,
|
isPublic = true,
|
||||||
isMatrixOrg = true,
|
isMatrixOrg = true,
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ class ChangeServerPresenter @Inject constructor(
|
||||||
changeServerAction: MutableState<Async<Unit>>,
|
changeServerAction: MutableState<Async<Unit>>,
|
||||||
) = launch {
|
) = launch {
|
||||||
suspend {
|
suspend {
|
||||||
authenticationService.setHomeserver(data.title).map {
|
authenticationService.setHomeserver(data.url).map {
|
||||||
authenticationService.getHomeserverDetails().value!!
|
authenticationService.getHomeserverDetails().value!!
|
||||||
// Valid, remember user choice
|
// Valid, remember user choice
|
||||||
accountProviderDataSource.userSelection(data)
|
accountProviderDataSource.userSelection(data)
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import io.element.android.features.login.impl.R
|
import io.element.android.features.login.impl.R
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -39,3 +41,12 @@ internal fun SlidingSyncNotSupportedDialog(
|
||||||
content = stringResource(R.string.screen_change_server_error_no_sliding_sync_message),
|
content = stringResource(R.string.screen_change_server_error_no_sliding_sync_message),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun SlidingSyncNotSupportedDialogPreview() = ElementPreview {
|
||||||
|
SlidingSyncNotSupportedDialog(
|
||||||
|
onLearnMoreClicked = {},
|
||||||
|
onDismiss = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ package io.element.android.features.login.impl.screens.changeaccountprovider
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||||
import io.element.android.features.login.impl.changeserver.ChangeServerPresenter
|
import io.element.android.features.login.impl.changeserver.ChangeServerPresenter
|
||||||
|
import io.element.android.features.login.impl.util.LoginConstants
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -33,7 +34,7 @@ class ChangeAccountProviderPresenter @Inject constructor(
|
||||||
// Just matrix.org by default for now
|
// Just matrix.org by default for now
|
||||||
accountProviders = listOf(
|
accountProviders = listOf(
|
||||||
AccountProvider(
|
AccountProvider(
|
||||||
title = "matrix.org",
|
url = LoginConstants.MATRIX_ORG_URL,
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
isPublic = true,
|
isPublic = true,
|
||||||
isMatrixOrg = true,
|
isMatrixOrg = true,
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,7 @@ fun ChangeAccountProviderView(
|
||||||
// Other
|
// Other
|
||||||
AccountProviderView(
|
AccountProviderView(
|
||||||
item = AccountProvider(
|
item = AccountProvider(
|
||||||
|
url = "",
|
||||||
title = stringResource(id = R.string.screen_change_account_provider_other),
|
title = stringResource(id = R.string.screen_change_account_provider_other),
|
||||||
),
|
),
|
||||||
onClick = onOtherProviderClicked
|
onClick = onOtherProviderClicked
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
|
||||||
fun handleEvents(event: ConfirmAccountProviderEvents) {
|
fun handleEvents(event: ConfirmAccountProviderEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
ConfirmAccountProviderEvents.Continue -> {
|
ConfirmAccountProviderEvents.Continue -> {
|
||||||
localCoroutineScope.submit(accountProvider.title, loginFlowAction)
|
localCoroutineScope.submit(accountProvider.url, loginFlowAction)
|
||||||
}
|
}
|
||||||
ConfirmAccountProviderEvents.ClearError -> loginFlowAction.value = Async.Uninitialized
|
ConfirmAccountProviderEvents.ClearError -> loginFlowAction.value = Async.Uninitialized
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ data class ConfirmAccountProviderState(
|
||||||
val loginFlow: Async<LoginFlow>,
|
val loginFlow: Async<LoginFlow>,
|
||||||
val eventSink: (ConfirmAccountProviderEvents) -> Unit
|
val eventSink: (ConfirmAccountProviderEvents) -> Unit
|
||||||
) {
|
) {
|
||||||
val submitEnabled: Boolean get() = accountProvider.title.isNotEmpty() && (loginFlow is Async.Uninitialized || loginFlow is Async.Loading)
|
val submitEnabled: Boolean get() = accountProvider.url.isNotEmpty() && (loginFlow is Async.Uninitialized || loginFlow is Async.Loading)
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface LoginFlow {
|
sealed interface LoginFlow {
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||||
import io.element.android.libraries.testtags.TestTags
|
import io.element.android.libraries.testtags.TestTags
|
||||||
import io.element.android.libraries.testtags.testTag
|
import io.element.android.libraries.testtags.testTag
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ConfirmAccountProviderView(
|
fun ConfirmAccountProviderView(
|
||||||
|
|
@ -86,7 +87,7 @@ fun ConfirmAccountProviderView(
|
||||||
footer = {
|
footer = {
|
||||||
ButtonColumnMolecule {
|
ButtonColumnMolecule {
|
||||||
Button(
|
Button(
|
||||||
text = stringResource(id = R.string.screen_account_provider_continue),
|
text = stringResource(id = CommonStrings.action_continue),
|
||||||
showProgress = isLoading,
|
showProgress = isLoading,
|
||||||
onClick = { eventSink.invoke(ConfirmAccountProviderEvents.Continue) },
|
onClick = { eventSink.invoke(ConfirmAccountProviderEvents.Continue) },
|
||||||
enabled = state.submitEnabled || isLoading,
|
enabled = state.submitEnabled || isLoading,
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ fun LoginPasswordView(
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
// Submit
|
// Submit
|
||||||
Button(
|
Button(
|
||||||
text = stringResource(R.string.screen_login_submit),
|
text = stringResource(CommonStrings.action_continue),
|
||||||
showProgress = isLoading,
|
showProgress = isLoading,
|
||||||
onClick = ::submit,
|
onClick = ::submit,
|
||||||
enabled = state.submitEnabled || isLoading,
|
enabled = state.submitEnabled || isLoading,
|
||||||
|
|
@ -167,7 +167,7 @@ fun LoginPasswordView(
|
||||||
|
|
||||||
@OptIn(ExperimentalComposeUiApi::class)
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun LoginForm(
|
private fun LoginForm(
|
||||||
state: LoginPasswordState,
|
state: LoginPasswordState,
|
||||||
isLoading: Boolean,
|
isLoading: Boolean,
|
||||||
onSubmit: () -> Unit,
|
onSubmit: () -> Unit,
|
||||||
|
|
@ -199,7 +199,7 @@ internal fun LoginForm(
|
||||||
eventSink(LoginPasswordEvents.SetLogin(it))
|
eventSink(LoginPasswordEvents.SetLogin(it))
|
||||||
}),
|
}),
|
||||||
placeholder = {
|
placeholder = {
|
||||||
Text(text = stringResource(R.string.screen_login_username_hint))
|
Text(text = stringResource(CommonStrings.common_username))
|
||||||
},
|
},
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
loginFieldState = it
|
loginFieldState = it
|
||||||
|
|
@ -246,7 +246,7 @@ internal fun LoginForm(
|
||||||
eventSink(LoginPasswordEvents.SetPassword(it))
|
eventSink(LoginPasswordEvents.SetPassword(it))
|
||||||
},
|
},
|
||||||
placeholder = {
|
placeholder = {
|
||||||
Text(text = stringResource(R.string.screen_login_password_hint))
|
Text(text = stringResource(CommonStrings.common_password))
|
||||||
},
|
},
|
||||||
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
|
|
@ -272,7 +272,7 @@ internal fun LoginForm(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun LoginErrorDialog(error: Throwable, onDismiss: () -> Unit) {
|
private fun LoginErrorDialog(error: Throwable, onDismiss: () -> Unit) {
|
||||||
ErrorDialog(
|
ErrorDialog(
|
||||||
title = stringResource(id = CommonStrings.dialog_title_error),
|
title = stringResource(id = CommonStrings.dialog_title_error),
|
||||||
content = stringResource(loginError(error)),
|
content = stringResource(loginError(error)),
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ package io.element.android.features.login.impl.screens.searchaccountprovider
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.login.impl.changeserver.aChangeServerState
|
import io.element.android.features.login.impl.changeserver.aChangeServerState
|
||||||
import io.element.android.features.login.impl.resolver.HomeserverData
|
import io.element.android.features.login.impl.resolver.HomeserverData
|
||||||
|
import io.element.android.features.login.impl.util.LoginConstants
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
|
|
||||||
open class SearchAccountProviderStateProvider : PreviewParameterProvider<SearchAccountProviderState> {
|
open class SearchAccountProviderStateProvider : PreviewParameterProvider<SearchAccountProviderState> {
|
||||||
|
|
@ -49,7 +50,7 @@ fun aHomeserverDataList(): List<HomeserverData> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aHomeserverData(
|
fun aHomeserverData(
|
||||||
homeserverUrl: String = "https://matrix.org",
|
homeserverUrl: String = LoginConstants.MATRIX_ORG_URL,
|
||||||
isWellknownValid: Boolean = true,
|
isWellknownValid: Boolean = true,
|
||||||
supportSlidingSync: Boolean = true,
|
supportSlidingSync: Boolean = true,
|
||||||
): HomeserverData {
|
): HomeserverData {
|
||||||
|
|
|
||||||
|
|
@ -54,12 +54,13 @@ import io.element.android.features.login.impl.accountprovider.AccountProviderVie
|
||||||
import io.element.android.features.login.impl.changeserver.ChangeServerEvents
|
import io.element.android.features.login.impl.changeserver.ChangeServerEvents
|
||||||
import io.element.android.features.login.impl.changeserver.ChangeServerView
|
import io.element.android.features.login.impl.changeserver.ChangeServerView
|
||||||
import io.element.android.features.login.impl.resolver.HomeserverData
|
import io.element.android.features.login.impl.resolver.HomeserverData
|
||||||
|
import io.element.android.features.login.impl.util.LoginConstants
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.components.form.textFieldState
|
import io.element.android.libraries.designsystem.components.form.textFieldState
|
||||||
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.preview.PreviewsDayNight
|
||||||
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.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.IconButton
|
||||||
|
|
@ -195,9 +196,9 @@ fun SearchAccountProviderView(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun HomeserverData.toAccountProvider(): AccountProvider {
|
private fun HomeserverData.toAccountProvider(): AccountProvider {
|
||||||
val isMatrixOrg = homeserverUrl == "https://matrix.org"
|
val isMatrixOrg = homeserverUrl == LoginConstants.MATRIX_ORG_URL
|
||||||
return AccountProvider(
|
return AccountProvider(
|
||||||
title = homeserverUrl.removePrefix("http://").removePrefix("https://"),
|
url = homeserverUrl,
|
||||||
subtitle = if (isMatrixOrg) stringResource(id = R.string.screen_change_account_provider_matrix_org_subtitle) else null,
|
subtitle = if (isMatrixOrg) stringResource(id = R.string.screen_change_account_provider_matrix_org_subtitle) else null,
|
||||||
isPublic = isMatrixOrg, // There is no need to know for other servers right now
|
isPublic = isMatrixOrg, // There is no need to know for other servers right now
|
||||||
isMatrixOrg = isMatrixOrg,
|
isMatrixOrg = isMatrixOrg,
|
||||||
|
|
|
||||||
|
|
@ -19,14 +19,14 @@ package io.element.android.features.login.impl.util
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||||
|
|
||||||
object LoginConstants {
|
object LoginConstants {
|
||||||
const val MATRIX_ORG_URL = "matrix.org"
|
const val MATRIX_ORG_URL = "https://matrix.org"
|
||||||
|
|
||||||
const val DEFAULT_HOMESERVER_URL = "matrix.org"
|
const val DEFAULT_HOMESERVER_URL = "https://matrix.org"
|
||||||
const val SLIDING_SYNC_READ_MORE_URL = "https://github.com/matrix-org/sliding-sync/blob/main/docs/Landing.md"
|
const val SLIDING_SYNC_READ_MORE_URL = "https://github.com/matrix-org/sliding-sync/blob/main/docs/Landing.md"
|
||||||
}
|
}
|
||||||
|
|
||||||
val defaultAccountProvider = AccountProvider(
|
val defaultAccountProvider = AccountProvider(
|
||||||
title = LoginConstants.DEFAULT_HOMESERVER_URL,
|
url = LoginConstants.DEFAULT_HOMESERVER_URL,
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
isPublic = LoginConstants.DEFAULT_HOMESERVER_URL == LoginConstants.MATRIX_ORG_URL,
|
isPublic = LoginConstants.DEFAULT_HOMESERVER_URL == LoginConstants.MATRIX_ORG_URL,
|
||||||
isMatrixOrg = LoginConstants.DEFAULT_HOMESERVER_URL == LoginConstants.MATRIX_ORG_URL,
|
isMatrixOrg = LoginConstants.DEFAULT_HOMESERVER_URL == LoginConstants.MATRIX_ORG_URL,
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,28 @@
|
||||||
<?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_account_provider_change">"更改帳號提供者"</string>
|
||||||
|
<string name="screen_account_provider_form_hint">"家伺服器位址"</string>
|
||||||
|
<string name="screen_account_provider_form_notice">"輸入關鍵字或網域名稱。"</string>
|
||||||
|
<string name="screen_account_provider_form_subtitle">"搜尋公司、社群、私有伺服器"</string>
|
||||||
|
<string name="screen_account_provider_form_title">"尋找帳號提供者"</string>
|
||||||
|
<string name="screen_account_provider_signin_subtitle">"您的所有對話將保存於此,就如同您的電子郵件供應商會保存您的電子郵件一樣。"</string>
|
||||||
<string name="screen_account_provider_signin_title">"您即將登入 %s"</string>
|
<string name="screen_account_provider_signin_title">"您即將登入 %s"</string>
|
||||||
|
<string name="screen_account_provider_signup_subtitle">"您的所有對話將保存於此,就如同您的電子郵件供應商會保存您的電子郵件一樣。"</string>
|
||||||
<string name="screen_account_provider_signup_title">"您即將在 %s 建立帳號"</string>
|
<string name="screen_account_provider_signup_title">"您即將在 %s 建立帳號"</string>
|
||||||
|
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org 由 Matrix.org 基金會營運,是用於安全、去中心化通訊的公共 Matrix 網路上的大型免費伺服器。"</string>
|
||||||
<string name="screen_change_account_provider_other">"其他"</string>
|
<string name="screen_change_account_provider_other">"其他"</string>
|
||||||
<string name="screen_change_server_error_no_sliding_sync_message">"此伺服器目前不支援 sliding sync。"</string>
|
<string name="screen_change_account_provider_subtitle">"使用不同的帳戶提供者,例如您自己的伺服器或工作帳號。"</string>
|
||||||
|
<string name="screen_change_account_provider_title">"更改帳號提供者"</string>
|
||||||
|
<string name="screen_change_server_error_no_sliding_sync_message">"此伺服器目前不支援滑動同步(sliding sync)。"</string>
|
||||||
<string name="screen_change_server_form_header">"家伺服器 URL"</string>
|
<string name="screen_change_server_form_header">"家伺服器 URL"</string>
|
||||||
|
<string name="screen_login_error_deactivated_account">"這個帳號已被停用。"</string>
|
||||||
|
<string name="screen_login_error_invalid_credentials">"不正確的使用者名稱或密碼"</string>
|
||||||
<string name="screen_login_form_header">"輸入您的詳細資料"</string>
|
<string name="screen_login_form_header">"輸入您的詳細資料"</string>
|
||||||
<string name="screen_login_title">"歡迎回來!"</string>
|
<string name="screen_login_title">"歡迎回來!"</string>
|
||||||
<string name="screen_login_title_with_homeserver">"登入 %1$s"</string>
|
<string name="screen_login_title_with_homeserver">"登入 %1$s"</string>
|
||||||
|
<string name="screen_server_confirmation_change_server">"更改帳號提供者"</string>
|
||||||
|
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix 是一個開放網路,為了安全、去中心化的通訊而生。"</string>
|
||||||
|
<string name="screen_server_confirmation_message_register">"您的所有對話將保存於此,就如同您的電子郵件供應商會保存您的電子郵件一樣。"</string>
|
||||||
<string name="screen_server_confirmation_title_login">"您即將登入 %1$s"</string>
|
<string name="screen_server_confirmation_title_login">"您即將登入 %1$s"</string>
|
||||||
<string name="screen_server_confirmation_title_register">"您即將在 %1$s 建立帳號"</string>
|
<string name="screen_server_confirmation_title_register">"您即將在 %1$s 建立帳號"</string>
|
||||||
<string name="screen_waitlist_message_success">"歡迎使用 %1$s!"</string>
|
<string name="screen_waitlist_message_success">"歡迎使用 %1$s!"</string>
|
||||||
|
|
@ -16,5 +31,6 @@
|
||||||
<string name="screen_change_server_title">"選擇您的伺服器"</string>
|
<string name="screen_change_server_title">"選擇您的伺服器"</string>
|
||||||
<string name="screen_login_password_hint">"密碼"</string>
|
<string name="screen_login_password_hint">"密碼"</string>
|
||||||
<string name="screen_login_submit">"繼續"</string>
|
<string name="screen_login_submit">"繼續"</string>
|
||||||
|
<string name="screen_login_subtitle">"Matrix 是一個開放網路,為了安全、去中心化的通訊而生。"</string>
|
||||||
<string name="screen_login_username_hint">"使用者名稱"</string>
|
<string name="screen_login_username_hint">"使用者名稱"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ class ChangeServerPresenterTest {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.changeServerAction).isEqualTo(Async.Uninitialized)
|
assertThat(initialState.changeServerAction).isEqualTo(Async.Uninitialized)
|
||||||
authenticationService.givenHomeserver(A_HOMESERVER)
|
authenticationService.givenHomeserver(A_HOMESERVER)
|
||||||
initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(A_HOMESERVER_URL)))
|
initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL)))
|
||||||
val loadingState = awaitItem()
|
val loadingState = awaitItem()
|
||||||
assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java)
|
assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java)
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
|
|
@ -83,7 +83,7 @@ class ChangeServerPresenterTest {
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.changeServerAction).isEqualTo(Async.Uninitialized)
|
assertThat(initialState.changeServerAction).isEqualTo(Async.Uninitialized)
|
||||||
initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(A_HOMESERVER_URL)))
|
initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL)))
|
||||||
val loadingState = awaitItem()
|
val loadingState = awaitItem()
|
||||||
assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java)
|
assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java)
|
||||||
val failureState = awaitItem()
|
val failureState = awaitItem()
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ class ChangeAccountProviderPresenterTest {
|
||||||
assertThat(initialState.accountProviders).isEqualTo(
|
assertThat(initialState.accountProviders).isEqualTo(
|
||||||
listOf(
|
listOf(
|
||||||
AccountProvider(
|
AccountProvider(
|
||||||
|
url = "https://matrix.org",
|
||||||
title = "matrix.org",
|
title = "matrix.org",
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
isPublic = true,
|
isPublic = true,
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ fun LogoutPreferenceView(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LogoutPreferenceContent(
|
private fun LogoutPreferenceContent(
|
||||||
onClick: () -> Unit = {},
|
onClick: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
PreferenceText(
|
PreferenceText(
|
||||||
|
|
@ -38,7 +38,6 @@ dependencies {
|
||||||
implementation(projects.libraries.testtags)
|
implementation(projects.libraries.testtags)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.libraries.dateformatter.api)
|
implementation(projects.libraries.dateformatter.api)
|
||||||
implementation(libs.accompanist.placeholder)
|
|
||||||
api(projects.features.logout.api)
|
api(projects.features.logout.api)
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
|
|
||||||
|
|
|
||||||