Merge branch 'develop' of https://github.com/vector-im/element-x-android into yostyle/notifications_global_settings
This commit is contained in:
commit
5e2ec8b504
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
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2.7.1
|
||||
uses: gradle/gradle-build-action@v2.8.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- 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
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2.7.1
|
||||
uses: gradle/gradle-build-action@v2.8.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- 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
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2.7.1
|
||||
uses: gradle/gradle-build-action@v2.8.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- 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'
|
||||
# Add gradle cache, this should speed up the process
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2.7.1
|
||||
uses: gradle/gradle-build-action@v2.8.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- 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
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2.7.1
|
||||
uses: gradle/gradle-build-action@v2.8.0
|
||||
- name: Create app bundle
|
||||
env:
|
||||
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
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2.7.1
|
||||
uses: gradle/gradle-build-action@v2.8.0
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- 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
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2.7.1
|
||||
uses: gradle/gradle-build-action@v2.8.0
|
||||
with:
|
||||
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)
|
||||
========================================
|
||||
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ If after your research you still have a question, ask at [#element-x-android:mat
|
|||
|
||||
## 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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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.ParentNode
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import io.element.android.appnav.LoggedInFlowNode
|
||||
import io.element.android.appnav.room.RoomLoadedFlowNode
|
||||
import io.element.android.appnav.LoggedInAppScopeFlowNode
|
||||
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.createNode
|
||||
import io.element.android.libraries.di.DaggerComponentOwner
|
||||
|
|
@ -45,18 +45,17 @@ class MainNode(
|
|||
buildContext: BuildContext,
|
||||
private val mainDaggerComponentOwner: MainDaggerComponentsOwner,
|
||||
plugins: List<Plugin>,
|
||||
) :
|
||||
ParentNode<MainNode.RootNavTarget>(
|
||||
navModel = PermanentNavModel(
|
||||
navTargets = setOf(RootNavTarget),
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
) : ParentNode<MainNode.RootNavTarget>(
|
||||
navModel = PermanentNavModel(
|
||||
navTargets = setOf(RootNavTarget),
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
),
|
||||
DaggerComponentOwner by mainDaggerComponentOwner {
|
||||
|
||||
private val loggedInFlowNodeCallback = object : LoggedInFlowNode.LifecycleCallback {
|
||||
private val loggedInFlowNodeCallback = object : LoggedInAppScopeFlowNode.LifecycleCallback {
|
||||
override fun onFlowCreated(identifier: String, client: MatrixClient) {
|
||||
val component = bindings<SessionComponent.ParentBindings>().sessionComponentBuilder().client(client).build()
|
||||
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.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import coil.Coil
|
||||
import com.bumble.appyx.core.composable.Children
|
||||
import com.bumble.appyx.core.lifecycle.subscribe
|
||||
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.verifysession.api.VerifySessionEntryPoint
|
||||
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.bindings
|
||||
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.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.core.MAIN_SPACE
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
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.services.appnavstate.api.AppNavigationStateService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -76,7 +71,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.parcelize.Parcelize
|
||||
import timber.log.Timber
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
@ContributesNode(SessionScope::class)
|
||||
class LoggedInFlowNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
@ -91,6 +86,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
private val networkMonitor: NetworkMonitor,
|
||||
private val notificationDrawerManager: NotificationDrawerManager,
|
||||
private val ftueState: FtueState,
|
||||
private val matrixClient: MatrixClient,
|
||||
snackbarDispatcher: SnackbarDispatcher,
|
||||
) : BackstackNode<LoggedInFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
|
|
@ -105,32 +101,18 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
fun onOpenBugReport()
|
||||
}
|
||||
|
||||
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()
|
||||
private val syncService = inputs.matrixClient.syncService()
|
||||
private val syncService = matrixClient.syncService()
|
||||
private val loggedInFlowProcessor = LoggedInEventProcessor(
|
||||
snackbarDispatcher,
|
||||
inputs.matrixClient.roomMembershipObserver(),
|
||||
inputs.matrixClient.sessionVerificationService(),
|
||||
matrixClient.roomMembershipObserver(),
|
||||
matrixClient.sessionVerificationService(),
|
||||
)
|
||||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
lifecycle.subscribe(
|
||||
onCreate = {
|
||||
plugins<LifecycleCallback>().forEach { it.onFlowCreated(id, inputs.matrixClient) }
|
||||
val imageLoaderFactory = bindings<MatrixUIBindings>().loggedInImageLoaderFactory()
|
||||
Coil.setImageLoader(imageLoaderFactory)
|
||||
appNavigationStateService.onNavigateToSession(id, inputs.matrixClient.sessionId)
|
||||
appNavigationStateService.onNavigateToSession(id, matrixClient.sessionId)
|
||||
// TODO We do not support Space yet, so directly navigate to main space
|
||||
appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE)
|
||||
loggedInFlowProcessor.observeEvents(coroutineScope)
|
||||
|
|
@ -146,7 +128,6 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
}
|
||||
},
|
||||
onDestroy = {
|
||||
plugins<LifecycleCallback>().forEach { it.onFlowReleased(id, inputs.matrixClient) }
|
||||
appNavigationStateService.onLeavingSpace(id)
|
||||
appNavigationStateService.onLeavingSession(id)
|
||||
loggedInFlowProcessor.stopObserving()
|
||||
|
|
@ -178,10 +159,10 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object Permanent : NavTarget
|
||||
data object Permanent : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object RoomList : NavTarget
|
||||
data object RoomList : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class Room(
|
||||
|
|
@ -190,19 +171,19 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object Settings : NavTarget
|
||||
data object Settings : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object CreateRoom : NavTarget
|
||||
data object CreateRoom : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object VerifySession : NavTarget
|
||||
data object VerifySession : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object InviteList : NavTarget
|
||||
data object InviteList : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object Ftue : NavTarget
|
||||
data object Ftue : NavTarget
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
|
|
@ -351,4 +332,3 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
backstack.push(NavTarget.InviteList)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ class NotLoggedInFlowNode @AssistedInject constructor(
|
|||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object OnBoarding : NavTarget
|
||||
data object OnBoarding : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class LoginFlow(
|
||||
|
|
|
|||
|
|
@ -72,15 +72,14 @@ class RootFlowNode @AssistedInject constructor(
|
|||
private val bugReportEntryPoint: BugReportEntryPoint,
|
||||
private val intentResolver: IntentResolver,
|
||||
private val oidcActionFlow: OidcActionFlow,
|
||||
) :
|
||||
BackstackNode<RootFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.SplashScreen,
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins
|
||||
) {
|
||||
) : BackstackNode<RootFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.SplashScreen,
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins
|
||||
) {
|
||||
|
||||
override fun onBuilt() {
|
||||
matrixClientsHolder.restoreWithSavedState(buildContext.savedStateMap)
|
||||
|
|
@ -170,10 +169,10 @@ class RootFlowNode @AssistedInject constructor(
|
|||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object SplashScreen : NavTarget
|
||||
data object SplashScreen : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object NotLoggedInFlow : NavTarget
|
||||
data object NotLoggedInFlow : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class LoggedInFlow(
|
||||
|
|
@ -182,7 +181,7 @@ class RootFlowNode @AssistedInject constructor(
|
|||
) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object BugReport : NavTarget
|
||||
data object BugReport : NavTarget
|
||||
}
|
||||
|
||||
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 {
|
||||
Timber.w("Couldn't find any session, go through SplashScreen")
|
||||
}
|
||||
val inputs = LoggedInFlowNode.Inputs(matrixClient)
|
||||
val callback = object : LoggedInFlowNode.Callback {
|
||||
val inputs = LoggedInAppScopeFlowNode.Inputs(matrixClient)
|
||||
val callback = object : LoggedInAppScopeFlowNode.Callback {
|
||||
override fun onOpenBugReport() {
|
||||
backstack.push(NavTarget.BugReport)
|
||||
}
|
||||
}
|
||||
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.SplashScreen -> splashNode(buildContext)
|
||||
|
|
@ -233,6 +232,7 @@ class RootFlowNode @AssistedInject constructor(
|
|||
private suspend fun navigateTo(deeplinkData: DeeplinkData) {
|
||||
Timber.d("Navigating to $deeplinkData")
|
||||
attachSession(deeplinkData.sessionId)
|
||||
.attachSession()
|
||||
.apply {
|
||||
when (deeplinkData) {
|
||||
is DeeplinkData.Root -> attachRoot()
|
||||
|
|
@ -246,7 +246,7 @@ class RootFlowNode @AssistedInject constructor(
|
|||
oidcActionFlow.post(oidcAction)
|
||||
}
|
||||
|
||||
private suspend fun attachSession(sessionId: SessionId): LoggedInFlowNode {
|
||||
private suspend fun attachSession(sessionId: SessionId): LoggedInAppScopeFlowNode {
|
||||
//TODO handle multi-session
|
||||
return waitForChildAttached { navTarget ->
|
||||
navTarget is NavTarget.LoggedInFlow && navTarget.sessionId == sessionId
|
||||
|
|
|
|||
|
|
@ -17,5 +17,5 @@
|
|||
package io.element.android.appnav.loggedin
|
||||
|
||||
// sealed interface LoggedInEvents {
|
||||
// object MyEvent : LoggedInEvents
|
||||
// data object MyEvent : LoggedInEvents
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ import kotlinx.coroutines.flow.stateIn
|
|||
import javax.inject.Inject
|
||||
|
||||
sealed interface LoadingRoomState {
|
||||
object Loading : LoadingRoomState
|
||||
object Error : LoadingRoomState
|
||||
data object Loading : LoadingRoomState
|
||||
data object Error : LoadingRoomState
|
||||
data class Loaded(val room: MatrixRoom) : LoadingRoomState
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -77,10 +77,10 @@ class RoomFlowNode @AssistedInject constructor(
|
|||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object Loading : NavTarget
|
||||
data object Loading : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object Loaded : NavTarget
|
||||
data object Loaded : NavTarget
|
||||
}
|
||||
|
||||
override fun onBuilt() {
|
||||
|
|
|
|||
|
|
@ -152,10 +152,10 @@ class RoomLoadedFlowNode @AssistedInject constructor(
|
|||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object Messages : NavTarget
|
||||
data object Messages : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object RoomDetails : NavTarget
|
||||
data object RoomDetails : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class RoomMemberDetails(val userId: UserId) : NavTarget
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import org.jetbrains.kotlin.cli.common.toBooleanLenient
|
|||
|
||||
buildscript {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
val applicationName: String,
|
||||
val isEnabled: Boolean,
|
||||
val policyUrl: String,
|
||||
val eventSink: (AnalyticsOptInEvents) -> Unit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -28,5 +28,6 @@ open class AnalyticsPreferencesStateProvider : PreviewParameterProvider<Analytic
|
|||
fun aAnalyticsPreferencesState() = AnalyticsPreferencesState(
|
||||
applicationName = "Element X",
|
||||
isEnabled = false,
|
||||
policyUrl = "https://element.io",
|
||||
eventSink = {}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,22 +16,21 @@
|
|||
|
||||
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.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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.PreviewParameter
|
||||
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.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
|
||||
|
||||
@Composable
|
||||
|
|
@ -43,40 +42,33 @@ fun AnalyticsPreferencesView(
|
|||
state.eventSink(AnalyticsOptInEvents.EnableAnalytics(isEnabled = isEnabled))
|
||||
}
|
||||
|
||||
val firstPart = stringResource(id = CommonStrings.screen_analytics_settings_help_us_improve, state.applicationName)
|
||||
val secondPart = buildAnnotatedStringWithColoredPart(
|
||||
val supportingText = stringResource(
|
||||
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_content_link
|
||||
)
|
||||
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
|
||||
CommonStrings.screen_analytics_settings_read_terms_content_link,
|
||||
tagAndLink = LINK_TAG to state.policyUrl,
|
||||
)
|
||||
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
|
||||
|
|
@ -91,5 +83,7 @@ internal fun AnalyticsPreferencesViewDarkPreview(@PreviewParameter(AnalyticsPref
|
|||
|
||||
@Composable
|
||||
private fun ContentToPreview(state: AnalyticsPreferencesState) {
|
||||
AnalyticsPreferencesView(state)
|
||||
AnalyticsPreferencesView(
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,5 @@ dependencies {
|
|||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.test.mockk)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.features.analytics.test)
|
||||
testImplementation(projects.features.analytics.impl)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ package io.element.android.features.analytics.impl
|
|||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.systemBarsPadding
|
||||
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.filled.Poll
|
||||
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.BiasAlignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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.unit.dp
|
||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
||||
import io.element.android.features.analytics.api.Config
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.InfoListItem
|
||||
|
|
@ -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.ButtonSize
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
import io.element.android.libraries.designsystem.theme.temporaryColorBgSpecial
|
||||
import io.element.android.libraries.designsystem.utils.LogCompositions
|
||||
|
|
@ -98,6 +96,8 @@ fun AnalyticsOptInView(
|
|||
)
|
||||
}
|
||||
|
||||
private const val LINK_TAG = "link"
|
||||
|
||||
@Composable
|
||||
private fun AnalyticsOptInHeader(
|
||||
state: AnalyticsOptInState,
|
||||
|
|
@ -114,21 +114,29 @@ private fun AnalyticsOptInHeader(
|
|||
subTitle = stringResource(id = R.string.screen_analytics_prompt_help_us_improve),
|
||||
iconImageVector = Icons.Filled.Poll
|
||||
)
|
||||
Text(
|
||||
text = buildAnnotatedStringWithStyledPart(
|
||||
R.string.screen_analytics_prompt_read_terms,
|
||||
R.string.screen_analytics_prompt_read_terms_content_link,
|
||||
color = Color.Unspecified,
|
||||
underline = false,
|
||||
bold = true,
|
||||
),
|
||||
val text = buildAnnotatedStringWithStyledPart(
|
||||
R.string.screen_analytics_prompt_read_terms,
|
||||
R.string.screen_analytics_prompt_read_terms_content_link,
|
||||
color = Color.Unspecified,
|
||||
underline = false,
|
||||
bold = true,
|
||||
tagAndLink = LINK_TAG to Config.POLICY_LINK,
|
||||
)
|
||||
ClickableText(
|
||||
text = text,
|
||||
onClick = {
|
||||
text
|
||||
.getStringAnnotations(LINK_TAG, it, it)
|
||||
.firstOrNull()
|
||||
?.let { _ -> onClickTerms() }
|
||||
},
|
||||
modifier = Modifier
|
||||
.clip(shape = RoundedCornerShape(8.dp))
|
||||
.clickable { onClickTerms() }
|
||||
.padding(8.dp),
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
style = ElementTheme.typography.fontBodyMdRegular
|
||||
.copy(
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import androidx.compose.runtime.collectAsState
|
|||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
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.AnalyticsPreferencesState
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
|
|
@ -51,6 +52,7 @@ class DefaultAnalyticsPreferencesPresenter @Inject constructor(
|
|||
return AnalyticsPreferencesState(
|
||||
applicationName = buildMeta.applicationName,
|
||||
isEnabled = isEnabled.value,
|
||||
policyUrl = Config.POLICY_LINK,
|
||||
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_content_link">"hier"</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>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ import app.cash.molecule.moleculeFlow
|
|||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
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.services.analytics.test.FakeAnalyticsService
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ import app.cash.molecule.moleculeFlow
|
|||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
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.services.analytics.test.FakeAnalyticsService
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
|
|
@ -39,6 +39,7 @@ class AnalyticsPreferencesPresenterTest {
|
|||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.isEnabled).isTrue()
|
||||
assertThat(initialState.policyUrl).isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ dependencies {
|
|||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(projects.features.analytics.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.mediapickers.test)
|
||||
testImplementation(projects.libraries.mediaupload.test)
|
||||
|
|
|
|||
|
|
@ -63,10 +63,10 @@ class ConfigureRoomFlowNode @AssistedInject constructor(
|
|||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object Root : NavTarget
|
||||
data object Root : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object ConfigureRoom : NavTarget
|
||||
data object ConfigureRoom : NavTarget
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
|
|
|
|||
|
|
@ -54,10 +54,10 @@ class CreateRoomFlowNode @AssistedInject constructor(
|
|||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object Root : NavTarget
|
||||
data object Root : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object NewRoom : NavTarget
|
||||
data object NewRoom : NavTarget
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
|
|
|
|||
|
|
@ -27,5 +27,5 @@ sealed interface ConfigureRoomEvents {
|
|||
data class RemoveFromSelection(val matrixUser: MatrixUser) : ConfigureRoomEvents
|
||||
data class CreateRoom(val config: CreateRoomConfig) : 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 {
|
||||
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 com.google.common.truth.Truth.assertThat
|
||||
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.CreateRoomDataStore
|
||||
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.mediaupload.api.MediaUploadInfo
|
||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import app.cash.molecule.moleculeFlow
|
|||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
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.FakeUserListPresenterFactory
|
||||
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.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.usersearch.test.FakeUserRepository
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ dependencies {
|
|||
implementation(projects.anvilannotations)
|
||||
anvil(projects.anvilcodegen)
|
||||
api(projects.features.ftue.api)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
|
|
@ -49,7 +50,7 @@ dependencies {
|
|||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.features.analytics.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import dagger.assisted.AssistedInject
|
|||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.analytics.api.AnalyticsEntryPoint
|
||||
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.FtueStep
|
||||
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.createNode
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
|
@ -50,7 +52,7 @@ import kotlinx.coroutines.flow.onEach
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
@ContributesNode(SessionScope::class)
|
||||
class FtueFlowNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
|
|
@ -69,13 +71,16 @@ class FtueFlowNode @AssistedInject constructor(
|
|||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object Placeholder : NavTarget
|
||||
data object Placeholder : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object WelcomeScreen : NavTarget
|
||||
data object MigrationScreen : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object AnalyticsOptIn : NavTarget
|
||||
data object WelcomeScreen : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object AnalyticsOptIn : NavTarget
|
||||
}
|
||||
|
||||
private val callback = plugins.filterIsInstance<FtueEntryPoint.Callback>().firstOrNull()
|
||||
|
|
@ -102,6 +107,14 @@ class FtueFlowNode @AssistedInject constructor(
|
|||
NavTarget.Placeholder -> {
|
||||
createNode<PlaceholderNode>(buildContext)
|
||||
}
|
||||
NavTarget.MigrationScreen -> {
|
||||
val callback = object : MigrationScreenNode.Callback {
|
||||
override fun onMigrationFinished() {
|
||||
lifecycleScope.launch { moveToNextStep() }
|
||||
}
|
||||
}
|
||||
createNode<MigrationScreenNode>(buildContext, listOf(callback))
|
||||
}
|
||||
NavTarget.WelcomeScreen -> {
|
||||
val callback = object : WelcomeNode.Callback {
|
||||
override fun onContinueClicked() {
|
||||
|
|
@ -117,12 +130,15 @@ class FtueFlowNode @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun moveToNextStep() {
|
||||
private fun moveToNextStep() {
|
||||
when (ftueState.getNextStep()) {
|
||||
is FtueStep.WelcomeScreen -> {
|
||||
FtueStep.MigrationScreen -> {
|
||||
backstack.newRoot(NavTarget.MigrationScreen)
|
||||
}
|
||||
FtueStep.WelcomeScreen -> {
|
||||
backstack.newRoot(NavTarget.WelcomeScreen)
|
||||
}
|
||||
is FtueStep.AnalyticsOptIn -> {
|
||||
FtueStep.AnalyticsOptIn -> {
|
||||
backstack.replace(NavTarget.AnalyticsOptIn)
|
||||
}
|
||||
null -> callback?.onFtueFlowFinished()
|
||||
|
|
|
|||
|
|
@ -14,57 +14,40 @@
|
|||
* 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.ui.Modifier
|
||||
import com.bumble.appyx.core.composable.Children
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.architecture.BackstackNode
|
||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class PollFlowNode @AssistedInject constructor(
|
||||
class MigrationScreenNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
) : BackstackNode<PollFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Root,
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
) {
|
||||
private val presenter: MigrationScreenPresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object Root : NavTarget
|
||||
interface Callback : Plugin {
|
||||
fun onMigrationFinished()
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
NavTarget.Root -> {
|
||||
createNode(buildContext)
|
||||
}
|
||||
}
|
||||
private fun onMigrationFinished() {
|
||||
plugins.filterIsInstance<Callback>().forEach { it.onMigrationFinished() }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
Children(
|
||||
navModel = backstack,
|
||||
modifier = modifier,
|
||||
transitionHandler = rememberDefaultTransitionHandler(),
|
||||
val state = presenter.present()
|
||||
MigrationScreenView(
|
||||
state,
|
||||
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 com.squareup.anvil.annotations.ContributesBinding
|
||||
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.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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
|
@ -30,11 +32,13 @@ import kotlinx.coroutines.flow.onEach
|
|||
import kotlinx.coroutines.runBlocking
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultFtueState @Inject constructor(
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val welcomeScreenState: WelcomeScreenState,
|
||||
private val migrationScreenStore: MigrationScreenStore,
|
||||
private val matrixClient: MatrixClient,
|
||||
) : FtueState {
|
||||
|
||||
override val shouldDisplayFlow = MutableStateFlow(isAnyStepIncomplete())
|
||||
|
|
@ -42,6 +46,7 @@ class DefaultFtueState @Inject constructor(
|
|||
override suspend fun reset() {
|
||||
welcomeScreenState.reset()
|
||||
analyticsService.reset()
|
||||
migrationScreenStore.reset()
|
||||
}
|
||||
|
||||
init {
|
||||
|
|
@ -52,7 +57,10 @@ class DefaultFtueState @Inject constructor(
|
|||
|
||||
fun getNextStep(currentStep: FtueStep? = null): FtueStep? =
|
||||
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 -> if (needsAnalyticsOptIn()) FtueStep.AnalyticsOptIn else getNextStep(
|
||||
|
|
@ -63,11 +71,16 @@ class DefaultFtueState @Inject constructor(
|
|||
|
||||
private fun isAnyStepIncomplete(): Boolean {
|
||||
return listOf(
|
||||
shouldDisplayMigrationScreen(),
|
||||
shouldDisplayWelcomeScreen(),
|
||||
needsAnalyticsOptIn()
|
||||
).any { it }
|
||||
}
|
||||
|
||||
private fun shouldDisplayMigrationScreen(): Boolean {
|
||||
return migrationScreenStore.isMigrationScreenNeeded(matrixClient.sessionId)
|
||||
}
|
||||
|
||||
private fun needsAnalyticsOptIn(): Boolean {
|
||||
// We need this function to not be suspend, so we need to load the value through runBlocking
|
||||
return runBlocking { analyticsService.didAskUserConsent().first().not() }
|
||||
|
|
@ -89,6 +102,7 @@ class DefaultFtueState @Inject constructor(
|
|||
}
|
||||
|
||||
sealed interface FtueStep {
|
||||
object WelcomeScreen : FtueStep
|
||||
object AnalyticsOptIn : FtueStep
|
||||
data object MigrationScreen : 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"?>
|
||||
<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_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>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_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>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_2">"История сообщений для зашифрованных комнат в этом обновлении будет недоступна."</string>
|
||||
<string name="screen_welcome_bullet_3">"Мы будем рады услышать ваше мнение, сообщите нам об этом через страницу настроек."</string>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_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>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_migration_title">"設定您的帳號"</string>
|
||||
<string name="screen_welcome_button">"開始吧!"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_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>
|
||||
|
|
|
|||
|
|
@ -17,11 +17,16 @@
|
|||
package io.element.android.features.ftue.impl
|
||||
|
||||
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.FtueStep
|
||||
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.test.FakeAnalyticsService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
|
|
@ -45,12 +50,14 @@ class DefaultFtueStateTests {
|
|||
fun `given all checks being true, should display flow is false`() = runTest {
|
||||
val welcomeState = FakeWelcomeState()
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val migrationScreenStore = InMemoryMigrationScreenStore()
|
||||
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||
|
||||
val state = createState(coroutineScope, welcomeState, analyticsService)
|
||||
val state = createState(coroutineScope, welcomeState, analyticsService, migrationScreenStore)
|
||||
|
||||
welcomeState.setWelcomeScreenShown()
|
||||
analyticsService.setDidAskUserConsent()
|
||||
migrationScreenStore.setMigrationScreenShown(A_SESSION_ID)
|
||||
state.updateState()
|
||||
|
||||
assertThat(state.shouldDisplayFlow.value).isFalse()
|
||||
|
|
@ -63,16 +70,21 @@ class DefaultFtueStateTests {
|
|||
fun `traverse flow`() = runTest {
|
||||
val welcomeState = FakeWelcomeState()
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val migrationScreenStore = InMemoryMigrationScreenStore()
|
||||
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||
|
||||
val state = createState(coroutineScope, welcomeState, analyticsService)
|
||||
val state = createState(coroutineScope, welcomeState, analyticsService, migrationScreenStore)
|
||||
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()))
|
||||
welcomeState.setWelcomeScreenShown()
|
||||
|
||||
// Second step, analytics opt in
|
||||
// Third step, analytics opt in
|
||||
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||
analyticsService.setDidAskUserConsent()
|
||||
|
||||
|
|
@ -80,6 +92,7 @@ class DefaultFtueStateTests {
|
|||
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||
|
||||
assertThat(steps).containsExactly(
|
||||
FtueStep.MigrationScreen,
|
||||
FtueStep.WelcomeScreen,
|
||||
FtueStep.AnalyticsOptIn,
|
||||
null, // Final state
|
||||
|
|
@ -93,7 +106,16 @@ class DefaultFtueStateTests {
|
|||
fun `if a check for a step is true, start from the next one`() = runTest {
|
||||
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||
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()
|
||||
assertThat(state.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn)
|
||||
|
|
@ -108,7 +130,14 @@ class DefaultFtueStateTests {
|
|||
private fun createState(
|
||||
coroutineScope: CoroutineScope,
|
||||
welcomeState: FakeWelcomeState = FakeWelcomeState(),
|
||||
analyticsService: AnalyticsService = FakeAnalyticsService()
|
||||
) = DefaultFtueState(coroutineScope, analyticsService, welcomeState)
|
||||
|
||||
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
||||
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.push.test)
|
||||
testImplementation(projects.features.invitelist.test)
|
||||
testImplementation(projects.features.analytics.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,14 +19,12 @@ package io.element.android.features.invitelist.impl
|
|||
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
|
||||
|
||||
sealed interface InviteListEvents {
|
||||
|
||||
data class AcceptInvite(val invite: InviteListInviteSummary) : InviteListEvents
|
||||
data class DeclineInvite(val invite: InviteListInviteSummary) : InviteListEvents
|
||||
|
||||
object ConfirmDeclineInvite: InviteListEvents
|
||||
object CancelDeclineInvite: InviteListEvents
|
||||
|
||||
object DismissAcceptError: InviteListEvents
|
||||
object DismissDeclineError: InviteListEvents
|
||||
data object ConfirmDeclineInvite: InviteListEvents
|
||||
data object CancelDeclineInvite: InviteListEvents
|
||||
|
||||
data object DismissAcceptError: InviteListEvents
|
||||
data object DismissDeclineError: InviteListEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,6 @@ data class InviteListState(
|
|||
)
|
||||
|
||||
sealed interface InviteDeclineConfirmationDialog {
|
||||
object Hidden : InviteDeclineConfirmationDialog
|
||||
data object Hidden : 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.turbine.test
|
||||
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.test.FakeSeenInvitesStore
|
||||
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.test.notifications.FakeNotificationDrawerManager
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||
|
||||
sealed interface LeaveRoomEvent {
|
||||
data class ShowConfirmation(val roomId: RoomId) : LeaveRoomEvent
|
||||
object HideConfirmation : LeaveRoomEvent
|
||||
data object HideConfirmation : 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 = {},
|
||||
) {
|
||||
sealed interface Confirmation {
|
||||
object Hidden : Confirmation
|
||||
data object Hidden : Confirmation
|
||||
data class Generic(val roomId: RoomId) : Confirmation
|
||||
data class PrivateRoom(val roomId: RoomId) : Confirmation
|
||||
data class LastUserInRoom(val roomId: RoomId) : Confirmation
|
||||
}
|
||||
|
||||
sealed interface Progress {
|
||||
object Hidden : Progress
|
||||
object Shown : Progress
|
||||
data object Hidden : Progress
|
||||
data object Shown : Progress
|
||||
}
|
||||
|
||||
sealed interface Error {
|
||||
object Hidden : Error
|
||||
object Shown : Error
|
||||
data object Hidden : Error
|
||||
data object Shown : Error
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,6 @@ dependencies {
|
|||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.features.analytics.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.features.messages.test)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,5 +17,5 @@
|
|||
package io.element.android.features.location.impl.common.permissions
|
||||
|
||||
sealed interface PermissionsEvents {
|
||||
object RequestPermissions : PermissionsEvents
|
||||
data object RequestPermissions : PermissionsEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,9 +22,9 @@ data class PermissionsState(
|
|||
val eventSink: (PermissionsEvents) -> Unit = {},
|
||||
) {
|
||||
sealed interface Permissions {
|
||||
object AllGranted : Permissions
|
||||
object SomeGranted : Permissions
|
||||
object NoneGranted : Permissions
|
||||
data object AllGranted : Permissions
|
||||
data object SomeGranted : Permissions
|
||||
data object NoneGranted : Permissions
|
||||
}
|
||||
|
||||
val isAnyGranted: Boolean
|
||||
|
|
|
|||
|
|
@ -30,13 +30,9 @@ sealed interface SendLocationEvents {
|
|||
)
|
||||
}
|
||||
|
||||
object SwitchToMyLocationMode : SendLocationEvents
|
||||
|
||||
object SwitchToPinLocationMode : SendLocationEvents
|
||||
|
||||
object DismissDialog : SendLocationEvents
|
||||
|
||||
object RequestPermissions : SendLocationEvents
|
||||
|
||||
object OpenAppSettings : SendLocationEvents
|
||||
data object SwitchToMyLocationMode : SendLocationEvents
|
||||
data object SwitchToPinLocationMode : SendLocationEvents
|
||||
data object DismissDialog : SendLocationEvents
|
||||
data object RequestPermissions : SendLocationEvents
|
||||
data object OpenAppSettings : SendLocationEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,13 +24,13 @@ data class SendLocationState(
|
|||
val eventSink: (SendLocationEvents) -> Unit = {},
|
||||
) {
|
||||
sealed interface Mode {
|
||||
object SenderLocation : Mode
|
||||
object PinLocation : Mode
|
||||
data object SenderLocation : Mode
|
||||
data object PinLocation : Mode
|
||||
}
|
||||
|
||||
sealed interface Dialog {
|
||||
object None : Dialog
|
||||
object PermissionRationale : Dialog
|
||||
object PermissionDenied : Dialog
|
||||
data object None : Dialog
|
||||
data object PermissionRationale : Dialog
|
||||
data object PermissionDenied : Dialog
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,11 +17,9 @@
|
|||
package io.element.android.features.location.impl.show
|
||||
|
||||
sealed interface ShowLocationEvents {
|
||||
object Share : ShowLocationEvents
|
||||
data object Share : ShowLocationEvents
|
||||
data class TrackMyLocation(val enabled: Boolean) : ShowLocationEvents
|
||||
object DismissDialog : ShowLocationEvents
|
||||
|
||||
object RequestPermissions : ShowLocationEvents
|
||||
|
||||
object OpenAppSettings : ShowLocationEvents
|
||||
data object DismissDialog : ShowLocationEvents
|
||||
data object RequestPermissions : ShowLocationEvents
|
||||
data object OpenAppSettings : ShowLocationEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ data class ShowLocationState(
|
|||
val eventSink: (ShowLocationEvents) -> Unit,
|
||||
) {
|
||||
sealed interface Dialog {
|
||||
object None : Dialog
|
||||
object PermissionRationale : Dialog
|
||||
object PermissionDenied : Dialog
|
||||
data object None : Dialog
|
||||
data object PermissionRationale : Dialog
|
||||
data object PermissionDenied : Dialog
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import app.cash.molecule.moleculeFlow
|
|||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth
|
||||
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.impl.common.actions.FakeLocationActions
|
||||
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.SendLocationInvocation
|
||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
|
|
|||
|
|
@ -17,6 +17,6 @@
|
|||
package io.element.android.features.login.api.oidc
|
||||
|
||||
sealed interface OidcAction {
|
||||
object GoBack : OidcAction
|
||||
data object GoBack : OidcAction
|
||||
data class Success(val url: String) : OidcAction
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ plugins {
|
|||
alias(libs.plugins.anvil)
|
||||
alias(libs.plugins.ksp)
|
||||
id("kotlin-parcelize")
|
||||
kotlin("plugin.serialization") version "1.9.0"
|
||||
kotlin("plugin.serialization") version "1.9.10"
|
||||
}
|
||||
|
||||
android {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
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.node.Node
|
||||
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.AssistedInject
|
||||
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.oidc.CustomTabAvailabilityChecker
|
||||
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.matrix.api.auth.OidcDetails
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
|
|
@ -61,6 +67,7 @@ class LoginFlowNode @AssistedInject constructor(
|
|||
private val customTabHandler: CustomTabHandler,
|
||||
private val accountProviderDataSource: AccountProviderDataSource,
|
||||
private val defaultLoginUserStory: DefaultLoginUserStory,
|
||||
private val oidcActionFlow: OidcActionFlow,
|
||||
) : BackstackNode<LoginFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.ConfirmAccountProvider,
|
||||
|
|
@ -78,23 +85,40 @@ class LoginFlowNode @AssistedInject constructor(
|
|||
|
||||
private val inputs: Inputs = inputs()
|
||||
|
||||
private var customChromeTabStarted = false
|
||||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
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 {
|
||||
@Parcelize
|
||||
object ConfirmAccountProvider : NavTarget
|
||||
data object ConfirmAccountProvider : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object ChangeAccountProvider : NavTarget
|
||||
data object ChangeAccountProvider : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object SearchAccountProvider : NavTarget
|
||||
data object SearchAccountProvider : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object LoginPassword : NavTarget
|
||||
data object LoginPassword : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class WaitList(val loginFormState: LoginFormState) : NavTarget
|
||||
|
|
@ -113,7 +137,10 @@ class LoginFlowNode @AssistedInject constructor(
|
|||
override fun onOidcDetails(oidcDetails: OidcDetails) {
|
||||
if (customTabAvailabilityChecker.supportCustomTab()) {
|
||||
// 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 {
|
||||
// Fallback to WebView mode
|
||||
backstack.push(NavTarget.OidcView(oidcDetails))
|
||||
|
|
|
|||
|
|
@ -20,5 +20,5 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider
|
|||
|
||||
sealed interface ChangeServerEvents {
|
||||
data class ChangeServer(val accountProvider: AccountProvider) : ChangeServerEvents
|
||||
object ClearError : ChangeServerEvents
|
||||
data object ClearError : ChangeServerEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ sealed class ChangeServerError : Throwable() {
|
|||
@Composable
|
||||
fun message(): String = stringResource(messageId)
|
||||
}
|
||||
object SlidingSyncAlert : ChangeServerError()
|
||||
data object SlidingSyncAlert : ChangeServerError()
|
||||
|
||||
companion object {
|
||||
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
|
||||
|
||||
sealed interface OidcEvents {
|
||||
object Cancel : OidcEvents
|
||||
data object Cancel : 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
|
||||
|
||||
sealed interface ConfirmAccountProviderEvents {
|
||||
object Continue : ConfirmAccountProviderEvents
|
||||
object ClearError : ConfirmAccountProviderEvents
|
||||
data object Continue : ConfirmAccountProviderEvents
|
||||
data object ClearError : ConfirmAccountProviderEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,9 +68,9 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
|
|||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
launch {
|
||||
defaultOidcActionFlow.collect {
|
||||
onOidcAction(it, loginFlowAction)
|
||||
defaultOidcActionFlow.collect { oidcAction ->
|
||||
if (oidcAction != null) {
|
||||
onOidcAction(oidcAction, loginFlowAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -113,10 +113,9 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private suspend fun onOidcAction(
|
||||
oidcAction: OidcAction?,
|
||||
oidcAction: OidcAction,
|
||||
loginFlowAction: MutableState<Async<LoginFlow>>,
|
||||
) {
|
||||
oidcAction ?: return
|
||||
loginFlowAction.value = Async.Loading()
|
||||
when (oidcAction) {
|
||||
OidcAction.GoBack -> {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,6 @@ data class ConfirmAccountProviderState(
|
|||
}
|
||||
|
||||
sealed interface LoginFlow {
|
||||
object PasswordLogin : LoginFlow
|
||||
data object PasswordLogin : LoginFlow
|
||||
data class OidcFlow(val oidcDetails: OidcDetails) : LoginFlow
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,6 @@ package io.element.android.features.login.impl.screens.loginpassword
|
|||
sealed interface LoginPasswordEvents {
|
||||
data class SetLogin(val login: String) : LoginPasswordEvents
|
||||
data class SetPassword(val password: String) : LoginPasswordEvents
|
||||
object Submit : LoginPasswordEvents
|
||||
object ClearError : LoginPasswordEvents
|
||||
data object Submit : LoginPasswordEvents
|
||||
data object ClearError : LoginPasswordEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
package io.element.android.features.login.impl.screens.waitlistscreen
|
||||
|
||||
sealed interface WaitListEvents {
|
||||
object AttemptLogin : WaitListEvents
|
||||
object ClearError : WaitListEvents
|
||||
object Continue : WaitListEvents
|
||||
data object AttemptLogin : WaitListEvents
|
||||
data object ClearError : 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