Merge branch 'release/0.1.4' into main

This commit is contained in:
Benoit Marty 2023-08-28 15:34:49 +02:00
commit cff8df44a6
2064 changed files with 8670 additions and 4141 deletions

View file

@ -1,4 +1,4 @@
<!-- Please read [CONTRIBUTING.md](https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->
<!-- Please read [CONTRIBUTING.md](https://github.com/vector-im/element-x-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->
## Type of change
@ -17,13 +17,17 @@
## Screenshots / GIFs
<!-- Only if UI have been changed
<!--
We have screenshot tests in the project, so attaching screenshots to a PR is not mandatory, as far as there
is a Composable Preview covering the changes. In this case, the change will appear in the file diff.
Note that all the UI composables should be covered by a Composable Preview.
Providing a video of the change is still very useful for the reviewer and for the history of the project.
You can use a table like this to show screenshots comparison.
Uncomment this markdown table below and edit the last line `|||`:
|copy screenshot of before here|copy screenshot of after here|
-->
<!--
|Before|After|
|-|-|
|||
@ -47,11 +51,11 @@ Uncomment this markdown table below and edit the last line `|||`:
<!-- Depending on the Pull Request content, it can be acceptable if some of the following checkboxes stay unchecked. -->
- [ ] Changes has been tested on an Android device or Android emulator with API 21
- [ ] Changes have been tested on an Android device or Android emulator with API 23
- [ ] UI change has been tested on both light and dark themes
- [ ] Accessibility has been taken into account. See https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md#accessibility
- [ ] Accessibility has been taken into account. See https://github.com/vector-im/element-x-android/blob/develop/CONTRIBUTING.md#accessibility
- [ ] Pull request is based on the develop branch
- [ ] Pull request includes a new file under ./changelog.d. See https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md#changelog
- [ ] Pull request includes a new file under ./changelog.d. See https://github.com/vector-im/element-x-android/blob/develop/CONTRIBUTING.md#changelog
- [ ] Pull request includes screenshots or videos if containing UI changes
- [ ] Pull request includes a [sign off](https://matrix-org.github.io/synapse/latest/development/contributing_guide.html#sign-off)
- [ ] You've made a self review of your PR

View file

@ -16,8 +16,8 @@ jobs:
debug:
name: Build APKs
runs-on: ubuntu-latest
# Skip for `main` and the merge queue if the branch is up to date with `develop`
if: github.ref != 'refs/heads/main' && github.event.merge_group.base_ref != 'refs/heads/develop'
# Skip for `main`
if: github.ref != 'refs/heads/main'
strategy:
matrix:
variant: [debug, release, nightly, samples]
@ -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.0
uses: gradle/gradle-build-action@v2.7.1
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug APK

View file

@ -5,8 +5,6 @@ on: [pull_request, merge_group]
jobs:
build:
runs-on: ubuntu-latest
# Don't run in the merge queue again if the branch is up to date with `develop`
if: github.event.merge_group.base_ref != 'refs/heads/develop'
name: Danger main check
steps:
- uses: actions/checkout@v3

View file

@ -8,8 +8,6 @@ on:
jobs:
validation:
name: "Validation"
# Don't run in the merge queue again if the branch is up to date with `develop`
if: github.event.merge_group.base_ref != 'refs/heads/develop'
runs-on: ubuntu-latest
# No concurrency required, this is a prerequisite to other actions and should run every time.
steps:

View file

@ -18,7 +18,7 @@ jobs:
if: ${{ github.repository == 'vector-im/element-x-android' }}
steps:
- name: ⏬ Checkout with LFS
uses: nschloe/action-cached-lfs-checkout@v1.2.1
uses: nschloe/action-cached-lfs-checkout@v1.2.2
- name: Use JDK 17
uses: actions/setup-java@v3
@ -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.0
uses: gradle/gradle-build-action@v2.7.1
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Dependency analysis

View file

@ -16,8 +16,6 @@ jobs:
checkScript:
name: Search for forbidden patterns
runs-on: ubuntu-latest
# Don't run in the merge queue again if the branch is up to date with `develop`
if: github.event.merge_group.base_ref != 'refs/heads/develop'
steps:
- uses: actions/checkout@v3
- name: Run code quality check suite
@ -26,8 +24,6 @@ jobs:
check:
name: Project Check Suite
runs-on: ubuntu-latest
# Don't run in the merge queue again if the branch is up to date with `develop`
if: github.event.merge_group.base_ref != 'refs/heads/develop'
# Allow all jobs on main and develop. Just one per PR.
concurrency:
group: ${{ github.ref == 'refs/heads/main' && format('check-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-develop-{0}', github.sha) || format('check-{0}', github.ref) }}
@ -44,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.0
uses: gradle/gradle-build-action@v2.7.1
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run code quality check suite

View file

@ -14,7 +14,7 @@ jobs:
steps:
- name: ⏬ Checkout with LFS
uses: nschloe/action-cached-lfs-checkout@v1.2.1
uses: nschloe/action-cached-lfs-checkout@v1.2.2
with:
persist-credentials: false
- name: ☕️ Use JDK 17
@ -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.0
uses: gradle/gradle-build-action@v2.7.1
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Record screenshots

View file

@ -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.0
uses: gradle/gradle-build-action@v2.7.1
- name: Create app bundle
env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}

View file

@ -16,8 +16,6 @@ jobs:
sonar:
name: Project Check Suite
runs-on: ubuntu-latest
# Don't run in the merge queue again if the branch is up to date with `develop`
if: github.event.merge_group.base_ref != 'refs/heads/develop'
# Allow all jobs on main and develop. Just one per PR.
concurrency:
group: ${{ github.ref == 'refs/heads/main' && format('sonar-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('sonar-develop-{0}', github.sha) || format('sonar-{0}', github.ref) }}
@ -34,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.0
uses: gradle/gradle-build-action@v2.7.1
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: 🔊 Publish results to Sonar

View file

@ -16,8 +16,6 @@ jobs:
tests:
name: Runs unit tests
runs-on: ubuntu-latest
# Don't run in the merge queue again if the branch is up to date with `develop`
if: github.event.merge_group.base_ref != 'refs/heads/develop'
# Allow all jobs on main and develop. Just one per PR.
concurrency:
@ -25,7 +23,7 @@ jobs:
cancel-in-progress: true
steps:
- name: ⏬ Checkout with LFS
uses: nschloe/action-cached-lfs-checkout@v1.2.1
uses: nschloe/action-cached-lfs-checkout@v1.2.2
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
@ -36,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.0
uses: gradle/gradle-build-action@v2.7.1
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
@ -57,22 +55,6 @@ jobs:
path: |
**/kover/merged/verification/errors.txt
- name: 📸 Upload Screenshot test report
uses: actions/upload-artifact@v3
if: always()
with:
name: reports
path: tests/uitests/build/reports/tests/testDebugUnitTest/
retention-days: 5
- name: 🚫 Upload Screenshot failure differences on error
uses: actions/upload-artifact@v3
if: failure()
with:
name: failures
path: tests/uitests/out/failures/
retention-days: 5
- name: ✅ Upload kover report (disabled)
if: always()
run: echo "This is now done only once a day, see nightlyReports.yml"
@ -83,7 +65,7 @@ jobs:
with:
name: tests-and-screenshot-tests-results
path: |
**/out/failures/
**/build/paparazzi/failures/
**/build/reports/tests/*UnitTest/
# https://github.com/codecov/codecov-action

View file

@ -5,11 +5,9 @@ on: [pull_request, merge_group]
jobs:
build:
runs-on: ubuntu-latest
# Don't run in the merge queue again if the branch is up to date with `develop`
if: github.event.merge_group.base_ref != 'refs/heads/develop'
name: Validate
steps:
- uses: nschloe/action-cached-lfs-checkout@v1.2.1
- uses: nschloe/action-cached-lfs-checkout@v1.2.2
- run: |
./tools/git/validate_lfs.sh

2
.idea/kotlinc.xml generated
View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.8.22" />
<option name="version" value="1.9.0" />
</component>
</project>

View file

@ -1,5 +1,7 @@
appId: ${APP_ID}
---
## Check that all env variables required in the whole test suite are declared (to fail faster)
- runScript: ./scripts/checkEnv.js
- runFlow: tests/init.yaml
- runFlow: tests/account/login.yaml
- runFlow: tests/settings/settings.yaml

View file

@ -0,0 +1,9 @@
// This array contains all the required environment variable. When adding a variable, add it here also.
// If a variable is missing, an error will occur.
if (APP_ID == null) throw "Fatal: missing env variable APP_ID"
if (USERNAME == null) throw "Fatal: missing env variable USERNAME"
if (PASSWORD == null) throw "Fatal: missing env variable PASSWORD"
if (ROOM_NAME == null) throw "Fatal: missing env variable ROOM_NAME"
if (INVITEE1_MXID == null) throw "Fatal: missing env variable INVITEE1_MXID"
if (INVITEE2_MXID == null) throw "Fatal: missing env variable INVITEE2_MXID"

View file

@ -9,9 +9,13 @@ appId: ${APP_ID}
- tapOn: "Other"
- tapOn:
id: "change_server-server"
- inputText: "element"
# Test server that does not support sliding sync.
- inputText: "gnuradio"
- hideKeyboard
- tapOn: "element.io"
- tapOn: "gnuradio.org"
- extendedWaitUntil:
visible: "This server currently doesnt support sliding sync."
timeout: 10_000
- tapOn: "Cancel"
- back
- back

View file

@ -17,7 +17,7 @@ appId: ${APP_ID}
- takeScreenshot: build/maestro/320-createAndDeleteRoom
- tapOn: "aRoomName"
- tapOn: "Invite people"
# assert there's 1 memeber and 1 invitee
# assert there's 1 member and 1 invitee
- tapOn: "Search for someone"
- inputText: ${INVITEE2_MXID}
- tapOn:
@ -27,7 +27,7 @@ appId: ${APP_ID}
- tapOn: "Back"
- tapOn: "aRoomName"
- tapOn: "People"
# assert there's 1 memeber and 2 invitees
# assert there's 1 member and 2 invitees
- tapOn: "Back"
- tapOn: "Leave room"
- tapOn: "Leave"

View file

@ -7,6 +7,7 @@ appId: ${APP_ID}
- tapOn: ${ROOM_NAME}
# Back from timeline
- back
- assertVisible: "MyR"
# Close keyboard
- hideKeyboard
# Back from search

View file

@ -1,3 +1,36 @@
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)
========================================

View file

@ -1,4 +1,4 @@
# Contributing to Element Android
# Contributing to Element X Android
<!--- TOC -->

View file

@ -37,8 +37,6 @@ plugins {
android {
namespace = "io.element.android.x"
testOptions { unitTests.isIncludeAndroidResources = true }
defaultConfig {
applicationId = "io.element.android.x"
targetSdk = Versions.targetSdk

View file

@ -35,8 +35,7 @@
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
android:exported="false">
<meta-data
android:name='androidx.lifecycle.ProcessLifecycleInitializer'

View file

@ -27,7 +27,7 @@ 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.LoggedInAppScopeFlowNode
import io.element.android.appnav.room.RoomLoadedFlowNode
import io.element.android.appnav.RootFlowNode
import io.element.android.libraries.architecture.bindings
@ -56,7 +56,7 @@ class MainNode(
),
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)

View file

@ -0,0 +1,124 @@
/*
* 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.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
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.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.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>,
) : BackstackNode<LoggedInAppScopeFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Root,
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
plugins = plugins
) {
interface Callback : Plugin {
fun onOpenBugReport()
}
sealed interface NavTarget : Parcelable {
@Parcelize
data object Root : NavTarget
}
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 {
return when (navTarget) {
NavTarget.Root -> {
val callback = object : LoggedInFlowNode.Callback {
override fun onOpenBugReport() {
plugins<Callback>().forEach { it.onOpenBugReport() }
}
}
createNode<LoggedInFlowNode>(buildContext, listOf(callback))
}
}
}
suspend fun attachSession(): LoggedInFlowNode {
return waitForChildAttached { navTarget ->
navTarget is NavTarget.Root
}
}
@Composable
override fun View(modifier: Modifier) {
Children(
navModel = backstack,
modifier = modifier,
transitionHandler = rememberDefaultTransitionHandler(),
)
}
}

View file

@ -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)
}
}

View file

@ -64,7 +64,7 @@ class NotLoggedInFlowNode @AssistedInject constructor(
sealed interface NavTarget : Parcelable {
@Parcelize
object OnBoarding : NavTarget
data object OnBoarding : NavTarget
@Parcelize
data class LoginFlow(

View file

@ -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

View file

@ -17,5 +17,5 @@
package io.element.android.appnav.loggedin
// sealed interface LoggedInEvents {
// object MyEvent : LoggedInEvents
// data object MyEvent : LoggedInEvents
// }

View file

@ -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
}

View file

@ -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() {

View file

@ -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

View file

@ -1,9 +1,11 @@
import com.google.devtools.ksp.gradle.KspTask
import kotlinx.kover.api.KoverTaskExtension
import org.apache.tools.ant.taskdefs.optional.ReplaceRegExp
import org.jetbrains.kotlin.cli.common.toBooleanLenient
buildscript {
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.22")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0")
classpath("com.google.gms:google-services:4.3.15")
}
}
@ -34,6 +36,7 @@ plugins {
alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.kapt) apply false
alias(libs.plugins.dependencycheck) apply false
alias(libs.plugins.dependencyanalysis)
alias(libs.plugins.detekt)
alias(libs.plugins.ktlint)
alias(libs.plugins.dependencygraph)
@ -59,7 +62,7 @@ allprojects {
config.from(files("$rootDir/tools/detekt/detekt.yml"))
}
dependencies {
detektPlugins("io.nlopez.compose.rules:detekt:0.1.12")
detektPlugins("io.nlopez.compose.rules:detekt:0.2.1")
}
// KtLint
@ -98,6 +101,22 @@ allprojects {
// Or add a line with "allWarningsAsErrors=true" in your ~/.gradle/gradle.properties file
kotlinOptions.allWarningsAsErrors = project.properties["allWarningsAsErrors"] == "true"
}
// Detect unused dependencies
apply {
plugin("com.autonomousapps.dependency-analysis")
}
}
// See https://github.com/autonomousapps/dependency-analysis-android-gradle-plugin/wiki/Customizing-plugin-behavior
dependencyAnalysis {
issues {
all {
onUnusedDependencies {
exclude("com.jakewharton.timber:timber")
}
}
}
}
// To run a sonar analysis:
@ -150,7 +169,7 @@ allprojects {
maxHeapSize = "1g"
} else {
// Disable screenshot tests by default
exclude("**/ScreenshotTest*")
exclude("ui/S.class")
}
}
}
@ -325,6 +344,7 @@ tasks.register("runQualityChecks") {
tasks.findByPath("$path:lint")?.let { dependsOn(it) }
tasks.findByName("detekt")?.let { dependsOn(it) }
tasks.findByName("ktlintCheck")?.let { dependsOn(it) }
// tasks.findByName("buildHealth")?.let { dependsOn(it) }
}
dependsOn(":app:knitCheck")
}
@ -343,3 +363,21 @@ subprojects {
tasks.findByName("recordPaparazziDebug")?.dependsOn(removeOldScreenshotsTask)
tasks.findByName("recordPaparazziRelease")?.dependsOn(removeOldScreenshotsTask)
}
// Workaround for https://github.com/airbnb/Showkase/issues/335
subprojects {
tasks.withType<KspTask>() {
doLast {
fileTree(buildDir).apply { include("**/*ShowkaseExtension*.kt") }.files.forEach { file ->
ReplaceRegExp().apply {
setMatch("^public fun Showkase.getMetadata")
setReplace("@Suppress(\"DEPRECATION\") public fun Showkase.getMetadata")
setFlags("g")
setByLine(true)
setFile(file)
execute()
}
}
}
}
}

View file

@ -45,3 +45,7 @@ state: ex6mNJVFZ5jn9wL8
Oidc client example: https://github.com/matrix-org/matrix-rust-sdk/blob/39ad8a46801fb4317a777ebf895822b3675b709c/examples/oidc_cli/src/main.rs
Oidc sdk doc: https://github.com/matrix-org/matrix-rust-sdk/blob/39ad8a46801fb4317a777ebf895822b3675b709c/crates/matrix-sdk/src/oidc.rs
Test server:
synapse-oidc.lab.element.dev

View file

@ -58,7 +58,7 @@ Paparazzi will generate images in `:tests:uitests/src/test/snapshots`, which wil
./gradlew verifyPaparazziDebug
```
In the case of failure, Paparazzi will generate images in `:tests:uitests/out/failure`. The images will show the expected and actual screenshots along with a delta of the two images.
In the case of failure, Paparazzi will generate images in `:tests:uitests/build/paparazzi/failures`. The images will show the expected and actual screenshots along with a delta of the two images.
## Contributing

View 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

View file

@ -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)
}

View file

@ -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>

View file

@ -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_analytics_prompt_settings">"您可以在任何時候關閉它"</string>
<string name="screen_analytics_prompt_third_party_sharing">"我們不會和第三方分享您的資料"</string>
</resources>

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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 {

View file

@ -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 {

View file

@ -22,7 +22,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.theme.components.Divider
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.matrix.ui.components.CheckableMatrixUserRow
import io.element.android.libraries.matrix.ui.components.CheckableUnresolvedUserRow
import io.element.android.libraries.matrix.ui.components.aMatrixUser
@ -63,11 +63,11 @@ internal fun SearchMultipleUsersResultItemPreview() = ElementThemedPreview { Con
private fun ContentToPreview() {
Column {
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false), isUserSelected = false)
Divider()
HorizontalDivider()
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false), isUserSelected = true)
Divider()
HorizontalDivider()
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true), isUserSelected = false)
Divider()
HorizontalDivider()
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true), isUserSelected = true)
}
}

View file

@ -23,7 +23,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.theme.components.Divider
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
import io.element.android.libraries.matrix.ui.components.UnresolvedUserRow
import io.element.android.libraries.matrix.ui.components.aMatrixUser
@ -59,7 +59,7 @@ internal fun SearchSingleUserResultItemPreview() = ElementThemedPreview { Conten
private fun ContentToPreview() {
Column {
SearchSingleUserResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false))
Divider()
HorizontalDivider()
SearchSingleUserResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true))
}
}

View file

@ -35,7 +35,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.theme.components.Divider
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.designsystem.theme.components.SearchBar
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.user.MatrixUser
@ -117,7 +117,7 @@ fun SearchUserBar(
}
)
if (index < users.lastIndex) {
Divider()
HorizontalDivider()
}
}
} else {
@ -128,7 +128,7 @@ fun SearchUserBar(
onClick = { onUserSelected(searchResult.matrixUser) }
)
if (index < users.lastIndex) {
Divider()
HorizontalDivider()
}
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -1,6 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_room_action_create_room">"建立聊天室"</string>
<string name="screen_create_room_action_invite_people">"邀請朋友使用 Element"</string>
<string name="screen_create_room_add_people_title">"邀請夥伴"</string>
<string name="screen_create_room_error_creating_room">"建立聊天室時發生錯誤"</string>
<string name="screen_create_room_room_name_label">"聊天室名稱"</string>
<string name="screen_create_room_topic_label">"主題(非必填)"</string>
<string name="screen_create_room_title">"建立聊天室"</string>

View file

@ -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

View file

@ -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

View file

@ -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)
}

View file

@ -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()

View file

@ -0,0 +1,53 @@
/*
* 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.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
class MigrationScreenNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: MigrationScreenPresenter,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun onMigrationFinished()
}
private fun onMigrationFinished() {
plugins.filterIsInstance<Callback>().forEach { it.onMigrationFinished() }
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
MigrationScreenView(
state,
onMigrationFinished = ::onMigrationFinished,
modifier = modifier
)
}
}

View file

@ -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
)
}
}

View file

@ -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
)

View file

@ -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()
}

View file

@ -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 = {})
}

View file

@ -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_"
}
}

View file

@ -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
}

View 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>

View file

@ -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>

View file

@ -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 na besoin dêtre fait quune seule fois, merci de patienter."</string>
<string name="screen_migration_title">"Configuration de votre compte."</string>
<string name="screen_welcome_bullet_2">"Lhistorique 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 davoir votre avis, nhésitez pas à nous le partager via la page des paramètres."</string>
<string name="screen_welcome_button">"Cest parti !"</string>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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 wont be available in this update."</string>
<string name="screen_welcome_bullet_3">"Wed love to hear from you, let us know what you think via the settings page."</string>

View file

@ -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,
)
}

View file

@ -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()
}
}

View file

@ -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,
)
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -43,7 +43,7 @@ import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
import io.element.android.libraries.designsystem.theme.components.Divider
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
@ -161,7 +161,7 @@ fun InviteListContent(
)
if (index != state.inviteList.lastIndex) {
Divider()
HorizontalDivider()
}
}
}

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invites_invited_you">"%1$s%2$s邀請您"</string>
</resources>

View file

@ -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

View file

@ -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
}

View file

@ -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
}
}

View file

@ -111,7 +111,8 @@ fun StaticMapView(
StaticMapPlaceholder(
showProgress = painter.state is AsyncImagePainter.State.Loading,
contentDescription = contentDescription,
modifier = Modifier.size(width = maxWidth, height = maxHeight),
width = maxWidth,
height = maxHeight,
onLoadMapClick = { retryHash++ }
)
}

View file

@ -31,6 +31,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import io.element.android.features.location.api.R
import io.element.android.libraries.designsystem.preview.DayNightPreviews
@ -44,34 +45,34 @@ import io.element.android.libraries.ui.strings.CommonStrings
internal fun StaticMapPlaceholder(
showProgress: Boolean,
contentDescription: String?,
width: Dp,
height: Dp,
modifier: Modifier = Modifier,
onLoadMapClick: () -> Unit,
) {
Box(
contentAlignment = Alignment.Center,
modifier = modifier
.size(width = width, height = height)
.then(if (showProgress) Modifier else Modifier.clickable(onClick = onLoadMapClick))
) {
Image(
painter = painterResource(id = R.drawable.blurred_map),
contentDescription = contentDescription,
modifier = modifier,
contentScale = ContentScale.FillBounds,
modifier = Modifier.size(width = width, height = height)
)
if (showProgress) {
CircularProgressIndicator()
} else {
Box(
modifier = modifier.clickable(onClick = onLoadMapClick),
contentAlignment = Alignment.Center,
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
imageVector = Icons.Default.Refresh,
contentDescription = null
)
Text(text = stringResource(id = CommonStrings.action_static_map_load))
}
Icon(
imageVector = Icons.Default.Refresh,
contentDescription = null
)
Text(text = stringResource(id = CommonStrings.action_static_map_load))
}
}
}
@ -85,7 +86,8 @@ internal fun StaticMapPlaceholderPreview(
StaticMapPlaceholder(
showProgress = values,
contentDescription = null,
modifier = Modifier.size(400.dp),
width = 400.dp,
height = 400.dp,
onLoadMapClick = {},
)
}

View file

@ -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)
}

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.location.impl
package io.element.android.features.location.impl.common
import android.Manifest
import android.view.Gravity

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.location.impl.common
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
internal fun PermissionDeniedDialog(
onContinue: () -> Unit,
onDismiss: () -> Unit,
appName: String,
) {
ConfirmationDialog(
content = stringResource(CommonStrings.error_missing_location_auth_android, appName),
onSubmitClicked = onContinue,
onDismiss = onDismiss,
submitText = stringResource(CommonStrings.action_continue),
cancelText = stringResource(CommonStrings.action_cancel),
)
}

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.location.impl.common
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
internal fun PermissionRationaleDialog(
onContinue: () -> Unit,
onDismiss: () -> Unit,
appName: String,
) {
ConfirmationDialog(
content = stringResource(CommonStrings.error_missing_location_rationale_android, appName),
onSubmitClicked = onContinue,
onDismiss = onDismiss,
submitText = stringResource(CommonStrings.action_continue),
cancelText = stringResource(CommonStrings.action_cancel),
)
}

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.location.impl
package io.element.android.features.location.impl.common.actions
import android.content.Context
import android.content.Intent
@ -22,7 +22,6 @@ import android.net.Uri
import androidx.annotation.VisibleForTesting
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.location.api.Location
import io.element.android.features.location.impl.show.LocationActions
import io.element.android.libraries.androidutils.system.openAppSettingsPage
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.location.impl.show
package io.element.android.features.location.impl.common.actions
import io.element.android.features.location.api.Location

View file

@ -14,8 +14,8 @@
* limitations under the License.
*/
package io.element.android.features.location.impl.permissions
package io.element.android.features.location.impl.common.permissions
sealed interface PermissionsEvents {
object RequestPermissions : PermissionsEvents
data object RequestPermissions : PermissionsEvents
}

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.location.impl.permissions
package io.element.android.features.location.impl.common.permissions
import io.element.android.libraries.architecture.Presenter

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.location.impl.permissions
package io.element.android.features.location.impl.common.permissions
import androidx.compose.runtime.Composable
import com.google.accompanist.permissions.ExperimentalPermissionsApi

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.location.impl.permissions
package io.element.android.features.location.impl.common.permissions
data class PermissionsState(
val permissions: Permissions = Permissions.NoneGranted,
@ -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

View file

@ -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
}

View file

@ -25,11 +25,11 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import im.vector.app.features.analytics.plan.Composer
import io.element.android.features.location.impl.MapDefaults
import io.element.android.features.location.impl.permissions.PermissionsEvents
import io.element.android.features.location.impl.permissions.PermissionsPresenter
import io.element.android.features.location.impl.permissions.PermissionsState
import io.element.android.features.location.impl.show.LocationActions
import io.element.android.features.location.impl.common.MapDefaults
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
import io.element.android.features.location.impl.common.permissions.PermissionsState
import io.element.android.features.location.impl.common.actions.LocationActions
import io.element.android.features.messages.api.MessageComposerContext
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta

View file

@ -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
}
}

View file

@ -47,10 +47,11 @@ import com.mapbox.mapboxsdk.camera.CameraPosition
import io.element.android.features.location.api.Location
import io.element.android.features.location.api.internal.centerBottomEdge
import io.element.android.features.location.api.internal.rememberTileStyleUrl
import io.element.android.features.location.impl.MapDefaults
import io.element.android.features.location.impl.common.MapDefaults
import io.element.android.features.location.impl.R
import io.element.android.features.location.impl.common.PermissionDeniedDialog
import io.element.android.features.location.impl.common.PermissionRationaleDialog
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
@ -232,33 +233,3 @@ internal fun SendLocationViewPreview(
navigateUp = {},
)
}
@Composable
private fun PermissionRationaleDialog(
onContinue: () -> Unit,
onDismiss: () -> Unit,
appName: String,
) {
ConfirmationDialog(
content = stringResource(CommonStrings.error_missing_location_rationale_android, appName),
onSubmitClicked = onContinue,
onDismiss = onDismiss,
submitText = stringResource(CommonStrings.action_continue),
cancelText = stringResource(CommonStrings.action_cancel),
)
}
@Composable
private fun PermissionDeniedDialog(
onContinue: () -> Unit,
onDismiss: () -> Unit,
appName: String,
) {
ConfirmationDialog(
content = stringResource(CommonStrings.error_missing_location_auth_android, appName),
onSubmitClicked = onContinue,
onDismiss = onDismiss,
submitText = stringResource(CommonStrings.action_continue),
cancelText = stringResource(CommonStrings.action_cancel),
)
}

View file

@ -17,6 +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
data object DismissDialog : ShowLocationEvents
data object RequestPermissions : ShowLocationEvents
data object OpenAppSettings : ShowLocationEvents
}

View file

@ -17,6 +17,8 @@
package io.element.android.features.location.impl.show
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -25,14 +27,18 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.features.location.api.Location
import io.element.android.features.location.impl.MapDefaults
import io.element.android.features.location.impl.permissions.PermissionsPresenter
import io.element.android.features.location.impl.permissions.PermissionsState
import io.element.android.features.location.impl.common.MapDefaults
import io.element.android.features.location.impl.common.actions.LocationActions
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
import io.element.android.features.location.impl.common.permissions.PermissionsState
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta
class ShowLocationPresenter @AssistedInject constructor(
permissionsPresenterFactory: PermissionsPresenter.Factory,
private val actions: LocationActions,
private val locationActions: LocationActions,
private val buildMeta: BuildMeta,
@Assisted private val location: Location,
@Assisted private val description: String?
) : Presenter<ShowLocationState> {
@ -48,19 +54,47 @@ class ShowLocationPresenter @AssistedInject constructor(
override fun present(): ShowLocationState {
val permissionsState: PermissionsState = permissionsPresenter.present()
var isTrackMyLocation by remember { mutableStateOf(false) }
val appName by remember { derivedStateOf { buildMeta.applicationName } }
var permissionDialog: ShowLocationState.Dialog by remember {
mutableStateOf(ShowLocationState.Dialog.None)
}
LaunchedEffect(permissionsState.permissions) {
if (permissionsState.isAnyGranted) {
permissionDialog = ShowLocationState.Dialog.None
}
}
fun handleEvents(event: ShowLocationEvents) {
when (event) {
ShowLocationEvents.Share -> actions.share(location, description)
is ShowLocationEvents.TrackMyLocation -> isTrackMyLocation = event.enabled
ShowLocationEvents.Share -> locationActions.share(location, description)
is ShowLocationEvents.TrackMyLocation -> {
if (event.enabled) {
when {
permissionsState.isAnyGranted -> isTrackMyLocation = true
permissionsState.shouldShowRationale -> permissionDialog = ShowLocationState.Dialog.PermissionRationale
else -> permissionDialog = ShowLocationState.Dialog.PermissionDenied
}
} else {
isTrackMyLocation = false
}
}
ShowLocationEvents.DismissDialog -> permissionDialog = ShowLocationState.Dialog.None
ShowLocationEvents.OpenAppSettings -> {
locationActions.openSettings()
permissionDialog = ShowLocationState.Dialog.None
}
ShowLocationEvents.RequestPermissions -> permissionsState.eventSink(PermissionsEvents.RequestPermissions)
}
}
return ShowLocationState(
permissionDialog = permissionDialog,
location = location,
description = description,
hasLocationPermission = permissionsState.isAnyGranted,
isTrackMyLocation = isTrackMyLocation,
appName = appName,
eventSink = ::handleEvents,
)
}

View file

@ -19,9 +19,17 @@ package io.element.android.features.location.impl.show
import io.element.android.features.location.api.Location
data class ShowLocationState(
val permissionDialog: Dialog,
val location: Location,
val description: String?,
val hasLocationPermission: Boolean,
val isTrackMyLocation: Boolean,
val appName: String,
val eventSink: (ShowLocationEvents) -> Unit,
)
) {
sealed interface Dialog {
data object None : Dialog
data object PermissionRationale : Dialog
data object PermissionDenied : Dialog
}
}

View file

@ -19,50 +19,82 @@ package io.element.android.features.location.impl.show
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.location.api.Location
private const val APP_NAME = "ApplicationName"
class ShowLocationStateProvider : PreviewParameterProvider<ShowLocationState> {
override val values: Sequence<ShowLocationState>
get() = sequenceOf(
ShowLocationState(
ShowLocationState.Dialog.None,
Location(1.23, 2.34, 4f),
description = null,
hasLocationPermission = false,
isTrackMyLocation = false,
appName = APP_NAME,
eventSink = {},
),
ShowLocationState(
ShowLocationState.Dialog.PermissionDenied,
Location(1.23, 2.34, 4f),
description = null,
hasLocationPermission = false,
isTrackMyLocation = false,
appName = APP_NAME,
eventSink = {},
),
ShowLocationState(
ShowLocationState.Dialog.PermissionRationale,
Location(1.23, 2.34, 4f),
description = null,
hasLocationPermission = false,
isTrackMyLocation = false,
appName = APP_NAME,
eventSink = {},
),
ShowLocationState(
ShowLocationState.Dialog.None,
Location(1.23, 2.34, 4f),
description = null,
hasLocationPermission = true,
isTrackMyLocation = false,
appName = APP_NAME,
eventSink = {},
),
ShowLocationState(
ShowLocationState.Dialog.None,
Location(1.23, 2.34, 4f),
description = null,
hasLocationPermission = true,
isTrackMyLocation = true,
appName = APP_NAME,
eventSink = {},
),
ShowLocationState(
ShowLocationState.Dialog.None,
Location(1.23, 2.34, 4f),
description = "My favourite place!",
hasLocationPermission = false,
isTrackMyLocation = false,
appName = APP_NAME,
eventSink = {},
),
ShowLocationState(
ShowLocationState.Dialog.None,
Location(1.23, 2.34, 4f),
description = "For some reason I decided to to write a small essay that wraps at just two lines!",
hasLocationPermission = false,
isTrackMyLocation = false,
appName = APP_NAME,
eventSink = {},
),
ShowLocationState(
ShowLocationState.Dialog.None,
Location(1.23, 2.34, 4f),
description = "For some reason I decided to write a small essay in the location description. " +
"It is so long that it will wrap onto more than two lines!",
hasLocationPermission = false,
isTrackMyLocation = false,
appName = APP_NAME,
eventSink = {},
),
)

View file

@ -39,7 +39,9 @@ import androidx.compose.ui.unit.dp
import com.mapbox.mapboxsdk.camera.CameraPosition
import com.mapbox.mapboxsdk.geometry.LatLng
import io.element.android.features.location.api.internal.rememberTileStyleUrl
import io.element.android.features.location.impl.MapDefaults
import io.element.android.features.location.impl.common.MapDefaults
import io.element.android.features.location.impl.common.PermissionDeniedDialog
import io.element.android.features.location.impl.common.PermissionRationaleDialog
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
@ -70,6 +72,20 @@ fun ShowLocationView(
modifier: Modifier = Modifier,
onBackPressed: () -> Unit = {},
) {
when (state.permissionDialog) {
ShowLocationState.Dialog.None -> Unit
ShowLocationState.Dialog.PermissionDenied -> PermissionDeniedDialog(
onContinue = { state.eventSink(ShowLocationEvents.OpenAppSettings) },
onDismiss = { state.eventSink(ShowLocationEvents.DismissDialog) },
appName = state.appName,
)
ShowLocationState.Dialog.PermissionRationale -> PermissionRationaleDialog(
onContinue = { state.eventSink(ShowLocationEvents.RequestPermissions) },
onDismiss = { state.eventSink(ShowLocationEvents.DismissDialog) },
appName = state.appName,
)
}
val cameraPositionState = rememberCameraPositionState {
position = CameraPosition.Builder()
.target(LatLng(state.location.lat, state.location.lon))
@ -116,14 +132,12 @@ fun ShowLocationView(
)
},
floatingActionButton = {
if (state.hasLocationPermission) {
FloatingActionButton(
onClick = { state.eventSink(ShowLocationEvents.TrackMyLocation(true)) },
) {
when (state.isTrackMyLocation) {
false -> Icon(imageVector = Icons.Default.LocationSearching, contentDescription = null)
true -> Icon(imageVector = Icons.Default.MyLocation, contentDescription = null)
}
FloatingActionButton(
onClick = { state.eventSink(ShowLocationEvents.TrackMyLocation(true)) },
) {
when (state.isTrackMyLocation) {
false -> Icon(imageVector = Icons.Default.LocationSearching, contentDescription = null)
true -> Icon(imageVector = Icons.Default.MyLocation, contentDescription = null)
}
}
},

View file

@ -14,11 +14,10 @@
* limitations under the License.
*/
package io.element.android.features.location.impl.show
package io.element.android.features.location.impl.common.actions
import com.google.common.truth.Truth.assertThat
import io.element.android.features.location.api.Location
import io.element.android.features.location.impl.buildUrl
import org.junit.Test
import java.net.URLEncoder

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.location.impl.show
package io.element.android.features.location.impl.common.actions
import io.element.android.features.location.api.Location

Some files were not shown because too many files have changed in this diff Show more