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
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2.8.1
|
||||
uses: gradle/gradle-build-action@v2.9.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Assemble debug APK
|
||||
|
|
@ -55,7 +55,7 @@ jobs:
|
|||
name: elementx-debug
|
||||
path: |
|
||||
app/build/outputs/apk/debug/*.apk
|
||||
- uses: rnkdsh/action-upload-diawi@v1.5.2
|
||||
- uses: rnkdsh/action-upload-diawi@v1.5.3
|
||||
id: diawi
|
||||
# Do not fail the whole build if Diawi upload fails
|
||||
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_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_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:
|
||||
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
|
||||
# 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
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2.8.1
|
||||
uses: gradle/gradle-build-action@v2.9.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Dependency analysis
|
||||
|
|
|
|||
2
.github/workflows/quality.yml
vendored
|
|
@ -40,7 +40,7 @@ jobs:
|
|||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2.8.1
|
||||
uses: gradle/gradle-build-action@v2.9.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Run code quality check suite
|
||||
|
|
|
|||
2
.github/workflows/recordScreenshots.yml
vendored
|
|
@ -24,7 +24,7 @@ jobs:
|
|||
java-version: '17'
|
||||
# Add gradle cache, this should speed up the process
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2.8.1
|
||||
uses: gradle/gradle-build-action@v2.9.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Record screenshots
|
||||
|
|
|
|||
2
.github/workflows/release.yml
vendored
|
|
@ -25,7 +25,7 @@ jobs:
|
|||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2.8.1
|
||||
uses: gradle/gradle-build-action@v2.9.0
|
||||
- name: Create app bundle
|
||||
env:
|
||||
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
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2.8.1
|
||||
uses: gradle/gradle-build-action@v2.9.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: 🔊 Publish results to Sonar
|
||||
|
|
|
|||
2
.github/workflows/tests.yml
vendored
|
|
@ -44,7 +44,7 @@ jobs:
|
|||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2.8.1
|
||||
uses: gradle/gradle-build-action@v2.9.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
|
||||
|
|
|
|||
1
.idea/dictionaries/shared.xml
generated
|
|
@ -4,6 +4,7 @@
|
|||
<w>backstack</w>
|
||||
<w>ftue</w>
|
||||
<w>homeserver</w>
|
||||
<w>konsist</w>
|
||||
<w>kover</w>
|
||||
<w>measurables</w>
|
||||
<w>onboarding</w>
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ appId: ${APP_ID}
|
|||
- tapOn:
|
||||
id: "change_server-server"
|
||||
# Test server that does not support sliding sync.
|
||||
- inputText: "gnuradio"
|
||||
- inputText: "https://kieranml.ems-support.element.dev"
|
||||
- hideKeyboard
|
||||
- tapOn: "gnuradio.org"
|
||||
- tapOn: "kieranml.ems-support.element.dev"
|
||||
- extendedWaitUntil:
|
||||
visible: "This server currently doesn’t support sliding sync."
|
||||
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)
|
||||
========================================
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
* [knit](#knit)
|
||||
* [lint](#lint)
|
||||
* [Unit tests](#unit-tests)
|
||||
* [konsist](#konsist)
|
||||
* [Tests](#tests)
|
||||
* [Accessibility](#accessibility)
|
||||
* [Jetpack Compose](#jetpack-compose)
|
||||
|
|
@ -156,6 +157,10 @@ Make sure the following commands execute without any error:
|
|||
./gradlew test
|
||||
</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
|
||||
|
||||
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.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(libs.test.konsist)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,14 +20,16 @@
|
|||
<!-- To be able to install APK from the application -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
||||
<!-- Do not enable enableOnBackInvokedCallback until https://issuetracker.google.com/issues/271303558 is fixed -->
|
||||
<application
|
||||
android:name=".ElementXApplication"
|
||||
android:allowBackup="false"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:enableOnBackInvokedCallback="false"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.ElementX"
|
||||
|
|
@ -81,11 +83,12 @@
|
|||
tools:node="remove" />
|
||||
|
||||
<provider
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false">
|
||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_providers" />
|
||||
</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 io.element.android.libraries.architecture.bindings
|
||||
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.x.di.AppBindings
|
||||
import io.element.android.x.intent.SafeUriHandler
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ package io.element.android.x.di
|
|||
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import io.element.android.features.rageshake.api.reporter.BugReporter
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
import io.element.android.libraries.di.AppScope
|
||||
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.meta.BuildMeta
|
||||
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.ApplicationContext
|
||||
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
|
||||
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarMessage
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
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.verification.SessionVerificationService
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
||||
|
|
|
|||
|
|
@ -26,8 +26,10 @@ import androidx.lifecycle.Lifecycle
|
|||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
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.modality.BuildContext
|
||||
import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
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.waitForChildAttached
|
||||
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.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
|
||||
|
|
@ -95,6 +97,10 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
initialElement = NavTarget.RoomList,
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
permanentNavModel = PermanentNavModel(
|
||||
NavTarget.Permanent,
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins
|
||||
) {
|
||||
|
|
@ -328,7 +334,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
val isFtueDisplayed by ftueState.shouldDisplayFlow.collectAsState()
|
||||
|
||||
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.OidcActionFlow
|
||||
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.animation.rememberDefaultTransitionHandler
|
||||
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.matrix.api.auth.MatrixAuthenticationService
|
||||
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.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
|
@ -69,6 +71,7 @@ class RootFlowNode @AssistedInject constructor(
|
|||
private val matrixClientsHolder: MatrixClientsHolder,
|
||||
private val presenter: RootPresenter,
|
||||
private val bugReportEntryPoint: BugReportEntryPoint,
|
||||
private val signedOutEntryPoint: SignedOutEntryPoint,
|
||||
private val intentResolver: IntentResolver,
|
||||
private val oidcActionFlow: OidcActionFlow,
|
||||
) : BackstackNode<RootFlowNode.NavTarget>(
|
||||
|
|
@ -97,13 +100,20 @@ class RootFlowNode @AssistedInject constructor(
|
|||
.distinctUntilChanged()
|
||||
.onEach { navState ->
|
||||
Timber.v("navState=$navState")
|
||||
if (navState.isLoggedIn) {
|
||||
tryToRestoreLatestSession(
|
||||
onSuccess = { sessionId -> switchToLoggedInFlow(sessionId, navState.cacheIndex) },
|
||||
onFailure = { switchToNotLoggedInFlow() }
|
||||
)
|
||||
} else {
|
||||
switchToNotLoggedInFlow()
|
||||
when (navState.loggedInState) {
|
||||
is LoggedInState.LoggedIn -> {
|
||||
if (navState.loggedInState.isTokenValid) {
|
||||
tryToRestoreLatestSession(
|
||||
onSuccess = { sessionId -> switchToLoggedInFlow(sessionId, navState.cacheIndex) },
|
||||
onFailure = { switchToNotLoggedInFlow() }
|
||||
)
|
||||
} else {
|
||||
switchToSignedOutFlow(SessionId(navState.loggedInState.sessionId))
|
||||
}
|
||||
}
|
||||
LoggedInState.NotLoggedIn -> {
|
||||
switchToNotLoggedInFlow()
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
|
|
@ -118,6 +128,10 @@ class RootFlowNode @AssistedInject constructor(
|
|||
backstack.safeRoot(NavTarget.NotLoggedInFlow)
|
||||
}
|
||||
|
||||
private fun switchToSignedOutFlow(sessionId: SessionId) {
|
||||
backstack.safeRoot(NavTarget.SignedOutFlow(sessionId))
|
||||
}
|
||||
|
||||
private suspend fun restoreSessionIfNeeded(
|
||||
sessionId: SessionId,
|
||||
onFailure: () -> Unit = {},
|
||||
|
|
@ -179,6 +193,11 @@ class RootFlowNode @AssistedInject constructor(
|
|||
val navId: Int
|
||||
) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class SignedOutFlow(
|
||||
val sessionId: SessionId
|
||||
) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object BugReport : NavTarget
|
||||
}
|
||||
|
|
@ -198,6 +217,15 @@ class RootFlowNode @AssistedInject constructor(
|
|||
createNode<LoggedInAppScopeFlowNode>(buildContext, plugins = listOf(inputs, callback))
|
||||
}
|
||||
NavTarget.NotLoggedInFlow -> createNode<NotLoggedInFlowNode>(buildContext)
|
||||
is NavTarget.SignedOutFlow -> {
|
||||
signedOutEntryPoint.nodeBuilder(this, buildContext)
|
||||
.params(
|
||||
SignedOutEntryPoint.Params(
|
||||
sessionId = navTarget.sessionId
|
||||
)
|
||||
)
|
||||
.build()
|
||||
}
|
||||
NavTarget.SplashScreen -> splashNode(buildContext)
|
||||
NavTarget.BugReport -> {
|
||||
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.RoomMembershipObserver
|
||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
|
@ -60,6 +61,7 @@ class RoomLoadedFlowNode @AssistedInject constructor(
|
|||
private val messagesEntryPoint: MessagesEntryPoint,
|
||||
private val roomDetailsEntryPoint: RoomDetailsEntryPoint,
|
||||
private val appNavigationStateService: AppNavigationStateService,
|
||||
private val appCoroutineScope: CoroutineScope,
|
||||
roomComponentFactory: RoomComponentFactory,
|
||||
roomMembershipObserver: RoomMembershipObserver,
|
||||
) : BackstackNode<RoomLoadedFlowNode.NavTarget>(
|
||||
|
|
@ -91,6 +93,16 @@ class RoomLoadedFlowNode @AssistedInject constructor(
|
|||
appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId)
|
||||
fetchRoomMembers()
|
||||
},
|
||||
onResume = {
|
||||
appCoroutineScope.launch {
|
||||
inputs.room.subscribeToSync()
|
||||
}
|
||||
},
|
||||
onPause = {
|
||||
appCoroutineScope.launch {
|
||||
inputs.room.unsubscribeFromSync()
|
||||
}
|
||||
},
|
||||
onDestroy = {
|
||||
Timber.v("OnDestroy")
|
||||
appNavigationStateService.onLeavingRoom(id)
|
||||
|
|
@ -162,9 +174,7 @@ class RoomLoadedFlowNode @AssistedInject constructor(
|
|||
// 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.
|
||||
DisposableEffect(Unit) {
|
||||
inputs.room.subscribeToSync()
|
||||
onDispose {
|
||||
inputs.room.unsubscribeFromSync()
|
||||
if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
|
||||
inputs.room.destroy()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@
|
|||
|
||||
package io.element.android.appnav.root
|
||||
|
||||
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
||||
|
||||
/**
|
||||
* [RootNavState] produced by [RootNavStateFlowFactory].
|
||||
*/
|
||||
|
|
@ -26,7 +28,7 @@ data class RootNavState(
|
|||
*/
|
||||
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.preferences.api.CacheService
|
||||
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.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import javax.inject.Inject
|
||||
|
|
@ -47,9 +47,14 @@ class RootNavStateFlowFactory @Inject constructor(
|
|||
fun create(savedStateMap: SavedStateMap?): Flow<RootNavState> {
|
||||
return combine(
|
||||
cacheIndexFlow(savedStateMap),
|
||||
isUserLoggedInFlow(),
|
||||
) { cacheIndex, isLoggedIn ->
|
||||
RootNavState(cacheIndex = cacheIndex, isLoggedIn = isLoggedIn)
|
||||
authenticationService.loggedInStateFlow(),
|
||||
loginUserStory.loginFlowIsDone,
|
||||
) { 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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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.test.room.FakeMatrixRoom
|
||||
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
|
|
@ -85,6 +87,7 @@ class RoomFlowNodeTest {
|
|||
plugins: List<Plugin>,
|
||||
messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(),
|
||||
roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(),
|
||||
coroutineScope: CoroutineScope,
|
||||
) = RoomLoadedFlowNode(
|
||||
buildContext = BuildContext.root(savedStateMap = null),
|
||||
plugins = plugins,
|
||||
|
|
@ -92,16 +95,21 @@ class RoomFlowNodeTest {
|
|||
roomDetailsEntryPoint = roomDetailsEntryPoint,
|
||||
appNavigationStateService = FakeAppNavigationStateService(),
|
||||
roomMembershipObserver = RoomMembershipObserver(),
|
||||
appCoroutineScope = coroutineScope,
|
||||
roomComponentFactory = FakeRoomComponentFactory(),
|
||||
)
|
||||
|
||||
@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
|
||||
val room = FakeMatrixRoom()
|
||||
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||
val inputs = RoomLoadedFlowNode.Inputs(room)
|
||||
val roomFlowNode = aRoomFlowNode(listOf(inputs), fakeMessagesEntryPoint)
|
||||
val roomFlowNode = aRoomFlowNode(
|
||||
plugins = listOf(inputs),
|
||||
messagesEntryPoint = fakeMessagesEntryPoint,
|
||||
coroutineScope = this
|
||||
)
|
||||
// WHEN
|
||||
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
|
||||
|
||||
|
|
@ -113,13 +121,18 @@ class RoomFlowNodeTest {
|
|||
}
|
||||
|
||||
@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
|
||||
val room = FakeMatrixRoom()
|
||||
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
|
||||
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()
|
||||
// WHEN
|
||||
fakeMessagesEntryPoint.callback?.onRoomDetailsClicked()
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ class RootPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = createPresenter()
|
||||
val presenter = createRootPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -54,7 +54,7 @@ class RootPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - passes app error state`() = runTest {
|
||||
val presenter = createPresenter(
|
||||
val presenter = createRootPresenter(
|
||||
appErrorService = DefaultAppErrorStateService().apply {
|
||||
showError("Bad news", "Something bad happened")
|
||||
}
|
||||
|
|
@ -75,7 +75,7 @@ class RootPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
private fun createPresenter(
|
||||
private fun createRootPresenter(
|
||||
appErrorService: AppErrorStateService = DefaultAppErrorStateService()
|
||||
): RootPresenter {
|
||||
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
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = createPresenter()
|
||||
val presenter = createLoggedInPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -54,7 +54,7 @@ class LoggedInPresenterTest {
|
|||
@Test
|
||||
fun `present - show sync spinner`() = runTest {
|
||||
val roomListService = FakeRoomListService()
|
||||
val presenter = createPresenter(roomListService, NetworkStatus.Online)
|
||||
val presenter = createLoggedInPresenter(roomListService, NetworkStatus.Online)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -66,7 +66,7 @@ class LoggedInPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
private fun createPresenter(
|
||||
private fun createLoggedInPresenter(
|
||||
roomListService: RoomListService = FakeRoomListService(),
|
||||
networkStatus: NetworkStatus = NetworkStatus.Offline
|
||||
): LoggedInPresenter {
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ plugins {
|
|||
}
|
||||
|
||||
tasks.register<Delete>("clean").configure {
|
||||
delete(rootProject.buildDir)
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
}
|
||||
|
||||
allprojects {
|
||||
|
|
@ -86,7 +86,7 @@ allprojects {
|
|||
reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.CHECKSTYLE)
|
||||
}
|
||||
filter {
|
||||
exclude { element -> element.file.path.contains("$buildDir/generated/") }
|
||||
exclude { element -> element.file.path.contains("${layout.buildDirectory.asFile.get()}/generated/") }
|
||||
}
|
||||
}
|
||||
// Dependency check
|
||||
|
|
@ -176,10 +176,13 @@ koverMerged {
|
|||
"*_ModuleKt",
|
||||
"anvil.hint.binding.io.element.*",
|
||||
"anvil.hint.merge.*",
|
||||
"anvil.hint.multibinding.io.element.*",
|
||||
"anvil.module.*",
|
||||
"com.airbnb.android.showkase*",
|
||||
"io.element.android.libraries.designsystem.showkase.*",
|
||||
"io.element.android.x.di.DaggerAppComponent*",
|
||||
"*_Factory",
|
||||
"*_Factory_Impl",
|
||||
"*_Factory$*",
|
||||
"*_Module",
|
||||
"*_Module$*",
|
||||
|
|
@ -228,11 +231,11 @@ koverMerged {
|
|||
name = "Global minimum code coverage."
|
||||
target = kotlinx.kover.api.VerificationTarget.ALL
|
||||
bound {
|
||||
minValue = 55
|
||||
minValue = 60
|
||||
// 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
|
||||
// minValue to 25 and maxValue to 35.
|
||||
maxValue = 65
|
||||
maxValue = 70
|
||||
counter = kotlinx.kover.api.CounterType.INSTRUCTION
|
||||
valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE
|
||||
}
|
||||
|
|
@ -354,7 +357,7 @@ subprojects {
|
|||
subprojects {
|
||||
tasks.withType<KspTask>() {
|
||||
doLast {
|
||||
fileTree(buildDir).apply { include("**/*ShowkaseExtension*.kt") }.files.forEach { file ->
|
||||
fileTree(layout.buildDirectory).apply { include("**/*ShowkaseExtension*.kt") }.files.forEach { file ->
|
||||
ReplaceRegExp().apply {
|
||||
setMatch("^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
|
||||
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)
|
||||
- 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
|
||||
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
|
||||
git clone git@github.com:matrix-org/matrix-rust-sdk.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
|
||||
```
|
||||
|
||||
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`:
|
||||
|
||||
```groovy
|
||||
|
|
@ -280,11 +276,12 @@ Follow these steps to install and configure the plugin and templates:
|
|||
|
||||
1. Install the AS plugin for generating modules :
|
||||
[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
|
||||
- Pick the `tools/templates/file_templates.zip` files
|
||||
- Pick the `tmp/file_templates.zip` files
|
||||
- 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
|
||||
- Click on + / Import From File
|
||||
- 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.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||
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.molecules.InfoListOrganism
|
||||
import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem
|
||||
import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism
|
||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<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_content_link">"這裡"</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
|
||||
* - confineToRoom=true
|
||||
* to ensure that the rendering will bo correct on the embedded Webview.
|
||||
*/
|
||||
private fun Uri.withCustomParameters(): String {
|
||||
val builder = buildUpon()
|
||||
// Remove the existing query parameters
|
||||
builder.clearQuery()
|
||||
queryParameterNames.forEach {
|
||||
if (it == APP_PROMPT_PARAMETER || it == CONFINE_TO_ROOM_PARAMETER) return@forEach
|
||||
builder.appendQueryParameter(it, getQueryParameter(it))
|
||||
}
|
||||
builder.appendQueryParameter(APP_PROMPT_PARAMETER, "false")
|
||||
builder.appendQueryParameter(CONFINE_TO_ROOM_PARAMETER, "true")
|
||||
return builder.build().toString()
|
||||
// Remove the existing fragment parameters, and build the new fragment
|
||||
val currentFragment = fragment ?: ""
|
||||
// Reset the current fragment
|
||||
builder.fragment("")
|
||||
val queryFragmentPosition = currentFragment.lastIndexOf("?")
|
||||
val newFragment = if (queryFragmentPosition == -1) {
|
||||
// No existing query, build it.
|
||||
"$currentFragment?$APP_PROMPT_PARAMETER=false&$CONFINE_TO_ROOM_PARAMETER=true"
|
||||
} else {
|
||||
buildString {
|
||||
append(currentFragment.substring(0, queryFragmentPosition + 1))
|
||||
val queryFragment = currentFragment.substring(queryFragmentPosition + 1)
|
||||
// Replace the existing parameters
|
||||
val newQueryFragment = queryFragment
|
||||
.replace("$APP_PROMPT_PARAMETER=true", "$APP_PROMPT_PARAMETER=false")
|
||||
.replace("$CONFINE_TO_ROOM_PARAMETER=false", "$CONFINE_TO_ROOM_PARAMETER=true")
|
||||
append(newQueryFragment)
|
||||
// Ensure the parameters are there
|
||||
if (!newQueryFragment.contains("$APP_PROMPT_PARAMETER=false")) {
|
||||
if (newQueryFragment.isNotEmpty()) {
|
||||
append("&")
|
||||
}
|
||||
append("$APP_PROMPT_PARAMETER=false")
|
||||
}
|
||||
if (!newQueryFragment.contains("$CONFINE_TO_ROOM_PARAMETER=true")) {
|
||||
append("&$CONFINE_TO_ROOM_PARAMETER=true")
|
||||
}
|
||||
}
|
||||
}
|
||||
// We do not want to encode the Fragment part, so append it manually
|
||||
return builder.build().toString() + "#" + newFragment
|
||||
}
|
||||
|
||||
private const val APP_PROMPT_PARAMETER = "appPrompt"
|
||||
|
|
|
|||
|
|
@ -35,92 +35,51 @@ class CallIntentDataParserTests {
|
|||
|
||||
@Test
|
||||
fun `empty data returns null`() {
|
||||
val url = ""
|
||||
assertThat(callIntentDataParser.parse(url)).isNull()
|
||||
doTest("", null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invalid data returns null`() {
|
||||
val url = "!"
|
||||
assertThat(callIntentDataParser.parse(url)).isNull()
|
||||
doTest("!", null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `data with no scheme returns null`() {
|
||||
val url = "test"
|
||||
assertThat(callIntentDataParser.parse(url)).isNull()
|
||||
doTest("test", null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Element Call http urls returns null`() {
|
||||
val httpBaseUrl = "http://call.element.io"
|
||||
val httpCallUrl = "http://call.element.io/some-actual-call?with=parameters"
|
||||
assertThat(callIntentDataParser.parse(httpBaseUrl)).isNull()
|
||||
assertThat(callIntentDataParser.parse(httpCallUrl)).isNull()
|
||||
doTest("http://call.element.io", null)
|
||||
doTest("http://call.element.io/some-actual-call?with=parameters", null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Element Call urls will be returned as is`() {
|
||||
val httpsBaseUrl = "https://call.element.io"
|
||||
val httpsCallUrl = VALID_CALL_URL_WITH_PARAM
|
||||
assertThat(callIntentDataParser.parse(httpsBaseUrl)).isEqualTo("$httpsBaseUrl?$EXTRA_PARAMS")
|
||||
assertThat(callIntentDataParser.parse(httpsCallUrl)).isEqualTo("$httpsCallUrl&$EXTRA_PARAMS")
|
||||
doTest(
|
||||
url = "https://call.element.io",
|
||||
expectedResult = "https://call.element.io#?$EXTRA_PARAMS"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Element Call url with url param gets url extracted`() {
|
||||
doTest(
|
||||
url = VALID_CALL_URL_WITH_PARAM,
|
||||
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `HTTP and HTTPS urls that don't come from EC return null`() {
|
||||
val httpBaseUrl = "http://app.element.io"
|
||||
val httpsBaseUrl = "https://app.element.io"
|
||||
val httpInvalidUrl = "http://"
|
||||
val httpsInvalidUrl = "http://"
|
||||
assertThat(callIntentDataParser.parse(httpBaseUrl)).isNull()
|
||||
assertThat(callIntentDataParser.parse(httpsBaseUrl)).isNull()
|
||||
assertThat(callIntentDataParser.parse(httpInvalidUrl)).isNull()
|
||||
assertThat(callIntentDataParser.parse(httpsInvalidUrl)).isNull()
|
||||
doTest("http://app.element.io", null)
|
||||
doTest("https://app.element.io", null, testEmbedded = false)
|
||||
doTest("http://", null)
|
||||
doTest("https://", null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `element scheme with call host and url with http will 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`() {
|
||||
fun `Element Call url with no url returns null`() {
|
||||
val embeddedUrl = VALID_CALL_URL_WITH_PARAM
|
||||
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
|
||||
val url = "io.element.call:/?no_url=$encodedUrl"
|
||||
|
|
@ -142,7 +101,7 @@ class CallIntentDataParserTests {
|
|||
}
|
||||
|
||||
@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="
|
||||
assertThat(callIntentDataParser.parse(url)).isNull()
|
||||
}
|
||||
|
|
@ -156,29 +115,108 @@ class CallIntentDataParserTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `element scheme 2 with url extra param appPrompt gets url extracted`() {
|
||||
val embeddedUrl = "${VALID_CALL_URL_WITH_PARAM}&appPrompt=true"
|
||||
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")
|
||||
fun `Element Call url with url extra param appPrompt gets url extracted`() {
|
||||
doTest(
|
||||
url = "${VALID_CALL_URL_WITH_PARAM}&appPrompt=true",
|
||||
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `element scheme 2 with url extra param confineToRoom gets url extracted`() {
|
||||
val embeddedUrl = "${VALID_CALL_URL_WITH_PARAM}&confineToRoom=false"
|
||||
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")
|
||||
fun `Element Call url with url extra param in fragment appPrompt gets url extracted`() {
|
||||
doTest(
|
||||
url = "${VALID_CALL_URL_WITH_PARAM}#?appPrompt=true",
|
||||
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?appPrompt=false&confineToRoom=true"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `element scheme 2 with url fragment gets url extracted`() {
|
||||
val embeddedUrl = "${VALID_CALL_URL_WITH_PARAM}#fragment"
|
||||
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#fragment")
|
||||
fun `Element Call url with url extra param in fragment appPrompt and other gets url extracted`() {
|
||||
doTest(
|
||||
url = "${VALID_CALL_URL_WITH_PARAM}#?appPrompt=true&otherParam=maybe",
|
||||
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?appPrompt=false&otherParam=maybe&confineToRoom=true"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Element Call url with url extra param confineToRoom gets url extracted`() {
|
||||
doTest(
|
||||
url = "${VALID_CALL_URL_WITH_PARAM}&confineToRoom=false",
|
||||
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Element Call url with url extra param in fragment confineToRoom gets url extracted`() {
|
||||
doTest(
|
||||
url = "${VALID_CALL_URL_WITH_PARAM}#?confineToRoom=false",
|
||||
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?confineToRoom=true&appPrompt=false"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Element Call url with url extra param in fragment confineToRoom and more gets url extracted`() {
|
||||
doTest(
|
||||
url = "${VALID_CALL_URL_WITH_PARAM}#?confineToRoom=false&otherParam=maybe",
|
||||
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?confineToRoom=true&otherParam=maybe&appPrompt=false"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Element Call url with url fragment gets url extracted`() {
|
||||
doTest(
|
||||
url = "${VALID_CALL_URL_WITH_PARAM}#fragment",
|
||||
expectedResult = "$VALID_CALL_URL_WITH_PARAM#fragment?$EXTRA_PARAMS"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Element Call url with url fragment with params gets url extracted`() {
|
||||
doTest(
|
||||
url = "${VALID_CALL_URL_WITH_PARAM}#fragment?otherParam=maybe",
|
||||
expectedResult = "$VALID_CALL_URL_WITH_PARAM#fragment?otherParam=maybe&$EXTRA_PARAMS"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Element Call url with url fragment with other params gets url extracted`() {
|
||||
doTest(
|
||||
url = "${VALID_CALL_URL_WITH_PARAM}#?otherParam=maybe",
|
||||
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?otherParam=maybe&$EXTRA_PARAMS"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Element Call url with empty fragment`() {
|
||||
doTest(
|
||||
url = "${VALID_CALL_URL_WITH_PARAM}#",
|
||||
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Element Call url with empty fragment query`() {
|
||||
doTest(
|
||||
url = "${VALID_CALL_URL_WITH_PARAM}#?",
|
||||
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS"
|
||||
)
|
||||
}
|
||||
|
||||
private fun doTest(url: String, expectedResult: String?, testEmbedded: Boolean = true) {
|
||||
// Test direct parsing
|
||||
assertThat(callIntentDataParser.parse(url)).isEqualTo(expectedResult)
|
||||
|
||||
if (testEmbedded) {
|
||||
// Test embedded url, scheme 1
|
||||
val encodedUrl = URLEncoder.encode(url, "utf-8")
|
||||
val urlScheme1 = "element://call?url=$encodedUrl"
|
||||
assertThat(callIntentDataParser.parse(urlScheme1)).isEqualTo(expectedResult)
|
||||
|
||||
// Test embedded url, scheme 2
|
||||
val urlScheme2 = "io.element.call:/?url=$encodedUrl"
|
||||
assertThat(callIntentDataParser.parse(urlScheme2)).isEqualTo(expectedResult)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val VALID_CALL_URL_WITH_PARAM = "https://call.element.io/some-actual-call?with=parameters"
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ fun AddPeopleView(
|
|||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AddPeopleViewTopBar(
|
||||
private fun AddPeopleViewTopBar(
|
||||
hasSelectedUsers: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onBackPressed: () -> Unit = {},
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ fun ConfigureRoomView(
|
|||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ConfigureRoomToolbar(
|
||||
private fun ConfigureRoomToolbar(
|
||||
isNextActionEnabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onBackPressed: () -> Unit = {},
|
||||
|
|
@ -207,7 +207,7 @@ fun ConfigureRoomToolbar(
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun RoomNameWithAvatar(
|
||||
private fun RoomNameWithAvatar(
|
||||
avatarUri: Uri?,
|
||||
roomName: String,
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -235,7 +235,7 @@ fun RoomNameWithAvatar(
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun RoomTopic(
|
||||
private fun RoomTopic(
|
||||
topic: String,
|
||||
modifier: Modifier = Modifier,
|
||||
onTopicChanged: (String) -> Unit = {},
|
||||
|
|
@ -254,7 +254,7 @@ fun RoomTopic(
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun RoomPrivacyOptions(
|
||||
private fun RoomPrivacyOptions(
|
||||
selected: RoomPrivacy?,
|
||||
modifier: Modifier = Modifier,
|
||||
onOptionSelected: (RoomPrivacyItem) -> Unit = {},
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ fun CreateRoomRootView(
|
|||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CreateRoomRootViewTopBar(
|
||||
private fun CreateRoomRootViewTopBar(
|
||||
modifier: Modifier = Modifier,
|
||||
onClosePressed: () -> Unit = {},
|
||||
) {
|
||||
|
|
@ -148,7 +148,7 @@ fun CreateRoomRootViewTopBar(
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun CreateRoomActionButtonsList(
|
||||
private fun CreateRoomActionButtonsList(
|
||||
state: CreateRoomRootState,
|
||||
modifier: Modifier = Modifier,
|
||||
onNewRoomClicked: () -> Unit = {},
|
||||
|
|
@ -169,7 +169,7 @@ fun CreateRoomActionButtonsList(
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun CreateRoomActionButton(
|
||||
private fun CreateRoomActionButton(
|
||||
@DrawableRes iconRes: Int,
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@
|
|||
<string name="screen_create_room_action_invite_people">"邀請朋友使用 Element"</string>
|
||||
<string name="screen_create_room_add_people_title">"邀請夥伴"</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_topic_label">"主題(非必填)"</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.libraries.designsystem.atomic.atoms.ElementLogoAtom
|
||||
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.molecules.InfoListOrganism
|
||||
import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem
|
||||
import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism
|
||||
import io.element.android.libraries.designsystem.atomic.pages.OnBoardingPage
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_subtitle">"我們有些事想告訴您:"</string>
|
||||
<string name="screen_welcome_title">"歡迎使用 %1$s!"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed
|
||||
@Suppress("DSL_SCOPE_VIOLATION")
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
alias(libs.plugins.anvil)
|
||||
|
|
|
|||
|
|
@ -25,10 +25,10 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
@Immutable
|
||||
data class InviteListState(
|
||||
val inviteList: ImmutableList<InviteListInviteSummary>,
|
||||
val declineConfirmationDialog: InviteDeclineConfirmationDialog = InviteDeclineConfirmationDialog.Hidden,
|
||||
val acceptedAction: Async<RoomId> = Async.Uninitialized,
|
||||
val declinedAction: Async<Unit> = Async.Uninitialized,
|
||||
val eventSink: (InviteListEvents) -> Unit = {}
|
||||
val declineConfirmationDialog: InviteDeclineConfirmationDialog,
|
||||
val acceptedAction: Async<RoomId>,
|
||||
val declinedAction: Async<Unit>,
|
||||
val eventSink: (InviteListEvents) -> Unit
|
||||
)
|
||||
|
||||
sealed interface InviteDeclineConfirmationDialog {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,10 @@ open class InviteListStateProvider : PreviewParameterProvider<InviteListState> {
|
|||
|
||||
internal fun aInviteListState() = InviteListState(
|
||||
inviteList = aInviteListInviteSummaryList(),
|
||||
declineConfirmationDialog = InviteDeclineConfirmationDialog.Hidden,
|
||||
acceptedAction = Async.Uninitialized,
|
||||
declinedAction = Async.Uninitialized,
|
||||
eventSink = {},
|
||||
)
|
||||
|
||||
internal fun aInviteListInviteSummaryList(): ImmutableList<InviteListInviteSummary> {
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ fun InviteListView(
|
|||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun InviteListContent(
|
||||
private fun InviteListContent(
|
||||
state: InviteListState,
|
||||
modifier: Modifier = Modifier,
|
||||
onBackClicked: () -> Unit = {},
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ internal fun InviteSummaryRow(
|
|||
}
|
||||
|
||||
@Composable
|
||||
internal fun DefaultInviteSummaryRow(
|
||||
private fun DefaultInviteSummaryRow(
|
||||
invite: InviteListInviteSummary,
|
||||
onAcceptClicked: () -> Unit = {},
|
||||
onDeclineClicked: () -> Unit = {},
|
||||
|
|
|
|||
|
|
@ -19,10 +19,10 @@ package io.element.android.features.leaveroom.api
|
|||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
data class LeaveRoomState(
|
||||
val confirmation: Confirmation = Confirmation.Hidden,
|
||||
val progress: Progress = Progress.Hidden,
|
||||
val error: Error = Error.Hidden,
|
||||
val eventSink: (LeaveRoomEvent) -> Unit = {},
|
||||
val confirmation: Confirmation,
|
||||
val progress: Progress,
|
||||
val error: Error,
|
||||
val eventSink: (LeaveRoomEvent) -> Unit,
|
||||
) {
|
||||
sealed interface Confirmation {
|
||||
data object Hidden : Confirmation
|
||||
|
|
|
|||
|
|
@ -22,32 +22,32 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||
class LeaveRoomStateProvider : PreviewParameterProvider<LeaveRoomState> {
|
||||
override val values: Sequence<LeaveRoomState>
|
||||
get() = sequenceOf(
|
||||
LeaveRoomState(
|
||||
aLeaveRoomState(
|
||||
confirmation = LeaveRoomState.Confirmation.Hidden,
|
||||
progress = LeaveRoomState.Progress.Hidden,
|
||||
error = LeaveRoomState.Error.Hidden,
|
||||
),
|
||||
LeaveRoomState(
|
||||
aLeaveRoomState(
|
||||
confirmation = LeaveRoomState.Confirmation.Generic(A_ROOM_ID),
|
||||
progress = LeaveRoomState.Progress.Hidden,
|
||||
error = LeaveRoomState.Error.Hidden,
|
||||
),
|
||||
LeaveRoomState(
|
||||
aLeaveRoomState(
|
||||
confirmation = LeaveRoomState.Confirmation.PrivateRoom(A_ROOM_ID),
|
||||
progress = LeaveRoomState.Progress.Hidden,
|
||||
error = LeaveRoomState.Error.Hidden,
|
||||
),
|
||||
LeaveRoomState(
|
||||
aLeaveRoomState(
|
||||
confirmation = LeaveRoomState.Confirmation.LastUserInRoom(A_ROOM_ID),
|
||||
progress = LeaveRoomState.Progress.Hidden,
|
||||
error = LeaveRoomState.Error.Hidden,
|
||||
),
|
||||
LeaveRoomState(
|
||||
aLeaveRoomState(
|
||||
confirmation = LeaveRoomState.Confirmation.Hidden,
|
||||
progress = LeaveRoomState.Progress.Shown,
|
||||
error = LeaveRoomState.Error.Hidden,
|
||||
),
|
||||
LeaveRoomState(
|
||||
aLeaveRoomState(
|
||||
confirmation = LeaveRoomState.Confirmation.Hidden,
|
||||
progress = LeaveRoomState.Progress.Hidden,
|
||||
error = LeaveRoomState.Error.Shown,
|
||||
|
|
@ -56,3 +56,14 @@ class LeaveRoomStateProvider : PreviewParameterProvider<LeaveRoomState> {
|
|||
}
|
||||
|
||||
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
|
||||
fun `present - initial state hides all dialogs`() = runTest {
|
||||
val presenter = createPresenter()
|
||||
val presenter = createLeaveRoomPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -57,7 +57,7 @@ class LeaveRoomPresenterImplTest {
|
|||
|
||||
@Test
|
||||
fun `present - show generic confirmation`() = runTest {
|
||||
val presenter = createPresenter(
|
||||
val presenter = createLeaveRoomPresenter(
|
||||
client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(
|
||||
roomId = A_ROOM_ID,
|
||||
|
|
@ -77,7 +77,7 @@ class LeaveRoomPresenterImplTest {
|
|||
|
||||
@Test
|
||||
fun `present - show private room confirmation`() = runTest {
|
||||
val presenter = createPresenter(
|
||||
val presenter = createLeaveRoomPresenter(
|
||||
client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(
|
||||
roomId = A_ROOM_ID,
|
||||
|
|
@ -97,7 +97,7 @@ class LeaveRoomPresenterImplTest {
|
|||
|
||||
@Test
|
||||
fun `present - show last user in room confirmation`() = runTest {
|
||||
val presenter = createPresenter(
|
||||
val presenter = createLeaveRoomPresenter(
|
||||
client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(
|
||||
roomId = A_ROOM_ID,
|
||||
|
|
@ -118,7 +118,7 @@ class LeaveRoomPresenterImplTest {
|
|||
@Test
|
||||
fun `present - leaving a room leaves the room`() = runTest {
|
||||
val roomMembershipObserver = RoomMembershipObserver()
|
||||
val presenter = createPresenter(
|
||||
val presenter = createLeaveRoomPresenter(
|
||||
client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(
|
||||
roomId = A_ROOM_ID,
|
||||
|
|
@ -140,7 +140,7 @@ class LeaveRoomPresenterImplTest {
|
|||
|
||||
@Test
|
||||
fun `present - show error if leave room fails`() = runTest {
|
||||
val presenter = createPresenter(
|
||||
val presenter = createLeaveRoomPresenter(
|
||||
client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(
|
||||
roomId = A_ROOM_ID,
|
||||
|
|
@ -164,7 +164,7 @@ class LeaveRoomPresenterImplTest {
|
|||
|
||||
@Test
|
||||
fun `present - show progress indicator while leaving a room`() = runTest {
|
||||
val presenter = createPresenter(
|
||||
val presenter = createLeaveRoomPresenter(
|
||||
client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(
|
||||
roomId = A_ROOM_ID,
|
||||
|
|
@ -186,7 +186,7 @@ class LeaveRoomPresenterImplTest {
|
|||
|
||||
@Test
|
||||
fun `present - hide error hides the error`() = runTest {
|
||||
val presenter = createPresenter(
|
||||
val presenter = createLeaveRoomPresenter(
|
||||
client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(
|
||||
roomId = A_ROOM_ID,
|
||||
|
|
@ -212,7 +212,7 @@ class LeaveRoomPresenterImplTest {
|
|||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createPresenter(
|
||||
private fun TestScope.createLeaveRoomPresenter(
|
||||
client: MatrixClient = FakeMatrixClient(),
|
||||
roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(),
|
||||
): 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.LeaveRoomPresenter
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import javax.inject.Inject
|
||||
|
||||
class LeaveRoomPresenterFake @Inject constructor() : LeaveRoomPresenter {
|
||||
class FakeLeaveRoomPresenter : LeaveRoomPresenter {
|
||||
|
||||
val events = mutableListOf<LeaveRoomEvent>()
|
||||
|
||||
|
|
@ -30,7 +29,12 @@ class LeaveRoomPresenterFake @Inject constructor() : LeaveRoomPresenter {
|
|||
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) {
|
||||
field = value.copy(eventSink = ::handleEvent)
|
||||
}
|
||||
|
|
@ -17,9 +17,9 @@
|
|||
package io.element.android.features.location.impl.common.permissions
|
||||
|
||||
data class PermissionsState(
|
||||
val permissions: Permissions = Permissions.NoneGranted,
|
||||
val shouldShowRationale: Boolean = false,
|
||||
val eventSink: (PermissionsEvents) -> Unit = {},
|
||||
val permissions: Permissions,
|
||||
val shouldShowRationale: Boolean,
|
||||
val eventSink: (PermissionsEvents) -> Unit,
|
||||
) {
|
||||
sealed interface Permissions {
|
||||
data object AllGranted : Permissions
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@
|
|||
package io.element.android.features.location.impl.send
|
||||
|
||||
data class SendLocationState(
|
||||
val permissionDialog: Dialog = Dialog.None,
|
||||
val mode: Mode = Mode.PinLocation,
|
||||
val hasLocationPermission: Boolean = false,
|
||||
val appName: String = "AppName",
|
||||
val eventSink: (SendLocationEvents) -> Unit = {},
|
||||
val permissionDialog: Dialog,
|
||||
val mode: Mode,
|
||||
val hasLocationPermission: Boolean,
|
||||
val appName: String,
|
||||
val eventSink: (SendLocationEvents) -> Unit,
|
||||
) {
|
||||
sealed interface Mode {
|
||||
data object SenderLocation : Mode
|
||||
|
|
|
|||
|
|
@ -23,35 +23,44 @@ private const val APP_NAME = "ApplicationName"
|
|||
class SendLocationStateProvider : PreviewParameterProvider<SendLocationState> {
|
||||
override val values: Sequence<SendLocationState>
|
||||
get() = sequenceOf(
|
||||
SendLocationState(
|
||||
aSendLocationState(
|
||||
permissionDialog = SendLocationState.Dialog.None,
|
||||
mode = SendLocationState.Mode.PinLocation,
|
||||
hasLocationPermission = false,
|
||||
appName = APP_NAME,
|
||||
),
|
||||
SendLocationState(
|
||||
aSendLocationState(
|
||||
permissionDialog = SendLocationState.Dialog.PermissionDenied,
|
||||
mode = SendLocationState.Mode.PinLocation,
|
||||
hasLocationPermission = false,
|
||||
appName = APP_NAME,
|
||||
),
|
||||
SendLocationState(
|
||||
aSendLocationState(
|
||||
permissionDialog = SendLocationState.Dialog.PermissionRationale,
|
||||
mode = SendLocationState.Mode.PinLocation,
|
||||
hasLocationPermission = false,
|
||||
appName = APP_NAME,
|
||||
),
|
||||
SendLocationState(
|
||||
aSendLocationState(
|
||||
permissionDialog = SendLocationState.Dialog.None,
|
||||
mode = SendLocationState.Mode.PinLocation,
|
||||
hasLocationPermission = true,
|
||||
appName = APP_NAME,
|
||||
),
|
||||
SendLocationState(
|
||||
aSendLocationState(
|
||||
permissionDialog = SendLocationState.Dialog.None,
|
||||
mode = SendLocationState.Mode.SenderLocation,
|
||||
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
|
||||
}
|
||||
|
||||
private var state = PermissionsState(eventSink = ::handleEvent)
|
||||
private var state = PermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = false,
|
||||
eventSink = ::handleEvent
|
||||
)
|
||||
set(value) {
|
||||
field = value.copy(eventSink = ::handleEvent)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import app.cash.turbine.test
|
|||
import com.google.common.truth.Truth
|
||||
import im.vector.app.features.analytics.plan.Composer
|
||||
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.permissions.PermissionsEvents
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
|
||||
|
|
@ -65,7 +66,7 @@ class SendLocationPresenterTest {
|
|||
@Test
|
||||
fun `initial state with permissions granted`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
PermissionsState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.AllGranted,
|
||||
shouldShowRationale = false,
|
||||
)
|
||||
|
|
@ -92,7 +93,7 @@ class SendLocationPresenterTest {
|
|||
@Test
|
||||
fun `initial state with permissions partially granted`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
PermissionsState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.SomeGranted,
|
||||
shouldShowRationale = false,
|
||||
)
|
||||
|
|
@ -119,7 +120,7 @@ class SendLocationPresenterTest {
|
|||
@Test
|
||||
fun `initial state with permissions denied`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
PermissionsState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = false,
|
||||
)
|
||||
|
|
@ -145,7 +146,7 @@ class SendLocationPresenterTest {
|
|||
@Test
|
||||
fun `initial state with permissions denied once`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
PermissionsState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = true,
|
||||
)
|
||||
|
|
@ -171,7 +172,7 @@ class SendLocationPresenterTest {
|
|||
@Test
|
||||
fun `rationale dialog dismiss`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
PermissionsState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = true,
|
||||
)
|
||||
|
|
@ -202,7 +203,7 @@ class SendLocationPresenterTest {
|
|||
@Test
|
||||
fun `rationale dialog continue`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
PermissionsState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = true,
|
||||
)
|
||||
|
|
@ -230,7 +231,7 @@ class SendLocationPresenterTest {
|
|||
@Test
|
||||
fun `permission denied dialog dismiss`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
PermissionsState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = false,
|
||||
)
|
||||
|
|
@ -261,7 +262,7 @@ class SendLocationPresenterTest {
|
|||
@Test
|
||||
fun `share sender location`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
PermissionsState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.AllGranted,
|
||||
shouldShowRationale = false,
|
||||
)
|
||||
|
|
@ -317,7 +318,7 @@ class SendLocationPresenterTest {
|
|||
@Test
|
||||
fun `share pin location`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
PermissionsState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = false,
|
||||
)
|
||||
|
|
@ -373,7 +374,7 @@ class SendLocationPresenterTest {
|
|||
@Test
|
||||
fun `composer context passes through analytics`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
PermissionsState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = false,
|
||||
)
|
||||
|
|
@ -419,7 +420,7 @@ class SendLocationPresenterTest {
|
|||
@Test
|
||||
fun `open settings activity`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
PermissionsState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = false,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import app.cash.molecule.moleculeFlow
|
|||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth
|
||||
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.permissions.PermissionsEvents
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
|
||||
|
|
@ -55,7 +56,7 @@ class ShowLocationPresenterTest {
|
|||
@Test
|
||||
fun `emits initial state with no location permission`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
PermissionsState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = false,
|
||||
)
|
||||
|
|
@ -75,7 +76,7 @@ class ShowLocationPresenterTest {
|
|||
@Test
|
||||
fun `emits initial state location permission denied once`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
PermissionsState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = true,
|
||||
)
|
||||
|
|
@ -94,7 +95,7 @@ class ShowLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
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) {
|
||||
presenter.present()
|
||||
|
|
@ -109,7 +110,7 @@ class ShowLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
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) {
|
||||
presenter.present()
|
||||
|
|
@ -137,7 +138,7 @@ class ShowLocationPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `centers on user location`() = runTest {
|
||||
permissionsPresenterFake.givenState(PermissionsState(permissions = PermissionsState.Permissions.AllGranted))
|
||||
permissionsPresenterFake.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted))
|
||||
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -166,7 +167,7 @@ class ShowLocationPresenterTest {
|
|||
@Test
|
||||
fun `rationale dialog dismiss`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
PermissionsState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = true,
|
||||
)
|
||||
|
|
@ -197,7 +198,7 @@ class ShowLocationPresenterTest {
|
|||
@Test
|
||||
fun `rationale dialog continue`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
PermissionsState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = true,
|
||||
)
|
||||
|
|
@ -225,7 +226,7 @@ class ShowLocationPresenterTest {
|
|||
@Test
|
||||
fun `permission denied dialog dismiss`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
PermissionsState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = false,
|
||||
)
|
||||
|
|
@ -256,7 +257,7 @@ class ShowLocationPresenterTest {
|
|||
@Test
|
||||
fun `open settings activity`() = runTest {
|
||||
permissionsPresenterFake.givenState(
|
||||
PermissionsState(
|
||||
aPermissionsState(
|
||||
permissions = PermissionsState.Permissions.NoneGranted,
|
||||
shouldShowRationale = false,
|
||||
)
|
||||
|
|
@ -290,7 +291,6 @@ class ShowLocationPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
private const val A_DESCRIPTION = "My happy place"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,9 @@
|
|||
|
||||
package io.element.android.features.login.impl.accountprovider
|
||||
|
||||
data class AccountProvider constructor(
|
||||
val title: String,
|
||||
data class AccountProvider(
|
||||
val url: String,
|
||||
val title: String = url.removePrefix("https://").removePrefix("http://"),
|
||||
val subtitle: String? = null,
|
||||
val isPublic: Boolean = false,
|
||||
val isMatrixOrg: Boolean = false,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.features.login.impl.accountprovider
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.login.impl.util.LoginConstants
|
||||
|
||||
open class AccountProviderProvider : PreviewParameterProvider<AccountProvider> {
|
||||
override val values: Sequence<AccountProvider>
|
||||
|
|
@ -31,7 +32,7 @@ open class AccountProviderProvider : PreviewParameterProvider<AccountProvider> {
|
|||
}
|
||||
|
||||
fun anAccountProvider() = AccountProvider(
|
||||
title = "matrix.org",
|
||||
url = LoginConstants.MATRIX_ORG_URL,
|
||||
subtitle = "Matrix.org is an open network for secure, decentralized communication.",
|
||||
isPublic = true,
|
||||
isMatrixOrg = true,
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ class ChangeServerPresenter @Inject constructor(
|
|||
changeServerAction: MutableState<Async<Unit>>,
|
||||
) = launch {
|
||||
suspend {
|
||||
authenticationService.setHomeserver(data.title).map {
|
||||
authenticationService.setHomeserver(data.url).map {
|
||||
authenticationService.getHomeserverDetails().value!!
|
||||
// Valid, remember user choice
|
||||
accountProviderDataSource.userSelection(data)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.res.stringResource
|
||||
import io.element.android.features.login.impl.R
|
||||
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
|
||||
|
||||
@Composable
|
||||
|
|
@ -39,3 +41,12 @@ internal fun SlidingSyncNotSupportedDialog(
|
|||
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 io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||
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 javax.inject.Inject
|
||||
|
||||
|
|
@ -33,7 +34,7 @@ class ChangeAccountProviderPresenter @Inject constructor(
|
|||
// Just matrix.org by default for now
|
||||
accountProviders = listOf(
|
||||
AccountProvider(
|
||||
title = "matrix.org",
|
||||
url = LoginConstants.MATRIX_ORG_URL,
|
||||
subtitle = null,
|
||||
isPublic = true,
|
||||
isMatrixOrg = true,
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ fun ChangeAccountProviderView(
|
|||
// Other
|
||||
AccountProviderView(
|
||||
item = AccountProvider(
|
||||
url = "",
|
||||
title = stringResource(id = R.string.screen_change_account_provider_other),
|
||||
),
|
||||
onClick = onOtherProviderClicked
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
|
|||
fun handleEvents(event: ConfirmAccountProviderEvents) {
|
||||
when (event) {
|
||||
ConfirmAccountProviderEvents.Continue -> {
|
||||
localCoroutineScope.submit(accountProvider.title, loginFlowAction)
|
||||
localCoroutineScope.submit(accountProvider.url, loginFlowAction)
|
||||
}
|
||||
ConfirmAccountProviderEvents.ClearError -> loginFlowAction.value = Async.Uninitialized
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ data class ConfirmAccountProviderState(
|
|||
val loginFlow: Async<LoginFlow>,
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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.testtags.TestTags
|
||||
import io.element.android.libraries.testtags.testTag
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun ConfirmAccountProviderView(
|
||||
|
|
@ -86,7 +87,7 @@ fun ConfirmAccountProviderView(
|
|||
footer = {
|
||||
ButtonColumnMolecule {
|
||||
Button(
|
||||
text = stringResource(id = R.string.screen_account_provider_continue),
|
||||
text = stringResource(id = CommonStrings.action_continue),
|
||||
showProgress = isLoading,
|
||||
onClick = { eventSink.invoke(ConfirmAccountProviderEvents.Continue) },
|
||||
enabled = state.submitEnabled || isLoading,
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ fun LoginPasswordView(
|
|||
Spacer(modifier = Modifier.weight(1f))
|
||||
// Submit
|
||||
Button(
|
||||
text = stringResource(R.string.screen_login_submit),
|
||||
text = stringResource(CommonStrings.action_continue),
|
||||
showProgress = isLoading,
|
||||
onClick = ::submit,
|
||||
enabled = state.submitEnabled || isLoading,
|
||||
|
|
@ -167,7 +167,7 @@ fun LoginPasswordView(
|
|||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
internal fun LoginForm(
|
||||
private fun LoginForm(
|
||||
state: LoginPasswordState,
|
||||
isLoading: Boolean,
|
||||
onSubmit: () -> Unit,
|
||||
|
|
@ -199,7 +199,7 @@ internal fun LoginForm(
|
|||
eventSink(LoginPasswordEvents.SetLogin(it))
|
||||
}),
|
||||
placeholder = {
|
||||
Text(text = stringResource(R.string.screen_login_username_hint))
|
||||
Text(text = stringResource(CommonStrings.common_username))
|
||||
},
|
||||
onValueChange = {
|
||||
loginFieldState = it
|
||||
|
|
@ -246,7 +246,7 @@ internal fun LoginForm(
|
|||
eventSink(LoginPasswordEvents.SetPassword(it))
|
||||
},
|
||||
placeholder = {
|
||||
Text(text = stringResource(R.string.screen_login_password_hint))
|
||||
Text(text = stringResource(CommonStrings.common_password))
|
||||
},
|
||||
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
trailingIcon = {
|
||||
|
|
@ -272,7 +272,7 @@ internal fun LoginForm(
|
|||
}
|
||||
|
||||
@Composable
|
||||
internal fun LoginErrorDialog(error: Throwable, onDismiss: () -> Unit) {
|
||||
private fun LoginErrorDialog(error: Throwable, onDismiss: () -> Unit) {
|
||||
ErrorDialog(
|
||||
title = stringResource(id = CommonStrings.dialog_title_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 io.element.android.features.login.impl.changeserver.aChangeServerState
|
||||
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
|
||||
|
||||
open class SearchAccountProviderStateProvider : PreviewParameterProvider<SearchAccountProviderState> {
|
||||
|
|
@ -49,7 +50,7 @@ fun aHomeserverDataList(): List<HomeserverData> {
|
|||
}
|
||||
|
||||
fun aHomeserverData(
|
||||
homeserverUrl: String = "https://matrix.org",
|
||||
homeserverUrl: String = LoginConstants.MATRIX_ORG_URL,
|
||||
isWellknownValid: Boolean = true,
|
||||
supportSlidingSync: Boolean = true,
|
||||
): 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.ChangeServerView
|
||||
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.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
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.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||
|
|
@ -195,9 +196,9 @@ fun SearchAccountProviderView(
|
|||
|
||||
@Composable
|
||||
private fun HomeserverData.toAccountProvider(): AccountProvider {
|
||||
val isMatrixOrg = homeserverUrl == "https://matrix.org"
|
||||
val isMatrixOrg = homeserverUrl == LoginConstants.MATRIX_ORG_URL
|
||||
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,
|
||||
isPublic = isMatrixOrg, // There is no need to know for other servers right now
|
||||
isMatrixOrg = isMatrixOrg,
|
||||
|
|
|
|||
|
|
@ -19,14 +19,14 @@ package io.element.android.features.login.impl.util
|
|||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
val defaultAccountProvider = AccountProvider(
|
||||
title = LoginConstants.DEFAULT_HOMESERVER_URL,
|
||||
url = LoginConstants.DEFAULT_HOMESERVER_URL,
|
||||
subtitle = null,
|
||||
isPublic = 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"?>
|
||||
<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_signup_subtitle">"您的所有對話將保存於此,就如同您的電子郵件供應商會保存您的電子郵件一樣。"</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_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_login_error_deactivated_account">"這個帳號已被停用。"</string>
|
||||
<string name="screen_login_error_invalid_credentials">"不正確的使用者名稱或密碼"</string>
|
||||
<string name="screen_login_form_header">"輸入您的詳細資料"</string>
|
||||
<string name="screen_login_title">"歡迎回來!"</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_register">"您即將在 %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_login_password_hint">"密碼"</string>
|
||||
<string name="screen_login_submit">"繼續"</string>
|
||||
<string name="screen_login_subtitle">"Matrix 是一個開放網路,為了安全、去中心化的通訊而生。"</string>
|
||||
<string name="screen_login_username_hint">"使用者名稱"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ class ChangeServerPresenterTest {
|
|||
val initialState = awaitItem()
|
||||
assertThat(initialState.changeServerAction).isEqualTo(Async.Uninitialized)
|
||||
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()
|
||||
assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java)
|
||||
val successState = awaitItem()
|
||||
|
|
@ -83,7 +83,7 @@ class ChangeServerPresenterTest {
|
|||
}.test {
|
||||
val initialState = awaitItem()
|
||||
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()
|
||||
assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java)
|
||||
val failureState = awaitItem()
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ class ChangeAccountProviderPresenterTest {
|
|||
assertThat(initialState.accountProviders).isEqualTo(
|
||||
listOf(
|
||||
AccountProvider(
|
||||
url = "https://matrix.org",
|
||||
title = "matrix.org",
|
||||
subtitle = null,
|
||||
isPublic = true,
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ fun LogoutPreferenceView(
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun LogoutPreferenceContent(
|
||||
private fun LogoutPreferenceContent(
|
||||
onClick: () -> Unit = {},
|
||||
) {
|
||||
PreferenceText(
|
||||
|
|
@ -38,7 +38,6 @@ dependencies {
|
|||
implementation(projects.libraries.testtags)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.dateformatter.api)
|
||||
implementation(libs.accompanist.placeholder)
|
||||
api(projects.features.logout.api)
|
||||
ksp(libs.showkase.processor)
|
||||
|
||||
|
|
|
|||