Merge branch 'develop' of https://github.com/vector-im/element-x-android into yostyle/notifications_global_settings
This commit is contained in:
commit
d3d4eedb1a
315 changed files with 3724 additions and 1216 deletions
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
|
@ -38,7 +38,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/gradle-build-action@v2.7.1
|
uses: gradle/gradle-build-action@v2.8.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Assemble debug APK
|
- name: Assemble debug APK
|
||||||
|
|
|
||||||
2
.github/workflows/nightlyReports.yml
vendored
2
.github/workflows/nightlyReports.yml
vendored
|
|
@ -62,7 +62,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/gradle-build-action@v2.7.1
|
uses: gradle/gradle-build-action@v2.8.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Dependency analysis
|
- name: Dependency analysis
|
||||||
|
|
|
||||||
2
.github/workflows/quality.yml
vendored
2
.github/workflows/quality.yml
vendored
|
|
@ -40,7 +40,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/gradle-build-action@v2.7.1
|
uses: gradle/gradle-build-action@v2.8.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Run code quality check suite
|
- name: Run code quality check suite
|
||||||
|
|
|
||||||
2
.github/workflows/recordScreenshots.yml
vendored
2
.github/workflows/recordScreenshots.yml
vendored
|
|
@ -24,7 +24,7 @@ jobs:
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
# Add gradle cache, this should speed up the process
|
# Add gradle cache, this should speed up the process
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/gradle-build-action@v2.7.1
|
uses: gradle/gradle-build-action@v2.8.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Record screenshots
|
- name: Record screenshots
|
||||||
|
|
|
||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -25,7 +25,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/gradle-build-action@v2.7.1
|
uses: gradle/gradle-build-action@v2.8.0
|
||||||
- name: Create app bundle
|
- name: Create app bundle
|
||||||
env:
|
env:
|
||||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||||
|
|
|
||||||
2
.github/workflows/sonar.yml
vendored
2
.github/workflows/sonar.yml
vendored
|
|
@ -32,7 +32,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/gradle-build-action@v2.7.1
|
uses: gradle/gradle-build-action@v2.8.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: 🔊 Publish results to Sonar
|
- name: 🔊 Publish results to Sonar
|
||||||
|
|
|
||||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
|
|
@ -34,7 +34,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/gradle-build-action@v2.7.1
|
uses: gradle/gradle-build-action@v2.8.0
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
|
|
||||||
|
|
|
||||||
41
CHANGES.md
41
CHANGES.md
|
|
@ -1,3 +1,44 @@
|
||||||
|
Changes in Element X v0.1.5 (2023-08-28)
|
||||||
|
========================================
|
||||||
|
|
||||||
|
Bugfixes 🐛
|
||||||
|
----------
|
||||||
|
- Fix crash when opening any room. ([#1160](https://github.com/vector-im/element-x-android/issues/1160))
|
||||||
|
|
||||||
|
|
||||||
|
Changes in Element X v0.1.4 (2023-08-28)
|
||||||
|
========================================
|
||||||
|
|
||||||
|
Features ✨
|
||||||
|
----------
|
||||||
|
- Allow cancelling media upload ([#769](https://github.com/vector-im/element-x-android/issues/769))
|
||||||
|
- Enable OIDC support. ([#1127](https://github.com/vector-im/element-x-android/issues/1127))
|
||||||
|
- Add a "Setting up account" screen, displayed the first time the user logs in to the app (per account). ([#1149](https://github.com/vector-im/element-x-android/issues/1149))
|
||||||
|
|
||||||
|
Bugfixes 🐛
|
||||||
|
----------
|
||||||
|
- Videos sent from the app were cropped in some cases. ([#862](https://github.com/vector-im/element-x-android/issues/862))
|
||||||
|
- Timeline: sender names are now displayed in one single line. ([#1033](https://github.com/vector-im/element-x-android/issues/1033))
|
||||||
|
- Fix `TextButtons` being displayed in black. ([#1077](https://github.com/vector-im/element-x-android/issues/1077))
|
||||||
|
- Linkify links in HTML contents. ([#1079](https://github.com/vector-im/element-x-android/issues/1079))
|
||||||
|
- Fix bug reporter failing after not finding some log files. ([#1082](https://github.com/vector-im/element-x-android/issues/1082))
|
||||||
|
- Fix rendering of inline elements in list items. ([#1090](https://github.com/vector-im/element-x-android/issues/1090))
|
||||||
|
- Fix crash RuntimeException "No matching key found for the ciphertext in the stream" ([#1101](https://github.com/vector-im/element-x-android/issues/1101))
|
||||||
|
- Make links in messages clickable again. ([#1111](https://github.com/vector-im/element-x-android/issues/1111))
|
||||||
|
- When event has no id, just cancel parsing the latest room message for a room. ([#1125](https://github.com/vector-im/element-x-android/issues/1125))
|
||||||
|
- Only display verification prompt after initial sync is done. ([#1131](https://github.com/vector-im/element-x-android/issues/1131))
|
||||||
|
|
||||||
|
In development 🚧
|
||||||
|
----------------
|
||||||
|
- [Poll] Add feature flag in developer options ([#1064](https://github.com/vector-im/element-x-android/issues/1064))
|
||||||
|
- [Polls] Improve UI and render ended state ([#1113](https://github.com/vector-im/element-x-android/issues/1113))
|
||||||
|
|
||||||
|
Other changes
|
||||||
|
-------------
|
||||||
|
- Compound: add `ListItem` and `ListSectionHeader` components. ([#990](https://github.com/vector-im/element-x-android/issues/990))
|
||||||
|
- Migrate `object` to `data object` in sealed interface / class #1135 ([#1135](https://github.com/vector-im/element-x-android/issues/1135))
|
||||||
|
|
||||||
|
|
||||||
Changes in Element X v0.1.2 (2023-08-16)
|
Changes in Element X v0.1.2 (2023-08-16)
|
||||||
========================================
|
========================================
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ If after your research you still have a question, ask at [#element-x-android:mat
|
||||||
|
|
||||||
## Copyright & License
|
## Copyright & License
|
||||||
|
|
||||||
Copyright (c) 2022 New Vector Ltd
|
Copyright © New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with the License. You may obtain a copy of the License in the [LICENSE](LICENSE) file, or at:
|
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with the License. You may obtain a copy of the License in the [LICENSE](LICENSE) file, or at:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,9 @@ import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.node.ParentNode
|
import com.bumble.appyx.core.node.ParentNode
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import io.element.android.appnav.LoggedInFlowNode
|
import io.element.android.appnav.LoggedInAppScopeFlowNode
|
||||||
import io.element.android.appnav.room.RoomLoadedFlowNode
|
|
||||||
import io.element.android.appnav.RootFlowNode
|
import io.element.android.appnav.RootFlowNode
|
||||||
|
import io.element.android.appnav.room.RoomLoadedFlowNode
|
||||||
import io.element.android.libraries.architecture.bindings
|
import io.element.android.libraries.architecture.bindings
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.di.DaggerComponentOwner
|
import io.element.android.libraries.di.DaggerComponentOwner
|
||||||
|
|
@ -45,18 +45,17 @@ class MainNode(
|
||||||
buildContext: BuildContext,
|
buildContext: BuildContext,
|
||||||
private val mainDaggerComponentOwner: MainDaggerComponentsOwner,
|
private val mainDaggerComponentOwner: MainDaggerComponentsOwner,
|
||||||
plugins: List<Plugin>,
|
plugins: List<Plugin>,
|
||||||
) :
|
) : ParentNode<MainNode.RootNavTarget>(
|
||||||
ParentNode<MainNode.RootNavTarget>(
|
navModel = PermanentNavModel(
|
||||||
navModel = PermanentNavModel(
|
navTargets = setOf(RootNavTarget),
|
||||||
navTargets = setOf(RootNavTarget),
|
savedStateMap = buildContext.savedStateMap,
|
||||||
savedStateMap = buildContext.savedStateMap,
|
|
||||||
),
|
|
||||||
buildContext = buildContext,
|
|
||||||
plugins = plugins,
|
|
||||||
),
|
),
|
||||||
|
buildContext = buildContext,
|
||||||
|
plugins = plugins,
|
||||||
|
),
|
||||||
DaggerComponentOwner by mainDaggerComponentOwner {
|
DaggerComponentOwner by mainDaggerComponentOwner {
|
||||||
|
|
||||||
private val loggedInFlowNodeCallback = object : LoggedInFlowNode.LifecycleCallback {
|
private val loggedInFlowNodeCallback = object : LoggedInAppScopeFlowNode.LifecycleCallback {
|
||||||
override fun onFlowCreated(identifier: String, client: MatrixClient) {
|
override fun onFlowCreated(identifier: String, client: MatrixClient) {
|
||||||
val component = bindings<SessionComponent.ParentBindings>().sessionComponentBuilder().client(client).build()
|
val component = bindings<SessionComponent.ParentBindings>().sessionComponentBuilder().client(client).build()
|
||||||
mainDaggerComponentOwner.addComponent(identifier, component)
|
mainDaggerComponentOwner.addComponent(identifier, component)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.appnav
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import coil.Coil
|
||||||
|
import com.bumble.appyx.core.composable.Children
|
||||||
|
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.node.ParentNode
|
||||||
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
import com.bumble.appyx.core.plugin.plugins
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
|
import io.element.android.libraries.architecture.bindings
|
||||||
|
import io.element.android.libraries.architecture.createNode
|
||||||
|
import io.element.android.libraries.architecture.inputs
|
||||||
|
import io.element.android.libraries.architecture.waitForChildAttached
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.ui.di.MatrixUIBindings
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `LoggedInAppScopeFlowNode` is a Node responsible to set up the Dagger
|
||||||
|
* [io.element.android.libraries.di.SessionScope]. It has only one child: [LoggedInFlowNode].
|
||||||
|
* This allow to inject objects with SessionScope in the constructor of [LoggedInFlowNode].
|
||||||
|
*/
|
||||||
|
@ContributesNode(AppScope::class)
|
||||||
|
class LoggedInAppScopeFlowNode @AssistedInject constructor(
|
||||||
|
@Assisted buildContext: BuildContext,
|
||||||
|
@Assisted plugins: List<Plugin>,
|
||||||
|
) : ParentNode<LoggedInAppScopeFlowNode.NavTarget>(
|
||||||
|
navModel = PermanentNavModel(
|
||||||
|
navTargets = setOf(NavTarget),
|
||||||
|
savedStateMap = buildContext.savedStateMap,
|
||||||
|
),
|
||||||
|
buildContext = buildContext,
|
||||||
|
plugins = plugins
|
||||||
|
) {
|
||||||
|
interface Callback : Plugin {
|
||||||
|
fun onOpenBugReport()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
object NavTarget : Parcelable
|
||||||
|
|
||||||
|
interface LifecycleCallback : NodeLifecycleCallback {
|
||||||
|
fun onFlowCreated(identifier: String, client: MatrixClient)
|
||||||
|
|
||||||
|
fun onFlowReleased(identifier: String, client: MatrixClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Inputs(
|
||||||
|
val matrixClient: MatrixClient
|
||||||
|
) : NodeInputs
|
||||||
|
|
||||||
|
private val inputs: Inputs = inputs()
|
||||||
|
|
||||||
|
override fun onBuilt() {
|
||||||
|
super.onBuilt()
|
||||||
|
lifecycle.subscribe(
|
||||||
|
onCreate = {
|
||||||
|
plugins<LifecycleCallback>().forEach { it.onFlowCreated(id, inputs.matrixClient) }
|
||||||
|
val imageLoaderFactory = bindings<MatrixUIBindings>().loggedInImageLoaderFactory()
|
||||||
|
Coil.setImageLoader(imageLoaderFactory)
|
||||||
|
},
|
||||||
|
onDestroy = {
|
||||||
|
plugins<LifecycleCallback>().forEach { it.onFlowReleased(id, inputs.matrixClient) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
val callback = object : LoggedInFlowNode.Callback {
|
||||||
|
override fun onOpenBugReport() {
|
||||||
|
plugins<Callback>().forEach { it.onOpenBugReport() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val nodeLifecycleCallbacks = plugins<NodeLifecycleCallback>()
|
||||||
|
return createNode<LoggedInFlowNode>(buildContext, nodeLifecycleCallbacks + callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun attachSession(): LoggedInFlowNode {
|
||||||
|
return waitForChildAttached { _ -> true }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
Children(
|
||||||
|
navModel = navModel,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -25,7 +25,6 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import coil.Coil
|
|
||||||
import com.bumble.appyx.core.composable.Children
|
import com.bumble.appyx.core.composable.Children
|
||||||
import com.bumble.appyx.core.lifecycle.subscribe
|
import com.bumble.appyx.core.lifecycle.subscribe
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
|
@ -53,19 +52,15 @@ import io.element.android.features.preferences.api.PreferencesEntryPoint
|
||||||
import io.element.android.features.roomlist.api.RoomListEntryPoint
|
import io.element.android.features.roomlist.api.RoomListEntryPoint
|
||||||
import io.element.android.features.verifysession.api.VerifySessionEntryPoint
|
import io.element.android.features.verifysession.api.VerifySessionEntryPoint
|
||||||
import io.element.android.libraries.architecture.BackstackNode
|
import io.element.android.libraries.architecture.BackstackNode
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
|
||||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||||
import io.element.android.libraries.architecture.bindings
|
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.architecture.inputs
|
|
||||||
import io.element.android.libraries.deeplink.DeeplinkData
|
import io.element.android.libraries.deeplink.DeeplinkData
|
||||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
|
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||||
import io.element.android.libraries.matrix.ui.di.MatrixUIBindings
|
|
||||||
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
||||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -76,7 +71,7 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@ContributesNode(AppScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
class LoggedInFlowNode @AssistedInject constructor(
|
class LoggedInFlowNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
|
@ -91,6 +86,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
private val notificationDrawerManager: NotificationDrawerManager,
|
private val notificationDrawerManager: NotificationDrawerManager,
|
||||||
private val ftueState: FtueState,
|
private val ftueState: FtueState,
|
||||||
|
private val matrixClient: MatrixClient,
|
||||||
snackbarDispatcher: SnackbarDispatcher,
|
snackbarDispatcher: SnackbarDispatcher,
|
||||||
) : BackstackNode<LoggedInFlowNode.NavTarget>(
|
) : BackstackNode<LoggedInFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
|
|
@ -105,32 +101,18 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
fun onOpenBugReport()
|
fun onOpenBugReport()
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LifecycleCallback : NodeLifecycleCallback {
|
private val syncService = matrixClient.syncService()
|
||||||
fun onFlowCreated(identifier: String, client: MatrixClient)
|
|
||||||
|
|
||||||
fun onFlowReleased(identifier: String, client: MatrixClient)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Inputs(
|
|
||||||
val matrixClient: MatrixClient
|
|
||||||
) : NodeInputs
|
|
||||||
|
|
||||||
private val inputs: Inputs = inputs()
|
|
||||||
private val syncService = inputs.matrixClient.syncService()
|
|
||||||
private val loggedInFlowProcessor = LoggedInEventProcessor(
|
private val loggedInFlowProcessor = LoggedInEventProcessor(
|
||||||
snackbarDispatcher,
|
snackbarDispatcher,
|
||||||
inputs.matrixClient.roomMembershipObserver(),
|
matrixClient.roomMembershipObserver(),
|
||||||
inputs.matrixClient.sessionVerificationService(),
|
matrixClient.sessionVerificationService(),
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
super.onBuilt()
|
super.onBuilt()
|
||||||
lifecycle.subscribe(
|
lifecycle.subscribe(
|
||||||
onCreate = {
|
onCreate = {
|
||||||
plugins<LifecycleCallback>().forEach { it.onFlowCreated(id, inputs.matrixClient) }
|
appNavigationStateService.onNavigateToSession(id, matrixClient.sessionId)
|
||||||
val imageLoaderFactory = bindings<MatrixUIBindings>().loggedInImageLoaderFactory()
|
|
||||||
Coil.setImageLoader(imageLoaderFactory)
|
|
||||||
appNavigationStateService.onNavigateToSession(id, inputs.matrixClient.sessionId)
|
|
||||||
// TODO We do not support Space yet, so directly navigate to main space
|
// TODO We do not support Space yet, so directly navigate to main space
|
||||||
appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE)
|
appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE)
|
||||||
loggedInFlowProcessor.observeEvents(coroutineScope)
|
loggedInFlowProcessor.observeEvents(coroutineScope)
|
||||||
|
|
@ -146,7 +128,6 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDestroy = {
|
onDestroy = {
|
||||||
plugins<LifecycleCallback>().forEach { it.onFlowReleased(id, inputs.matrixClient) }
|
|
||||||
appNavigationStateService.onLeavingSpace(id)
|
appNavigationStateService.onLeavingSpace(id)
|
||||||
appNavigationStateService.onLeavingSession(id)
|
appNavigationStateService.onLeavingSession(id)
|
||||||
loggedInFlowProcessor.stopObserving()
|
loggedInFlowProcessor.stopObserving()
|
||||||
|
|
@ -178,10 +159,10 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object Permanent : NavTarget
|
data object Permanent : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object RoomList : NavTarget
|
data object RoomList : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Room(
|
data class Room(
|
||||||
|
|
@ -190,19 +171,19 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
) : NavTarget
|
) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object Settings : NavTarget
|
data object Settings : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object CreateRoom : NavTarget
|
data object CreateRoom : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object VerifySession : NavTarget
|
data object VerifySession : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object InviteList : NavTarget
|
data object InviteList : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object Ftue : NavTarget
|
data object Ftue : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
|
@ -351,4 +332,3 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
backstack.push(NavTarget.InviteList)
|
backstack.push(NavTarget.InviteList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ class NotLoggedInFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object OnBoarding : NavTarget
|
data object OnBoarding : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class LoginFlow(
|
data class LoginFlow(
|
||||||
|
|
|
||||||
|
|
@ -72,15 +72,14 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
private val bugReportEntryPoint: BugReportEntryPoint,
|
private val bugReportEntryPoint: BugReportEntryPoint,
|
||||||
private val intentResolver: IntentResolver,
|
private val intentResolver: IntentResolver,
|
||||||
private val oidcActionFlow: OidcActionFlow,
|
private val oidcActionFlow: OidcActionFlow,
|
||||||
) :
|
) : BackstackNode<RootFlowNode.NavTarget>(
|
||||||
BackstackNode<RootFlowNode.NavTarget>(
|
backstack = BackStack(
|
||||||
backstack = BackStack(
|
initialElement = NavTarget.SplashScreen,
|
||||||
initialElement = NavTarget.SplashScreen,
|
savedStateMap = buildContext.savedStateMap,
|
||||||
savedStateMap = buildContext.savedStateMap,
|
),
|
||||||
),
|
buildContext = buildContext,
|
||||||
buildContext = buildContext,
|
plugins = plugins
|
||||||
plugins = plugins
|
) {
|
||||||
) {
|
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
matrixClientsHolder.restoreWithSavedState(buildContext.savedStateMap)
|
matrixClientsHolder.restoreWithSavedState(buildContext.savedStateMap)
|
||||||
|
|
@ -170,10 +169,10 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object SplashScreen : NavTarget
|
data object SplashScreen : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object NotLoggedInFlow : NavTarget
|
data object NotLoggedInFlow : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class LoggedInFlow(
|
data class LoggedInFlow(
|
||||||
|
|
@ -182,7 +181,7 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
) : NavTarget
|
) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object BugReport : NavTarget
|
data object BugReport : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
|
@ -191,14 +190,14 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
val matrixClient = matrixClientsHolder.getOrNull(navTarget.sessionId) ?: return splashNode(buildContext).also {
|
val matrixClient = matrixClientsHolder.getOrNull(navTarget.sessionId) ?: return splashNode(buildContext).also {
|
||||||
Timber.w("Couldn't find any session, go through SplashScreen")
|
Timber.w("Couldn't find any session, go through SplashScreen")
|
||||||
}
|
}
|
||||||
val inputs = LoggedInFlowNode.Inputs(matrixClient)
|
val inputs = LoggedInAppScopeFlowNode.Inputs(matrixClient)
|
||||||
val callback = object : LoggedInFlowNode.Callback {
|
val callback = object : LoggedInAppScopeFlowNode.Callback {
|
||||||
override fun onOpenBugReport() {
|
override fun onOpenBugReport() {
|
||||||
backstack.push(NavTarget.BugReport)
|
backstack.push(NavTarget.BugReport)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val nodeLifecycleCallbacks = plugins<NodeLifecycleCallback>()
|
val nodeLifecycleCallbacks = plugins<NodeLifecycleCallback>()
|
||||||
createNode<LoggedInFlowNode>(buildContext, plugins = listOf(inputs, callback) + nodeLifecycleCallbacks)
|
createNode<LoggedInAppScopeFlowNode>(buildContext, plugins = listOf(inputs, callback) + nodeLifecycleCallbacks)
|
||||||
}
|
}
|
||||||
NavTarget.NotLoggedInFlow -> createNode<NotLoggedInFlowNode>(buildContext)
|
NavTarget.NotLoggedInFlow -> createNode<NotLoggedInFlowNode>(buildContext)
|
||||||
NavTarget.SplashScreen -> splashNode(buildContext)
|
NavTarget.SplashScreen -> splashNode(buildContext)
|
||||||
|
|
@ -233,6 +232,7 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
private suspend fun navigateTo(deeplinkData: DeeplinkData) {
|
private suspend fun navigateTo(deeplinkData: DeeplinkData) {
|
||||||
Timber.d("Navigating to $deeplinkData")
|
Timber.d("Navigating to $deeplinkData")
|
||||||
attachSession(deeplinkData.sessionId)
|
attachSession(deeplinkData.sessionId)
|
||||||
|
.attachSession()
|
||||||
.apply {
|
.apply {
|
||||||
when (deeplinkData) {
|
when (deeplinkData) {
|
||||||
is DeeplinkData.Root -> attachRoot()
|
is DeeplinkData.Root -> attachRoot()
|
||||||
|
|
@ -246,7 +246,7 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
oidcActionFlow.post(oidcAction)
|
oidcActionFlow.post(oidcAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun attachSession(sessionId: SessionId): LoggedInFlowNode {
|
private suspend fun attachSession(sessionId: SessionId): LoggedInAppScopeFlowNode {
|
||||||
//TODO handle multi-session
|
//TODO handle multi-session
|
||||||
return waitForChildAttached { navTarget ->
|
return waitForChildAttached { navTarget ->
|
||||||
navTarget is NavTarget.LoggedInFlow && navTarget.sessionId == sessionId
|
navTarget is NavTarget.LoggedInFlow && navTarget.sessionId == sessionId
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,5 @@
|
||||||
package io.element.android.appnav.loggedin
|
package io.element.android.appnav.loggedin
|
||||||
|
|
||||||
// sealed interface LoggedInEvents {
|
// sealed interface LoggedInEvents {
|
||||||
// object MyEvent : LoggedInEvents
|
// data object MyEvent : LoggedInEvents
|
||||||
// }
|
// }
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,8 @@ import kotlinx.coroutines.flow.stateIn
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
sealed interface LoadingRoomState {
|
sealed interface LoadingRoomState {
|
||||||
object Loading : LoadingRoomState
|
data object Loading : LoadingRoomState
|
||||||
object Error : LoadingRoomState
|
data object Error : LoadingRoomState
|
||||||
data class Loaded(val room: MatrixRoom) : LoadingRoomState
|
data class Loaded(val room: MatrixRoom) : LoadingRoomState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,10 +77,10 @@ class RoomFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object Loading : NavTarget
|
data object Loading : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object Loaded : NavTarget
|
data object Loaded : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
|
|
|
||||||
|
|
@ -152,10 +152,10 @@ class RoomLoadedFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object Messages : NavTarget
|
data object Messages : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object RoomDetails : NavTarget
|
data object RoomDetails : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class RoomMemberDetails(val userId: UserId) : NavTarget
|
data class RoomMemberDetails(val userId: UserId) : NavTarget
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import org.jetbrains.kotlin.cli.common.toBooleanLenient
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0")
|
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10")
|
||||||
classpath("com.google.gms:google-services:4.3.15")
|
classpath("com.google.gms:google-services:4.3.15")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Timeline: sender names are now displayed in one single line.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
[Poll] Add feature flag in developer options
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Fix `TextButtons` being displayed in black.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Linkify links in HTML contents.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Fix bug reporter failing after not finding some log files.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Fix rendering of inline elements in list items.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Fix crash RuntimeException "No matching key found for the ciphertext in the stream"
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Make links in messages clickable again.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
When event has no id, just cancel parsing the latest room message for a room.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Enable OIDC support.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Only display verification prompt after initial sync is done.
|
|
||||||
1
changelog.d/1143.feature
Normal file
1
changelog.d/1143.feature
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Create poll.
|
||||||
1
changelog.d/1168.bugfix
Normal file
1
changelog.d/1168.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Bug reporter crashes when 'send logs' is disabled.
|
||||||
1
changelog.d/1177.bugfix
Normal file
1
changelog.d/1177.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Add missing link to the terms on the analytics setting screen.
|
||||||
1
changelog.d/1187.misc
Normal file
1
changelog.d/1187.misc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Remove unnecessary year in copyright mention.
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Allow cancelling media upload
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Videos sent from the app were cropped in some cases.
|
|
||||||
1
changelog.d/928.bugfix
Normal file
1
changelog.d/928.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Make sure Snackbars are only displayed once.
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Compound: add `ListItem` and `ListSectionHeader` components.
|
|
||||||
2
fastlane/metadata/android/en-US/changelogs/40001040.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40001040.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
Main changes in this version: bug fixes and add OIDC support.
|
||||||
|
Full changelog: https://github.com/vector-im/element-x-android/releases
|
||||||
2
fastlane/metadata/android/en-US/changelogs/40001050.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40001050.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
Main changes in this version: bug fixes and add OIDC support.
|
||||||
|
Full changelog: https://github.com/vector-im/element-x-android/releases
|
||||||
|
|
@ -21,5 +21,6 @@ import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
||||||
data class AnalyticsPreferencesState(
|
data class AnalyticsPreferencesState(
|
||||||
val applicationName: String,
|
val applicationName: String,
|
||||||
val isEnabled: Boolean,
|
val isEnabled: Boolean,
|
||||||
|
val policyUrl: String,
|
||||||
val eventSink: (AnalyticsOptInEvents) -> Unit,
|
val eventSink: (AnalyticsOptInEvents) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -28,5 +28,6 @@ open class AnalyticsPreferencesStateProvider : PreviewParameterProvider<Analytic
|
||||||
fun aAnalyticsPreferencesState() = AnalyticsPreferencesState(
|
fun aAnalyticsPreferencesState() = AnalyticsPreferencesState(
|
||||||
applicationName = "Element X",
|
applicationName = "Element X",
|
||||||
isEnabled = false,
|
isEnabled = false,
|
||||||
|
policyUrl = "https://element.io",
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -16,22 +16,21 @@
|
||||||
|
|
||||||
package io.element.android.features.analytics.api.preferences
|
package io.element.android.features.analytics.api.preferences
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.SpanStyle
|
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
||||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
|
import io.element.android.libraries.designsystem.components.LINK_TAG
|
||||||
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.theme.LinkColor
|
import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ListSupportingText
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -43,40 +42,33 @@ fun AnalyticsPreferencesView(
|
||||||
state.eventSink(AnalyticsOptInEvents.EnableAnalytics(isEnabled = isEnabled))
|
state.eventSink(AnalyticsOptInEvents.EnableAnalytics(isEnabled = isEnabled))
|
||||||
}
|
}
|
||||||
|
|
||||||
val firstPart = stringResource(id = CommonStrings.screen_analytics_settings_help_us_improve, state.applicationName)
|
val supportingText = stringResource(
|
||||||
val secondPart = buildAnnotatedStringWithColoredPart(
|
id = CommonStrings.screen_analytics_settings_help_us_improve,
|
||||||
|
state.applicationName
|
||||||
|
)
|
||||||
|
val linkText = buildAnnotatedStringWithStyledPart(
|
||||||
CommonStrings.screen_analytics_settings_read_terms,
|
CommonStrings.screen_analytics_settings_read_terms,
|
||||||
CommonStrings.screen_analytics_settings_read_terms_content_link
|
CommonStrings.screen_analytics_settings_read_terms_content_link,
|
||||||
)
|
tagAndLink = LINK_TAG to state.policyUrl,
|
||||||
val subtitle = "$firstPart\n\n$secondPart"
|
|
||||||
|
|
||||||
PreferenceSwitch(
|
|
||||||
modifier = modifier,
|
|
||||||
title = stringResource(id = CommonStrings.screen_analytics_settings_share_data),
|
|
||||||
subtitle = subtitle,
|
|
||||||
isChecked = state.isEnabled,
|
|
||||||
onCheckedChange = ::onEnabledChanged,
|
|
||||||
switchAlignment = Alignment.Top,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun buildAnnotatedStringWithColoredPart(
|
|
||||||
@StringRes fullTextRes: Int,
|
|
||||||
@StringRes coloredTextRes: Int,
|
|
||||||
color: Color = LinkColor,
|
|
||||||
underline: Boolean = true,
|
|
||||||
) = buildAnnotatedString {
|
|
||||||
val coloredPart = stringResource(coloredTextRes)
|
|
||||||
val fullText = stringResource(fullTextRes, coloredPart)
|
|
||||||
val startIndex = fullText.indexOf(coloredPart)
|
|
||||||
append(fullText)
|
|
||||||
addStyle(
|
|
||||||
style = SpanStyle(
|
|
||||||
color = color,
|
|
||||||
textDecoration = if (underline) TextDecoration.Underline else null
|
|
||||||
), start = startIndex, end = startIndex + coloredPart.length
|
|
||||||
)
|
)
|
||||||
|
Column(modifier) {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(stringResource(id = CommonStrings.screen_analytics_settings_share_data))
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Text(supportingText)
|
||||||
|
},
|
||||||
|
leadingContent = null,
|
||||||
|
trailingContent = ListItemContent.Switch(
|
||||||
|
checked = state.isEnabled,
|
||||||
|
),
|
||||||
|
onClick = {
|
||||||
|
onEnabledChanged(!state.isEnabled)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ListSupportingText(annotatedString = linkText)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
|
|
@ -91,5 +83,7 @@ internal fun AnalyticsPreferencesViewDarkPreview(@PreviewParameter(AnalyticsPref
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ContentToPreview(state: AnalyticsPreferencesState) {
|
private fun ContentToPreview(state: AnalyticsPreferencesState) {
|
||||||
AnalyticsPreferencesView(state)
|
AnalyticsPreferencesView(
|
||||||
|
state = state,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,5 @@ dependencies {
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(libs.test.mockk)
|
testImplementation(libs.test.mockk)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.features.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.features.analytics.impl)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ package io.element.android.features.analytics.impl
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
|
@ -28,7 +27,7 @@ import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.systemBarsPadding
|
import androidx.compose.foundation.layout.systemBarsPadding
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.text.ClickableText
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Poll
|
import androidx.compose.material.icons.filled.Poll
|
||||||
import androidx.compose.material.icons.rounded.Check
|
import androidx.compose.material.icons.rounded.Check
|
||||||
|
|
@ -37,7 +36,6 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.BiasAlignment
|
import androidx.compose.ui.BiasAlignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
|
@ -45,6 +43,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
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.ButtonColumnMolecule
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.InfoListItem
|
import io.element.android.libraries.designsystem.atomic.molecules.InfoListItem
|
||||||
|
|
@ -56,7 +55,6 @@ import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithSt
|
||||||
import io.element.android.libraries.designsystem.theme.components.Button
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
import io.element.android.libraries.designsystem.theme.components.ButtonSize
|
import io.element.android.libraries.designsystem.theme.components.ButtonSize
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.designsystem.theme.temporaryColorBgSpecial
|
import io.element.android.libraries.designsystem.theme.temporaryColorBgSpecial
|
||||||
import io.element.android.libraries.designsystem.utils.LogCompositions
|
import io.element.android.libraries.designsystem.utils.LogCompositions
|
||||||
|
|
@ -98,6 +96,8 @@ fun AnalyticsOptInView(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val LINK_TAG = "link"
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AnalyticsOptInHeader(
|
private fun AnalyticsOptInHeader(
|
||||||
state: AnalyticsOptInState,
|
state: AnalyticsOptInState,
|
||||||
|
|
@ -114,21 +114,29 @@ private fun AnalyticsOptInHeader(
|
||||||
subTitle = stringResource(id = R.string.screen_analytics_prompt_help_us_improve),
|
subTitle = stringResource(id = R.string.screen_analytics_prompt_help_us_improve),
|
||||||
iconImageVector = Icons.Filled.Poll
|
iconImageVector = Icons.Filled.Poll
|
||||||
)
|
)
|
||||||
Text(
|
val text = buildAnnotatedStringWithStyledPart(
|
||||||
text = buildAnnotatedStringWithStyledPart(
|
R.string.screen_analytics_prompt_read_terms,
|
||||||
R.string.screen_analytics_prompt_read_terms,
|
R.string.screen_analytics_prompt_read_terms_content_link,
|
||||||
R.string.screen_analytics_prompt_read_terms_content_link,
|
color = Color.Unspecified,
|
||||||
color = Color.Unspecified,
|
underline = false,
|
||||||
underline = false,
|
bold = true,
|
||||||
bold = true,
|
tagAndLink = LINK_TAG to Config.POLICY_LINK,
|
||||||
),
|
)
|
||||||
|
ClickableText(
|
||||||
|
text = text,
|
||||||
|
onClick = {
|
||||||
|
text
|
||||||
|
.getStringAnnotations(LINK_TAG, it, it)
|
||||||
|
.firstOrNull()
|
||||||
|
?.let { _ -> onClickTerms() }
|
||||||
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(shape = RoundedCornerShape(8.dp))
|
|
||||||
.clickable { onClickTerms() }
|
|
||||||
.padding(8.dp),
|
.padding(8.dp),
|
||||||
style = ElementTheme.typography.fontBodyMdRegular,
|
style = ElementTheme.typography.fontBodyMdRegular
|
||||||
textAlign = TextAlign.Center,
|
.copy(
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
||||||
|
import io.element.android.features.analytics.api.Config
|
||||||
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesPresenter
|
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesPresenter
|
||||||
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState
|
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
|
|
@ -51,6 +52,7 @@ class DefaultAnalyticsPreferencesPresenter @Inject constructor(
|
||||||
return AnalyticsPreferencesState(
|
return AnalyticsPreferencesState(
|
||||||
applicationName = buildMeta.applicationName,
|
applicationName = buildMeta.applicationName,
|
||||||
isEnabled = isEnabled.value,
|
isEnabled = isEnabled.value,
|
||||||
|
policyUrl = Config.POLICY_LINK,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,6 @@
|
||||||
<string name="screen_analytics_prompt_read_terms">"Du kannst alle unsere Nutzerbedingungen %1$s lesen."</string>
|
<string name="screen_analytics_prompt_read_terms">"Du kannst alle unsere Nutzerbedingungen %1$s lesen."</string>
|
||||||
<string name="screen_analytics_prompt_read_terms_content_link">"hier"</string>
|
<string name="screen_analytics_prompt_read_terms_content_link">"hier"</string>
|
||||||
<string name="screen_analytics_prompt_settings">"Du kannst dies jederzeit deaktivieren"</string>
|
<string name="screen_analytics_prompt_settings">"Du kannst dies jederzeit deaktivieren"</string>
|
||||||
<string name="screen_analytics_prompt_third_party_sharing">"Wir geben "<b>"keine"</b>" Informationen an Dritte weiter"</string>
|
<string name="screen_analytics_prompt_third_party_sharing">"Wir geben deine Daten nicht an Dritte weiter"</string>
|
||||||
<string name="screen_analytics_prompt_title">"Hilf uns, %1$s zu verbessern"</string>
|
<string name="screen_analytics_prompt_title">"Hilf uns, %1$s zu verbessern"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
||||||
import io.element.android.features.analytics.test.FakeAnalyticsService
|
|
||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
||||||
import io.element.android.features.analytics.test.FakeAnalyticsService
|
|
||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
@ -39,6 +39,7 @@ class AnalyticsPreferencesPresenterTest {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.isEnabled).isTrue()
|
assertThat(initialState.isEnabled).isTrue()
|
||||||
|
assertThat(initialState.policyUrl).isNotEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ dependencies {
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(libs.test.robolectric)
|
testImplementation(libs.test.robolectric)
|
||||||
testImplementation(projects.features.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.libraries.mediapickers.test)
|
testImplementation(projects.libraries.mediapickers.test)
|
||||||
testImplementation(projects.libraries.mediaupload.test)
|
testImplementation(projects.libraries.mediaupload.test)
|
||||||
|
|
|
||||||
|
|
@ -63,10 +63,10 @@ class ConfigureRoomFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object Root : NavTarget
|
data object Root : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object ConfigureRoom : NavTarget
|
data object ConfigureRoom : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
|
|
||||||
|
|
@ -54,10 +54,10 @@ class CreateRoomFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object Root : NavTarget
|
data object Root : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object NewRoom : NavTarget
|
data object NewRoom : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
|
|
||||||
|
|
@ -27,5 +27,5 @@ sealed interface ConfigureRoomEvents {
|
||||||
data class RemoveFromSelection(val matrixUser: MatrixUser) : ConfigureRoomEvents
|
data class RemoveFromSelection(val matrixUser: MatrixUser) : ConfigureRoomEvents
|
||||||
data class CreateRoom(val config: CreateRoomConfig) : ConfigureRoomEvents
|
data class CreateRoom(val config: CreateRoomConfig) : ConfigureRoomEvents
|
||||||
data class HandleAvatarAction(val action: AvatarAction) : ConfigureRoomEvents
|
data class HandleAvatarAction(val action: AvatarAction) : ConfigureRoomEvents
|
||||||
object CancelCreateRoom : ConfigureRoomEvents
|
data object CancelCreateRoom : ConfigureRoomEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,5 +20,5 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
|
||||||
sealed interface CreateRoomRootEvents {
|
sealed interface CreateRoomRootEvents {
|
||||||
data class StartDM(val matrixUser: MatrixUser) : CreateRoomRootEvents
|
data class StartDM(val matrixUser: MatrixUser) : CreateRoomRootEvents
|
||||||
object CancelStartDM : CreateRoomRootEvents
|
data object CancelStartDM : CreateRoomRootEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import im.vector.app.features.analytics.plan.CreatedRoom
|
import im.vector.app.features.analytics.plan.CreatedRoom
|
||||||
import io.element.android.features.analytics.test.FakeAnalyticsService
|
|
||||||
import io.element.android.features.createroom.impl.CreateRoomConfig
|
import io.element.android.features.createroom.impl.CreateRoomConfig
|
||||||
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
||||||
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
||||||
|
|
@ -38,6 +37,7 @@ import io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||||
import io.element.android.libraries.mediapickers.test.FakePickerProvider
|
import io.element.android.libraries.mediapickers.test.FakePickerProvider
|
||||||
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||||
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.mockkStatic
|
import io.mockk.mockkStatic
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import im.vector.app.features.analytics.plan.CreatedRoom
|
import im.vector.app.features.analytics.plan.CreatedRoom
|
||||||
import io.element.android.features.analytics.test.FakeAnalyticsService
|
|
||||||
import io.element.android.features.createroom.impl.userlist.FakeUserListPresenter
|
import io.element.android.features.createroom.impl.userlist.FakeUserListPresenter
|
||||||
import io.element.android.features.createroom.impl.userlist.FakeUserListPresenterFactory
|
import io.element.android.features.createroom.impl.userlist.FakeUserListPresenterFactory
|
||||||
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
||||||
|
|
@ -35,6 +34,7 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.usersearch.test.FakeUserRepository
|
import io.element.android.libraries.usersearch.test.FakeUserRepository
|
||||||
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ dependencies {
|
||||||
implementation(projects.anvilannotations)
|
implementation(projects.anvilannotations)
|
||||||
anvil(projects.anvilcodegen)
|
anvil(projects.anvilcodegen)
|
||||||
api(projects.features.ftue.api)
|
api(projects.features.ftue.api)
|
||||||
|
implementation(projects.libraries.androidutils)
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
|
|
@ -49,7 +50,7 @@ dependencies {
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.features.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
|
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.features.analytics.api.AnalyticsEntryPoint
|
import io.element.android.features.analytics.api.AnalyticsEntryPoint
|
||||||
import io.element.android.features.ftue.api.FtueEntryPoint
|
import io.element.android.features.ftue.api.FtueEntryPoint
|
||||||
|
import io.element.android.features.ftue.impl.migration.MigrationScreenNode
|
||||||
import io.element.android.features.ftue.impl.state.DefaultFtueState
|
import io.element.android.features.ftue.impl.state.DefaultFtueState
|
||||||
import io.element.android.features.ftue.impl.state.FtueStep
|
import io.element.android.features.ftue.impl.state.FtueStep
|
||||||
import io.element.android.features.ftue.impl.welcome.WelcomeNode
|
import io.element.android.features.ftue.impl.welcome.WelcomeNode
|
||||||
|
|
@ -41,6 +42,7 @@ import io.element.android.libraries.architecture.BackstackNode
|
||||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
@ -50,7 +52,7 @@ import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@ContributesNode(AppScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
class FtueFlowNode @AssistedInject constructor(
|
class FtueFlowNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
|
@ -69,13 +71,16 @@ class FtueFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object Placeholder : NavTarget
|
data object Placeholder : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object WelcomeScreen : NavTarget
|
data object MigrationScreen : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object AnalyticsOptIn : NavTarget
|
data object WelcomeScreen : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object AnalyticsOptIn : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
private val callback = plugins.filterIsInstance<FtueEntryPoint.Callback>().firstOrNull()
|
private val callback = plugins.filterIsInstance<FtueEntryPoint.Callback>().firstOrNull()
|
||||||
|
|
@ -102,6 +107,14 @@ class FtueFlowNode @AssistedInject constructor(
|
||||||
NavTarget.Placeholder -> {
|
NavTarget.Placeholder -> {
|
||||||
createNode<PlaceholderNode>(buildContext)
|
createNode<PlaceholderNode>(buildContext)
|
||||||
}
|
}
|
||||||
|
NavTarget.MigrationScreen -> {
|
||||||
|
val callback = object : MigrationScreenNode.Callback {
|
||||||
|
override fun onMigrationFinished() {
|
||||||
|
lifecycleScope.launch { moveToNextStep() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createNode<MigrationScreenNode>(buildContext, listOf(callback))
|
||||||
|
}
|
||||||
NavTarget.WelcomeScreen -> {
|
NavTarget.WelcomeScreen -> {
|
||||||
val callback = object : WelcomeNode.Callback {
|
val callback = object : WelcomeNode.Callback {
|
||||||
override fun onContinueClicked() {
|
override fun onContinueClicked() {
|
||||||
|
|
@ -117,12 +130,15 @@ class FtueFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun moveToNextStep() {
|
private fun moveToNextStep() {
|
||||||
when (ftueState.getNextStep()) {
|
when (ftueState.getNextStep()) {
|
||||||
is FtueStep.WelcomeScreen -> {
|
FtueStep.MigrationScreen -> {
|
||||||
|
backstack.newRoot(NavTarget.MigrationScreen)
|
||||||
|
}
|
||||||
|
FtueStep.WelcomeScreen -> {
|
||||||
backstack.newRoot(NavTarget.WelcomeScreen)
|
backstack.newRoot(NavTarget.WelcomeScreen)
|
||||||
}
|
}
|
||||||
is FtueStep.AnalyticsOptIn -> {
|
FtueStep.AnalyticsOptIn -> {
|
||||||
backstack.replace(NavTarget.AnalyticsOptIn)
|
backstack.replace(NavTarget.AnalyticsOptIn)
|
||||||
}
|
}
|
||||||
null -> callback?.onFtueFlowFinished()
|
null -> callback?.onFtueFlowFinished()
|
||||||
|
|
|
||||||
|
|
@ -14,57 +14,40 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.poll.impl
|
package io.element.android.features.ftue.impl.migration
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import com.bumble.appyx.core.composable.Children
|
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.libraries.architecture.BackstackNode
|
|
||||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
|
||||||
import io.element.android.libraries.architecture.createNode
|
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
class PollFlowNode @AssistedInject constructor(
|
class MigrationScreenNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
) : BackstackNode<PollFlowNode.NavTarget>(
|
private val presenter: MigrationScreenPresenter,
|
||||||
backstack = BackStack(
|
) : Node(buildContext, plugins = plugins) {
|
||||||
initialElement = NavTarget.Root,
|
|
||||||
savedStateMap = buildContext.savedStateMap,
|
|
||||||
),
|
|
||||||
buildContext = buildContext,
|
|
||||||
plugins = plugins,
|
|
||||||
) {
|
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
interface Callback : Plugin {
|
||||||
@Parcelize
|
fun onMigrationFinished()
|
||||||
object Root : NavTarget
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
private fun onMigrationFinished() {
|
||||||
return when (navTarget) {
|
plugins.filterIsInstance<Callback>().forEach { it.onMigrationFinished() }
|
||||||
NavTarget.Root -> {
|
|
||||||
createNode(buildContext)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
Children(
|
val state = presenter.present()
|
||||||
navModel = backstack,
|
MigrationScreenView(
|
||||||
modifier = modifier,
|
state,
|
||||||
transitionHandler = rememberDefaultTransitionHandler(),
|
onMigrationFinished = ::onMigrationFinished,
|
||||||
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.ftue.impl.migration
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class MigrationScreenPresenter @Inject constructor(
|
||||||
|
private val matrixClient: MatrixClient,
|
||||||
|
private val migrationScreenStore: MigrationScreenStore,
|
||||||
|
) : Presenter<MigrationScreenState> {
|
||||||
|
@Composable
|
||||||
|
override fun present(): MigrationScreenState {
|
||||||
|
val roomListState by matrixClient.roomListService.state.collectAsState()
|
||||||
|
if (roomListState == RoomListService.State.Running) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
migrationScreenStore.setMigrationScreenShown(matrixClient.sessionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MigrationScreenState(
|
||||||
|
isMigrating = roomListState != RoomListService.State.Running
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.ftue.impl.migration
|
||||||
|
|
||||||
|
data class MigrationScreenState(
|
||||||
|
val isMigrating: Boolean
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.ftue.impl.migration
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
|
||||||
|
interface MigrationScreenStore {
|
||||||
|
fun isMigrationScreenNeeded(sessionId: SessionId): Boolean
|
||||||
|
fun setMigrationScreenShown(sessionId: SessionId)
|
||||||
|
fun reset()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.ftue.impl.migration
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import io.element.android.features.ftue.impl.R
|
||||||
|
import io.element.android.libraries.designsystem.atomic.pages.SunsetPage
|
||||||
|
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MigrationScreenView(
|
||||||
|
migrationState: MigrationScreenState,
|
||||||
|
onMigrationFinished: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
if (migrationState.isMigrating.not()) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
onMigrationFinished()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SunsetPage(
|
||||||
|
modifier = modifier,
|
||||||
|
isLoading = true,
|
||||||
|
title = stringResource(id = R.string.screen_migration_title),
|
||||||
|
subtitle = stringResource(id = R.string.screen_migration_message),
|
||||||
|
overallContent = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@DayNightPreviews
|
||||||
|
@Composable
|
||||||
|
internal fun MigrationViewPreview() = ElementPreview {
|
||||||
|
MigrationScreenView(
|
||||||
|
migrationState = MigrationScreenState(isMigrating = true),
|
||||||
|
onMigrationFinished = {})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* 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.ftue.impl.migration
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.libraries.androidutils.hash.hash
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.di.DefaultPreferences
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class SharedPrefsMigrationScreenStore @Inject constructor(
|
||||||
|
@DefaultPreferences private val sharedPreferences: SharedPreferences,
|
||||||
|
) : MigrationScreenStore {
|
||||||
|
|
||||||
|
override fun isMigrationScreenNeeded(sessionId: SessionId): Boolean {
|
||||||
|
return sharedPreferences.getBoolean(sessionId.toKey(), false).not()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setMigrationScreenShown(sessionId: SessionId) {
|
||||||
|
sharedPreferences.edit().putBoolean(sessionId.toKey(), true).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun reset() {
|
||||||
|
sharedPreferences.edit {
|
||||||
|
sharedPreferences.all.keys
|
||||||
|
.filter { it.startsWith(IS_MIGRATION_SCREEN_SHOWN_PREFIX) }
|
||||||
|
.forEach {
|
||||||
|
remove(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun SessionId.toKey(): String {
|
||||||
|
// Hash the sessionId to get rid of exotic char and take only the first 16 chars,
|
||||||
|
// The risk of collision is not high.
|
||||||
|
return IS_MIGRATION_SCREEN_SHOWN_PREFIX + value.hash().take(16)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val IS_MIGRATION_SCREEN_SHOWN_PREFIX = "is_migration_screen_shown_"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -19,8 +19,10 @@ package io.element.android.features.ftue.impl.state
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.features.ftue.api.state.FtueState
|
import io.element.android.features.ftue.api.state.FtueState
|
||||||
|
import io.element.android.features.ftue.impl.migration.MigrationScreenStore
|
||||||
import io.element.android.features.ftue.impl.welcome.state.WelcomeScreenState
|
import io.element.android.features.ftue.impl.welcome.state.WelcomeScreenState
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
@ -30,11 +32,13 @@ import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(SessionScope::class)
|
||||||
class DefaultFtueState @Inject constructor(
|
class DefaultFtueState @Inject constructor(
|
||||||
private val coroutineScope: CoroutineScope,
|
private val coroutineScope: CoroutineScope,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
private val welcomeScreenState: WelcomeScreenState,
|
private val welcomeScreenState: WelcomeScreenState,
|
||||||
|
private val migrationScreenStore: MigrationScreenStore,
|
||||||
|
private val matrixClient: MatrixClient,
|
||||||
) : FtueState {
|
) : FtueState {
|
||||||
|
|
||||||
override val shouldDisplayFlow = MutableStateFlow(isAnyStepIncomplete())
|
override val shouldDisplayFlow = MutableStateFlow(isAnyStepIncomplete())
|
||||||
|
|
@ -42,6 +46,7 @@ class DefaultFtueState @Inject constructor(
|
||||||
override suspend fun reset() {
|
override suspend fun reset() {
|
||||||
welcomeScreenState.reset()
|
welcomeScreenState.reset()
|
||||||
analyticsService.reset()
|
analyticsService.reset()
|
||||||
|
migrationScreenStore.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
@ -52,7 +57,10 @@ class DefaultFtueState @Inject constructor(
|
||||||
|
|
||||||
fun getNextStep(currentStep: FtueStep? = null): FtueStep? =
|
fun getNextStep(currentStep: FtueStep? = null): FtueStep? =
|
||||||
when (currentStep) {
|
when (currentStep) {
|
||||||
null -> if (shouldDisplayWelcomeScreen()) FtueStep.WelcomeScreen else getNextStep(
|
null -> if (shouldDisplayMigrationScreen()) FtueStep.MigrationScreen else getNextStep(
|
||||||
|
FtueStep.MigrationScreen
|
||||||
|
)
|
||||||
|
FtueStep.MigrationScreen -> if (shouldDisplayWelcomeScreen()) FtueStep.WelcomeScreen else getNextStep(
|
||||||
FtueStep.WelcomeScreen
|
FtueStep.WelcomeScreen
|
||||||
)
|
)
|
||||||
FtueStep.WelcomeScreen -> if (needsAnalyticsOptIn()) FtueStep.AnalyticsOptIn else getNextStep(
|
FtueStep.WelcomeScreen -> if (needsAnalyticsOptIn()) FtueStep.AnalyticsOptIn else getNextStep(
|
||||||
|
|
@ -63,11 +71,16 @@ class DefaultFtueState @Inject constructor(
|
||||||
|
|
||||||
private fun isAnyStepIncomplete(): Boolean {
|
private fun isAnyStepIncomplete(): Boolean {
|
||||||
return listOf(
|
return listOf(
|
||||||
|
shouldDisplayMigrationScreen(),
|
||||||
shouldDisplayWelcomeScreen(),
|
shouldDisplayWelcomeScreen(),
|
||||||
needsAnalyticsOptIn()
|
needsAnalyticsOptIn()
|
||||||
).any { it }
|
).any { it }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun shouldDisplayMigrationScreen(): Boolean {
|
||||||
|
return migrationScreenStore.isMigrationScreenNeeded(matrixClient.sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
private fun needsAnalyticsOptIn(): Boolean {
|
private fun needsAnalyticsOptIn(): Boolean {
|
||||||
// We need this function to not be suspend, so we need to load the value through runBlocking
|
// We need this function to not be suspend, so we need to load the value through runBlocking
|
||||||
return runBlocking { analyticsService.didAskUserConsent().first().not() }
|
return runBlocking { analyticsService.didAskUserConsent().first().not() }
|
||||||
|
|
@ -89,6 +102,7 @@ class DefaultFtueState @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface FtueStep {
|
sealed interface FtueStep {
|
||||||
object WelcomeScreen : FtueStep
|
data object MigrationScreen : FtueStep
|
||||||
object AnalyticsOptIn : FtueStep
|
data object WelcomeScreen : FtueStep
|
||||||
|
data object AnalyticsOptIn : FtueStep
|
||||||
}
|
}
|
||||||
|
|
|
||||||
11
features/ftue/impl/src/main/res/values-cs/translations.xml
Normal file
11
features/ftue/impl/src/main/res/values-cs/translations.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_migration_message">"Toto je jednorázový proces, děkujeme za čekání."</string>
|
||||||
|
<string name="screen_migration_title">"Nastavení vašeho účtu"</string>
|
||||||
|
<string name="screen_welcome_bullet_1">"Hovory, hlasování, vyhledávání a další budou přidány koncem tohoto roku."</string>
|
||||||
|
<string name="screen_welcome_bullet_2">"Historie zpráv šifrovaných místností nebude v této aktualizaci k dispozici."</string>
|
||||||
|
<string name="screen_welcome_bullet_3">"Rádi bychom se od vás dozvěděli, co si o tom myslíte, dejte nám vědět prostřednictvím stránky s nastavením."</string>
|
||||||
|
<string name="screen_welcome_button">"Jdeme na to!"</string>
|
||||||
|
<string name="screen_welcome_subtitle">"Zde je to, co potřebujete vědět:"</string>
|
||||||
|
<string name="screen_welcome_title">"Vítá vás %1$s!"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_welcome_bullet_1">"Anrufe, Standortfreigabe, Suche und mehr werden später in diesem Jahr hinzugefügt."</string>
|
<string name="screen_migration_message">"Dies ist ein einmaliger Vorgang, danke fürs Warten."</string>
|
||||||
|
<string name="screen_migration_title">"Dein Konto einrichten"</string>
|
||||||
|
<string name="screen_welcome_bullet_1">"Anrufe, Umfragen, Suche und mehr werden später in diesem Jahr hinzugefügt."</string>
|
||||||
<string name="screen_welcome_bullet_2">"Der Nachrichtenverlauf für verschlüsselte Räume wird in diesem Update nicht verfügbar sein."</string>
|
<string name="screen_welcome_bullet_2">"Der Nachrichtenverlauf für verschlüsselte Räume wird in diesem Update nicht verfügbar sein."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Wir würden uns freuen, wenn du uns über die Einstellungsseite deine Meinung mitteilst."</string>
|
<string name="screen_welcome_bullet_3">"Wir würden uns freuen, wenn du uns über die Einstellungsseite deine Meinung mitteilst."</string>
|
||||||
<string name="screen_welcome_button">"Los geht\'s!"</string>
|
<string name="screen_welcome_button">"Los geht\'s!"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_migration_message">"Ce processus n’a besoin d’être fait qu’une seule fois, merci de patienter."</string>
|
||||||
|
<string name="screen_migration_title">"Configuration de votre compte."</string>
|
||||||
<string name="screen_welcome_bullet_2">"L’historique des messages pour les salons chiffrés ne sera pas disponible dans cette mise à jour."</string>
|
<string name="screen_welcome_bullet_2">"L’historique des messages pour les salons chiffrés ne sera pas disponible dans cette mise à jour."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Nous serions ravis d’avoir votre avis, n’hésitez pas à nous le partager via la page des paramètres."</string>
|
<string name="screen_welcome_bullet_3">"Nous serions ravis d’avoir votre avis, n’hésitez pas à nous le partager via la page des paramètres."</string>
|
||||||
<string name="screen_welcome_button">"C’est parti !"</string>
|
<string name="screen_welcome_button">"C’est parti !"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_migration_message">"Это одноразовый процесс, спасибо, что подождали."</string>
|
||||||
|
<string name="screen_migration_title">"Настройка учетной записи."</string>
|
||||||
<string name="screen_welcome_bullet_1">"Звонки, опросы, поиск и многое другое будут добавлены позже в этом году."</string>
|
<string name="screen_welcome_bullet_1">"Звонки, опросы, поиск и многое другое будут добавлены позже в этом году."</string>
|
||||||
<string name="screen_welcome_bullet_2">"История сообщений для зашифрованных комнат в этом обновлении будет недоступна."</string>
|
<string name="screen_welcome_bullet_2">"История сообщений для зашифрованных комнат в этом обновлении будет недоступна."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Мы будем рады услышать ваше мнение, сообщите нам об этом через страницу настроек."</string>
|
<string name="screen_welcome_bullet_3">"Мы будем рады услышать ваше мнение, сообщите нам об этом через страницу настроек."</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_migration_message">"Ide o jednorazový proces, ďakujeme za trpezlivosť."</string>
|
||||||
|
<string name="screen_migration_title">"Nastavenie vášho účtu."</string>
|
||||||
<string name="screen_welcome_bullet_1">"Hovory, ankety, vyhľadávanie a ďalšie funkcie pribudnú neskôr v tomto roku."</string>
|
<string name="screen_welcome_bullet_1">"Hovory, ankety, vyhľadávanie a ďalšie funkcie pribudnú neskôr v tomto roku."</string>
|
||||||
<string name="screen_welcome_bullet_2">"História správ pre zašifrované miestnosti nebude v tejto aktualizácii k dispozícii."</string>
|
<string name="screen_welcome_bullet_2">"História správ pre zašifrované miestnosti nebude v tejto aktualizácii k dispozícii."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Radi by sme od vás počuli, dajte nám vedieť, čo si myslíte, prostredníctvom stránky nastavení."</string>
|
<string name="screen_welcome_bullet_3">"Radi by sme od vás počuli, dajte nám vedieť, čo si myslíte, prostredníctvom stránky nastavení."</string>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_migration_title">"設定您的帳號"</string>
|
||||||
<string name="screen_welcome_button">"開始吧!"</string>
|
<string name="screen_welcome_button">"開始吧!"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_migration_message">"This is a one time process, thanks for waiting."</string>
|
||||||
|
<string name="screen_migration_title">"Setting up your account."</string>
|
||||||
<string name="screen_welcome_bullet_1">"Calls, polls, search and more will be added later this year."</string>
|
<string name="screen_welcome_bullet_1">"Calls, polls, search and more will be added later this year."</string>
|
||||||
<string name="screen_welcome_bullet_2">"Message history for encrypted rooms won’t be available in this update."</string>
|
<string name="screen_welcome_bullet_2">"Message history for encrypted rooms won’t be available in this update."</string>
|
||||||
<string name="screen_welcome_bullet_3">"We’d love to hear from you, let us know what you think via the settings page."</string>
|
<string name="screen_welcome_bullet_3">"We’d love to hear from you, let us know what you think via the settings page."</string>
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,16 @@
|
||||||
package io.element.android.features.ftue.impl
|
package io.element.android.features.ftue.impl
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.analytics.test.FakeAnalyticsService
|
import io.element.android.features.ftue.impl.migration.InMemoryMigrationScreenStore
|
||||||
|
import io.element.android.features.ftue.impl.migration.MigrationScreenStore
|
||||||
import io.element.android.features.ftue.impl.state.DefaultFtueState
|
import io.element.android.features.ftue.impl.state.DefaultFtueState
|
||||||
import io.element.android.features.ftue.impl.state.FtueStep
|
import io.element.android.features.ftue.impl.state.FtueStep
|
||||||
import io.element.android.features.ftue.impl.welcome.state.FakeWelcomeState
|
import io.element.android.features.ftue.impl.welcome.state.FakeWelcomeState
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
|
|
@ -45,12 +50,14 @@ class DefaultFtueStateTests {
|
||||||
fun `given all checks being true, should display flow is false`() = runTest {
|
fun `given all checks being true, should display flow is false`() = runTest {
|
||||||
val welcomeState = FakeWelcomeState()
|
val welcomeState = FakeWelcomeState()
|
||||||
val analyticsService = FakeAnalyticsService()
|
val analyticsService = FakeAnalyticsService()
|
||||||
|
val migrationScreenStore = InMemoryMigrationScreenStore()
|
||||||
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||||
|
|
||||||
val state = createState(coroutineScope, welcomeState, analyticsService)
|
val state = createState(coroutineScope, welcomeState, analyticsService, migrationScreenStore)
|
||||||
|
|
||||||
welcomeState.setWelcomeScreenShown()
|
welcomeState.setWelcomeScreenShown()
|
||||||
analyticsService.setDidAskUserConsent()
|
analyticsService.setDidAskUserConsent()
|
||||||
|
migrationScreenStore.setMigrationScreenShown(A_SESSION_ID)
|
||||||
state.updateState()
|
state.updateState()
|
||||||
|
|
||||||
assertThat(state.shouldDisplayFlow.value).isFalse()
|
assertThat(state.shouldDisplayFlow.value).isFalse()
|
||||||
|
|
@ -63,16 +70,21 @@ class DefaultFtueStateTests {
|
||||||
fun `traverse flow`() = runTest {
|
fun `traverse flow`() = runTest {
|
||||||
val welcomeState = FakeWelcomeState()
|
val welcomeState = FakeWelcomeState()
|
||||||
val analyticsService = FakeAnalyticsService()
|
val analyticsService = FakeAnalyticsService()
|
||||||
|
val migrationScreenStore = InMemoryMigrationScreenStore()
|
||||||
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||||
|
|
||||||
val state = createState(coroutineScope, welcomeState, analyticsService)
|
val state = createState(coroutineScope, welcomeState, analyticsService, migrationScreenStore)
|
||||||
val steps = mutableListOf<FtueStep?>()
|
val steps = mutableListOf<FtueStep?>()
|
||||||
|
|
||||||
// First step, welcome screen
|
// First step, migration screen
|
||||||
|
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||||
|
migrationScreenStore.setMigrationScreenShown(A_SESSION_ID)
|
||||||
|
|
||||||
|
// Second step, welcome screen
|
||||||
steps.add(state.getNextStep(steps.lastOrNull()))
|
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||||
welcomeState.setWelcomeScreenShown()
|
welcomeState.setWelcomeScreenShown()
|
||||||
|
|
||||||
// Second step, analytics opt in
|
// Third step, analytics opt in
|
||||||
steps.add(state.getNextStep(steps.lastOrNull()))
|
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||||
analyticsService.setDidAskUserConsent()
|
analyticsService.setDidAskUserConsent()
|
||||||
|
|
||||||
|
|
@ -80,6 +92,7 @@ class DefaultFtueStateTests {
|
||||||
steps.add(state.getNextStep(steps.lastOrNull()))
|
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||||
|
|
||||||
assertThat(steps).containsExactly(
|
assertThat(steps).containsExactly(
|
||||||
|
FtueStep.MigrationScreen,
|
||||||
FtueStep.WelcomeScreen,
|
FtueStep.WelcomeScreen,
|
||||||
FtueStep.AnalyticsOptIn,
|
FtueStep.AnalyticsOptIn,
|
||||||
null, // Final state
|
null, // Final state
|
||||||
|
|
@ -93,7 +106,16 @@ class DefaultFtueStateTests {
|
||||||
fun `if a check for a step is true, start from the next one`() = runTest {
|
fun `if a check for a step is true, start from the next one`() = runTest {
|
||||||
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||||
val analyticsService = FakeAnalyticsService()
|
val analyticsService = FakeAnalyticsService()
|
||||||
val state = createState(coroutineScope = coroutineScope, analyticsService = analyticsService)
|
val migrationScreenStore = InMemoryMigrationScreenStore()
|
||||||
|
|
||||||
|
val state = createState(
|
||||||
|
coroutineScope = coroutineScope,
|
||||||
|
analyticsService = analyticsService,
|
||||||
|
migrationScreenStore = migrationScreenStore,
|
||||||
|
)
|
||||||
|
|
||||||
|
migrationScreenStore.setMigrationScreenShown(A_SESSION_ID)
|
||||||
|
assertThat(state.getNextStep()).isEqualTo(FtueStep.WelcomeScreen)
|
||||||
|
|
||||||
state.setWelcomeScreenShown()
|
state.setWelcomeScreenShown()
|
||||||
assertThat(state.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn)
|
assertThat(state.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn)
|
||||||
|
|
@ -108,7 +130,14 @@ class DefaultFtueStateTests {
|
||||||
private fun createState(
|
private fun createState(
|
||||||
coroutineScope: CoroutineScope,
|
coroutineScope: CoroutineScope,
|
||||||
welcomeState: FakeWelcomeState = FakeWelcomeState(),
|
welcomeState: FakeWelcomeState = FakeWelcomeState(),
|
||||||
analyticsService: AnalyticsService = FakeAnalyticsService()
|
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
||||||
) = DefaultFtueState(coroutineScope, analyticsService, welcomeState)
|
migrationScreenStore: MigrationScreenStore = InMemoryMigrationScreenStore(),
|
||||||
|
matrixClient: MatrixClient = FakeMatrixClient(),
|
||||||
|
) = DefaultFtueState(
|
||||||
|
coroutineScope = coroutineScope,
|
||||||
|
analyticsService = analyticsService,
|
||||||
|
welcomeScreenState = welcomeState,
|
||||||
|
migrationScreenStore = migrationScreenStore,
|
||||||
|
matrixClient = matrixClient,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.ftue.impl.migration
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
|
||||||
|
class InMemoryMigrationScreenStore : MigrationScreenStore {
|
||||||
|
private val store = mutableMapOf<SessionId, Boolean>()
|
||||||
|
|
||||||
|
override fun isMigrationScreenNeeded(sessionId: SessionId): Boolean {
|
||||||
|
// If store does not have key return true, else return the opposite of the value
|
||||||
|
return store[sessionId]?.not() ?: true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setMigrationScreenShown(sessionId: SessionId) {
|
||||||
|
store[sessionId] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun reset() {
|
||||||
|
store.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
* 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.ftue.impl.migration
|
||||||
|
|
||||||
|
import app.cash.molecule.RecompositionMode
|
||||||
|
import app.cash.molecule.moleculeFlow
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
|
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.roomlist.FakeRoomListService
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class MigrationScreenPresenterTest {
|
||||||
|
@Test
|
||||||
|
fun `present - initial`() = runTest {
|
||||||
|
val presenter = createPresenter()
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.isMigrating).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - migration end`() = runTest {
|
||||||
|
val matrixClient = FakeMatrixClient()
|
||||||
|
val migrationScreenStore = InMemoryMigrationScreenStore()
|
||||||
|
val presenter = createPresenter(matrixClient, migrationScreenStore)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.isMigrating).isTrue()
|
||||||
|
assertThat(migrationScreenStore.isMigrationScreenNeeded(A_SESSION_ID)).isTrue()
|
||||||
|
// Simulate room list loaded
|
||||||
|
(matrixClient.roomListService as FakeRoomListService).postState(RoomListService.State.Running)
|
||||||
|
val nextState = awaitItem()
|
||||||
|
assertThat(nextState.isMigrating).isFalse()
|
||||||
|
assertThat(migrationScreenStore.isMigrationScreenNeeded(A_SESSION_ID)).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createPresenter(
|
||||||
|
matrixClient: MatrixClient = FakeMatrixClient(),
|
||||||
|
migrationScreenStore: MigrationScreenStore = InMemoryMigrationScreenStore(),
|
||||||
|
) = MigrationScreenPresenter(
|
||||||
|
matrixClient,
|
||||||
|
migrationScreenStore,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -53,7 +53,7 @@ dependencies {
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.libraries.push.test)
|
testImplementation(projects.libraries.push.test)
|
||||||
testImplementation(projects.features.invitelist.test)
|
testImplementation(projects.features.invitelist.test)
|
||||||
testImplementation(projects.features.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
|
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,14 +19,12 @@ package io.element.android.features.invitelist.impl
|
||||||
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
|
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
|
||||||
|
|
||||||
sealed interface InviteListEvents {
|
sealed interface InviteListEvents {
|
||||||
|
|
||||||
data class AcceptInvite(val invite: InviteListInviteSummary) : InviteListEvents
|
data class AcceptInvite(val invite: InviteListInviteSummary) : InviteListEvents
|
||||||
data class DeclineInvite(val invite: InviteListInviteSummary) : InviteListEvents
|
data class DeclineInvite(val invite: InviteListInviteSummary) : InviteListEvents
|
||||||
|
|
||||||
object ConfirmDeclineInvite: InviteListEvents
|
data object ConfirmDeclineInvite: InviteListEvents
|
||||||
object CancelDeclineInvite: InviteListEvents
|
data object CancelDeclineInvite: InviteListEvents
|
||||||
|
|
||||||
object DismissAcceptError: InviteListEvents
|
|
||||||
object DismissDeclineError: InviteListEvents
|
|
||||||
|
|
||||||
|
data object DismissAcceptError: InviteListEvents
|
||||||
|
data object DismissDeclineError: InviteListEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,6 @@ data class InviteListState(
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed interface InviteDeclineConfirmationDialog {
|
sealed interface InviteDeclineConfirmationDialog {
|
||||||
object Hidden : InviteDeclineConfirmationDialog
|
data object Hidden : InviteDeclineConfirmationDialog
|
||||||
data class Visible(val isDirect: Boolean, val name: String) : InviteDeclineConfirmationDialog
|
data class Visible(val isDirect: Boolean, val name: String) : InviteDeclineConfirmationDialog
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth
|
import com.google.common.truth.Truth
|
||||||
import io.element.android.features.analytics.test.FakeAnalyticsService
|
|
||||||
import io.element.android.features.invitelist.api.SeenInvitesStore
|
import io.element.android.features.invitelist.api.SeenInvitesStore
|
||||||
import io.element.android.features.invitelist.test.FakeSeenInvitesStore
|
import io.element.android.features.invitelist.test.FakeSeenInvitesStore
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
|
|
@ -44,6 +43,7 @@ import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
||||||
import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager
|
import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
sealed interface LeaveRoomEvent {
|
sealed interface LeaveRoomEvent {
|
||||||
data class ShowConfirmation(val roomId: RoomId) : LeaveRoomEvent
|
data class ShowConfirmation(val roomId: RoomId) : LeaveRoomEvent
|
||||||
object HideConfirmation : LeaveRoomEvent
|
data object HideConfirmation : LeaveRoomEvent
|
||||||
data class LeaveRoom(val roomId: RoomId) : LeaveRoomEvent
|
data class LeaveRoom(val roomId: RoomId) : LeaveRoomEvent
|
||||||
object HideError : LeaveRoomEvent
|
data object HideError : LeaveRoomEvent
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,19 +25,19 @@ data class LeaveRoomState(
|
||||||
val eventSink: (LeaveRoomEvent) -> Unit = {},
|
val eventSink: (LeaveRoomEvent) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
sealed interface Confirmation {
|
sealed interface Confirmation {
|
||||||
object Hidden : Confirmation
|
data object Hidden : Confirmation
|
||||||
data class Generic(val roomId: RoomId) : Confirmation
|
data class Generic(val roomId: RoomId) : Confirmation
|
||||||
data class PrivateRoom(val roomId: RoomId) : Confirmation
|
data class PrivateRoom(val roomId: RoomId) : Confirmation
|
||||||
data class LastUserInRoom(val roomId: RoomId) : Confirmation
|
data class LastUserInRoom(val roomId: RoomId) : Confirmation
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface Progress {
|
sealed interface Progress {
|
||||||
object Hidden : Progress
|
data object Hidden : Progress
|
||||||
object Shown : Progress
|
data object Shown : Progress
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface Error {
|
sealed interface Error {
|
||||||
object Hidden : Error
|
data object Hidden : Error
|
||||||
object Shown : Error
|
data object Shown : Error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,6 @@ dependencies {
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.features.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.features.messages.test)
|
testImplementation(projects.features.messages.test)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,5 @@
|
||||||
package io.element.android.features.location.impl.common.permissions
|
package io.element.android.features.location.impl.common.permissions
|
||||||
|
|
||||||
sealed interface PermissionsEvents {
|
sealed interface PermissionsEvents {
|
||||||
object RequestPermissions : PermissionsEvents
|
data object RequestPermissions : PermissionsEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,9 @@ data class PermissionsState(
|
||||||
val eventSink: (PermissionsEvents) -> Unit = {},
|
val eventSink: (PermissionsEvents) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
sealed interface Permissions {
|
sealed interface Permissions {
|
||||||
object AllGranted : Permissions
|
data object AllGranted : Permissions
|
||||||
object SomeGranted : Permissions
|
data object SomeGranted : Permissions
|
||||||
object NoneGranted : Permissions
|
data object NoneGranted : Permissions
|
||||||
}
|
}
|
||||||
|
|
||||||
val isAnyGranted: Boolean
|
val isAnyGranted: Boolean
|
||||||
|
|
|
||||||
|
|
@ -30,13 +30,9 @@ sealed interface SendLocationEvents {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
object SwitchToMyLocationMode : SendLocationEvents
|
data object SwitchToMyLocationMode : SendLocationEvents
|
||||||
|
data object SwitchToPinLocationMode : SendLocationEvents
|
||||||
object SwitchToPinLocationMode : SendLocationEvents
|
data object DismissDialog : SendLocationEvents
|
||||||
|
data object RequestPermissions : SendLocationEvents
|
||||||
object DismissDialog : SendLocationEvents
|
data object OpenAppSettings : SendLocationEvents
|
||||||
|
|
||||||
object RequestPermissions : SendLocationEvents
|
|
||||||
|
|
||||||
object OpenAppSettings : SendLocationEvents
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,13 +24,13 @@ data class SendLocationState(
|
||||||
val eventSink: (SendLocationEvents) -> Unit = {},
|
val eventSink: (SendLocationEvents) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
sealed interface Mode {
|
sealed interface Mode {
|
||||||
object SenderLocation : Mode
|
data object SenderLocation : Mode
|
||||||
object PinLocation : Mode
|
data object PinLocation : Mode
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface Dialog {
|
sealed interface Dialog {
|
||||||
object None : Dialog
|
data object None : Dialog
|
||||||
object PermissionRationale : Dialog
|
data object PermissionRationale : Dialog
|
||||||
object PermissionDenied : Dialog
|
data object PermissionDenied : Dialog
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,9 @@
|
||||||
package io.element.android.features.location.impl.show
|
package io.element.android.features.location.impl.show
|
||||||
|
|
||||||
sealed interface ShowLocationEvents {
|
sealed interface ShowLocationEvents {
|
||||||
object Share : ShowLocationEvents
|
data object Share : ShowLocationEvents
|
||||||
data class TrackMyLocation(val enabled: Boolean) : ShowLocationEvents
|
data class TrackMyLocation(val enabled: Boolean) : ShowLocationEvents
|
||||||
object DismissDialog : ShowLocationEvents
|
data object DismissDialog : ShowLocationEvents
|
||||||
|
data object RequestPermissions : ShowLocationEvents
|
||||||
object RequestPermissions : ShowLocationEvents
|
data object OpenAppSettings : ShowLocationEvents
|
||||||
|
|
||||||
object OpenAppSettings : ShowLocationEvents
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,8 @@ data class ShowLocationState(
|
||||||
val eventSink: (ShowLocationEvents) -> Unit,
|
val eventSink: (ShowLocationEvents) -> Unit,
|
||||||
) {
|
) {
|
||||||
sealed interface Dialog {
|
sealed interface Dialog {
|
||||||
object None : Dialog
|
data object None : Dialog
|
||||||
object PermissionRationale : Dialog
|
data object PermissionRationale : Dialog
|
||||||
object PermissionDenied : Dialog
|
data object PermissionDenied : Dialog
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth
|
import com.google.common.truth.Truth
|
||||||
import im.vector.app.features.analytics.plan.Composer
|
import im.vector.app.features.analytics.plan.Composer
|
||||||
import io.element.android.features.analytics.test.FakeAnalyticsService
|
|
||||||
import io.element.android.features.location.api.Location
|
import io.element.android.features.location.api.Location
|
||||||
import io.element.android.features.location.impl.common.actions.FakeLocationActions
|
import io.element.android.features.location.impl.common.actions.FakeLocationActions
|
||||||
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
|
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
|
||||||
|
|
@ -34,6 +33,7 @@ import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.matrix.test.room.SendLocationInvocation
|
import io.element.android.libraries.matrix.test.room.SendLocationInvocation
|
||||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||||
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,6 @@
|
||||||
package io.element.android.features.login.api.oidc
|
package io.element.android.features.login.api.oidc
|
||||||
|
|
||||||
sealed interface OidcAction {
|
sealed interface OidcAction {
|
||||||
object GoBack : OidcAction
|
data object GoBack : OidcAction
|
||||||
data class Success(val url: String) : OidcAction
|
data class Success(val url: String) : OidcAction
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ plugins {
|
||||||
alias(libs.plugins.anvil)
|
alias(libs.plugins.anvil)
|
||||||
alias(libs.plugins.ksp)
|
alias(libs.plugins.ksp)
|
||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
kotlin("plugin.serialization") version "1.9.0"
|
kotlin("plugin.serialization") version "1.9.10"
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,9 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.bumble.appyx.core.composable.Children
|
import com.bumble.appyx.core.composable.Children
|
||||||
|
import com.bumble.appyx.core.lifecycle.subscribe
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
|
@ -33,6 +35,8 @@ import com.bumble.appyx.navmodel.backstack.operation.singleTop
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.features.login.api.oidc.OidcAction
|
||||||
|
import io.element.android.features.login.api.oidc.OidcActionFlow
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||||
import io.element.android.features.login.impl.oidc.CustomTabAvailabilityChecker
|
import io.element.android.features.login.impl.oidc.CustomTabAvailabilityChecker
|
||||||
import io.element.android.features.login.impl.oidc.customtab.CustomTabHandler
|
import io.element.android.features.login.impl.oidc.customtab.CustomTabHandler
|
||||||
|
|
@ -51,6 +55,8 @@ import io.element.android.libraries.architecture.inputs
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||||
import io.element.android.libraries.theme.ElementTheme
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@ContributesNode(AppScope::class)
|
@ContributesNode(AppScope::class)
|
||||||
|
|
@ -61,6 +67,7 @@ class LoginFlowNode @AssistedInject constructor(
|
||||||
private val customTabHandler: CustomTabHandler,
|
private val customTabHandler: CustomTabHandler,
|
||||||
private val accountProviderDataSource: AccountProviderDataSource,
|
private val accountProviderDataSource: AccountProviderDataSource,
|
||||||
private val defaultLoginUserStory: DefaultLoginUserStory,
|
private val defaultLoginUserStory: DefaultLoginUserStory,
|
||||||
|
private val oidcActionFlow: OidcActionFlow,
|
||||||
) : BackstackNode<LoginFlowNode.NavTarget>(
|
) : BackstackNode<LoginFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.ConfirmAccountProvider,
|
initialElement = NavTarget.ConfirmAccountProvider,
|
||||||
|
|
@ -78,23 +85,40 @@ class LoginFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
private val inputs: Inputs = inputs()
|
private val inputs: Inputs = inputs()
|
||||||
|
|
||||||
|
private var customChromeTabStarted = false
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
super.onBuilt()
|
super.onBuilt()
|
||||||
defaultLoginUserStory.setLoginFlowIsDone(false)
|
defaultLoginUserStory.setLoginFlowIsDone(false)
|
||||||
|
lifecycle.subscribe(
|
||||||
|
onResume = {
|
||||||
|
if (customChromeTabStarted) {
|
||||||
|
customChromeTabStarted = false
|
||||||
|
// Workaround to detect that the Custom Chrome Tab has been closed
|
||||||
|
// If there is no coming OidcAction (that would end this Node),
|
||||||
|
// consider that the user has cancelled the login
|
||||||
|
// by pressing back or by closing the Custom Chrome Tab.
|
||||||
|
lifecycleScope.launch {
|
||||||
|
delay(5000)
|
||||||
|
oidcActionFlow.post(OidcAction.GoBack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object ConfirmAccountProvider : NavTarget
|
data object ConfirmAccountProvider : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object ChangeAccountProvider : NavTarget
|
data object ChangeAccountProvider : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object SearchAccountProvider : NavTarget
|
data object SearchAccountProvider : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object LoginPassword : NavTarget
|
data object LoginPassword : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class WaitList(val loginFormState: LoginFormState) : NavTarget
|
data class WaitList(val loginFormState: LoginFormState) : NavTarget
|
||||||
|
|
@ -113,7 +137,10 @@ class LoginFlowNode @AssistedInject constructor(
|
||||||
override fun onOidcDetails(oidcDetails: OidcDetails) {
|
override fun onOidcDetails(oidcDetails: OidcDetails) {
|
||||||
if (customTabAvailabilityChecker.supportCustomTab()) {
|
if (customTabAvailabilityChecker.supportCustomTab()) {
|
||||||
// In this case open a Chrome Custom tab
|
// In this case open a Chrome Custom tab
|
||||||
activity?.let { customTabHandler.open(it, darkTheme, oidcDetails.url) }
|
activity?.let {
|
||||||
|
customChromeTabStarted = true
|
||||||
|
customTabHandler.open(it, darkTheme, oidcDetails.url)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback to WebView mode
|
// Fallback to WebView mode
|
||||||
backstack.push(NavTarget.OidcView(oidcDetails))
|
backstack.push(NavTarget.OidcView(oidcDetails))
|
||||||
|
|
|
||||||
|
|
@ -20,5 +20,5 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||||
|
|
||||||
sealed interface ChangeServerEvents {
|
sealed interface ChangeServerEvents {
|
||||||
data class ChangeServer(val accountProvider: AccountProvider) : ChangeServerEvents
|
data class ChangeServer(val accountProvider: AccountProvider) : ChangeServerEvents
|
||||||
object ClearError : ChangeServerEvents
|
data object ClearError : ChangeServerEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ sealed class ChangeServerError : Throwable() {
|
||||||
@Composable
|
@Composable
|
||||||
fun message(): String = stringResource(messageId)
|
fun message(): String = stringResource(messageId)
|
||||||
}
|
}
|
||||||
object SlidingSyncAlert : ChangeServerError()
|
data object SlidingSyncAlert : ChangeServerError()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun from(error: Throwable): ChangeServerError = when (error) {
|
fun from(error: Throwable): ChangeServerError = when (error) {
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ package io.element.android.features.login.impl.oidc.webview
|
||||||
import io.element.android.features.login.api.oidc.OidcAction
|
import io.element.android.features.login.api.oidc.OidcAction
|
||||||
|
|
||||||
sealed interface OidcEvents {
|
sealed interface OidcEvents {
|
||||||
object Cancel : OidcEvents
|
data object Cancel : OidcEvents
|
||||||
data class OidcActionEvent(val oidcAction: OidcAction): OidcEvents
|
data class OidcActionEvent(val oidcAction: OidcAction): OidcEvents
|
||||||
object ClearError : OidcEvents
|
data object ClearError : OidcEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,6 @@
|
||||||
package io.element.android.features.login.impl.screens.confirmaccountprovider
|
package io.element.android.features.login.impl.screens.confirmaccountprovider
|
||||||
|
|
||||||
sealed interface ConfirmAccountProviderEvents {
|
sealed interface ConfirmAccountProviderEvents {
|
||||||
object Continue : ConfirmAccountProviderEvents
|
data object Continue : ConfirmAccountProviderEvents
|
||||||
object ClearError : ConfirmAccountProviderEvents
|
data object ClearError : ConfirmAccountProviderEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,9 +68,9 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
launch {
|
defaultOidcActionFlow.collect { oidcAction ->
|
||||||
defaultOidcActionFlow.collect {
|
if (oidcAction != null) {
|
||||||
onOidcAction(it, loginFlowAction)
|
onOidcAction(oidcAction, loginFlowAction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -113,10 +113,9 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun onOidcAction(
|
private suspend fun onOidcAction(
|
||||||
oidcAction: OidcAction?,
|
oidcAction: OidcAction,
|
||||||
loginFlowAction: MutableState<Async<LoginFlow>>,
|
loginFlowAction: MutableState<Async<LoginFlow>>,
|
||||||
) {
|
) {
|
||||||
oidcAction ?: return
|
|
||||||
loginFlowAction.value = Async.Loading()
|
loginFlowAction.value = Async.Loading()
|
||||||
when (oidcAction) {
|
when (oidcAction) {
|
||||||
OidcAction.GoBack -> {
|
OidcAction.GoBack -> {
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,6 @@ data class ConfirmAccountProviderState(
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface LoginFlow {
|
sealed interface LoginFlow {
|
||||||
object PasswordLogin : LoginFlow
|
data object PasswordLogin : LoginFlow
|
||||||
data class OidcFlow(val oidcDetails: OidcDetails) : LoginFlow
|
data class OidcFlow(val oidcDetails: OidcDetails) : LoginFlow
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,6 @@ package io.element.android.features.login.impl.screens.loginpassword
|
||||||
sealed interface LoginPasswordEvents {
|
sealed interface LoginPasswordEvents {
|
||||||
data class SetLogin(val login: String) : LoginPasswordEvents
|
data class SetLogin(val login: String) : LoginPasswordEvents
|
||||||
data class SetPassword(val password: String) : LoginPasswordEvents
|
data class SetPassword(val password: String) : LoginPasswordEvents
|
||||||
object Submit : LoginPasswordEvents
|
data object Submit : LoginPasswordEvents
|
||||||
object ClearError : LoginPasswordEvents
|
data object ClearError : LoginPasswordEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
package io.element.android.features.login.impl.screens.waitlistscreen
|
package io.element.android.features.login.impl.screens.waitlistscreen
|
||||||
|
|
||||||
sealed interface WaitListEvents {
|
sealed interface WaitListEvents {
|
||||||
object AttemptLogin : WaitListEvents
|
data object AttemptLogin : WaitListEvents
|
||||||
object ClearError : WaitListEvents
|
data object ClearError : WaitListEvents
|
||||||
object Continue : WaitListEvents
|
data object Continue : WaitListEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue