Merge branch 'release/0.1.4' into main
This commit is contained in:
@@ -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
|
||||
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/danger.yml
vendored
2
.github/workflows/danger.yml
vendored
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
4
.github/workflows/nightlyReports.yml
vendored
4
.github/workflows/nightlyReports.yml
vendored
@@ -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
|
||||
|
||||
6
.github/workflows/quality.yml
vendored
6
.github/workflows/quality.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/recordScreenshots.yml
vendored
4
.github/workflows/recordScreenshots.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2.7.0
|
||||
uses: gradle/gradle-build-action@v2.7.1
|
||||
- name: Create app bundle
|
||||
env:
|
||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||
|
||||
4
.github/workflows/sonar.yml
vendored
4
.github/workflows/sonar.yml
vendored
@@ -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
|
||||
|
||||
24
.github/workflows/tests.yml
vendored
24
.github/workflows/tests.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/validate-lfs.yml
vendored
4
.github/workflows/validate-lfs.yml
vendored
@@ -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
2
.idea/kotlinc.xml
generated
@@ -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>
|
||||
@@ -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
|
||||
|
||||
9
.maestro/scripts/checkEnv.js
Normal file
9
.maestro/scripts/checkEnv.js
Normal 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"
|
||||
@@ -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 doesn’t support sliding sync."
|
||||
timeout: 10_000
|
||||
- tapOn: "Cancel"
|
||||
- back
|
||||
- back
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -7,6 +7,7 @@ appId: ${APP_ID}
|
||||
- tapOn: ${ROOM_NAME}
|
||||
# Back from timeline
|
||||
- back
|
||||
- assertVisible: "MyR"
|
||||
# Close keyboard
|
||||
- hideKeyboard
|
||||
# Back from search
|
||||
|
||||
33
CHANGES.md
33
CHANGES.md
@@ -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)
|
||||
========================================
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Contributing to Element Android
|
||||
# Contributing to Element X Android
|
||||
|
||||
<!--- TOC -->
|
||||
|
||||
|
||||
@@ -37,8 +37,6 @@ plugins {
|
||||
android {
|
||||
namespace = "io.element.android.x"
|
||||
|
||||
testOptions { unitTests.isIncludeAndroidResources = true }
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "io.element.android.x"
|
||||
targetSdk = Versions.targetSdk
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import coil.Coil
|
||||
import com.bumble.appyx.core.composable.Children
|
||||
import com.bumble.appyx.core.lifecycle.subscribe
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
@@ -53,19 +52,15 @@ import io.element.android.features.preferences.api.PreferencesEntryPoint
|
||||
import io.element.android.features.roomlist.api.RoomListEntryPoint
|
||||
import io.element.android.features.verifysession.api.VerifySessionEntryPoint
|
||||
import io.element.android.libraries.architecture.BackstackNode
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||
import io.element.android.libraries.architecture.bindings
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.deeplink.DeeplinkData
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||
import io.element.android.libraries.matrix.ui.di.MatrixUIBindings
|
||||
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -76,7 +71,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import timber.log.Timber
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
@ContributesNode(SessionScope::class)
|
||||
class LoggedInFlowNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
@@ -91,6 +86,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||
private val networkMonitor: NetworkMonitor,
|
||||
private val notificationDrawerManager: NotificationDrawerManager,
|
||||
private val ftueState: FtueState,
|
||||
private val matrixClient: MatrixClient,
|
||||
snackbarDispatcher: SnackbarDispatcher,
|
||||
) : BackstackNode<LoggedInFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
@@ -105,32 +101,18 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||
fun onOpenBugReport()
|
||||
}
|
||||
|
||||
interface LifecycleCallback : NodeLifecycleCallback {
|
||||
fun onFlowCreated(identifier: String, client: MatrixClient)
|
||||
|
||||
fun onFlowReleased(identifier: String, client: MatrixClient)
|
||||
}
|
||||
|
||||
data class Inputs(
|
||||
val matrixClient: MatrixClient
|
||||
) : NodeInputs
|
||||
|
||||
private val inputs: Inputs = inputs()
|
||||
private val syncService = inputs.matrixClient.syncService()
|
||||
private val syncService = matrixClient.syncService()
|
||||
private val loggedInFlowProcessor = LoggedInEventProcessor(
|
||||
snackbarDispatcher,
|
||||
inputs.matrixClient.roomMembershipObserver(),
|
||||
inputs.matrixClient.sessionVerificationService(),
|
||||
matrixClient.roomMembershipObserver(),
|
||||
matrixClient.sessionVerificationService(),
|
||||
)
|
||||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
lifecycle.subscribe(
|
||||
onCreate = {
|
||||
plugins<LifecycleCallback>().forEach { it.onFlowCreated(id, inputs.matrixClient) }
|
||||
val imageLoaderFactory = bindings<MatrixUIBindings>().loggedInImageLoaderFactory()
|
||||
Coil.setImageLoader(imageLoaderFactory)
|
||||
appNavigationStateService.onNavigateToSession(id, inputs.matrixClient.sessionId)
|
||||
appNavigationStateService.onNavigateToSession(id, matrixClient.sessionId)
|
||||
// TODO We do not support Space yet, so directly navigate to main space
|
||||
appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE)
|
||||
loggedInFlowProcessor.observeEvents(coroutineScope)
|
||||
@@ -146,7 +128,6 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||
}
|
||||
},
|
||||
onDestroy = {
|
||||
plugins<LifecycleCallback>().forEach { it.onFlowReleased(id, inputs.matrixClient) }
|
||||
appNavigationStateService.onLeavingSpace(id)
|
||||
appNavigationStateService.onLeavingSession(id)
|
||||
loggedInFlowProcessor.stopObserving()
|
||||
@@ -178,10 +159,10 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object Permanent : NavTarget
|
||||
data object Permanent : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object RoomList : NavTarget
|
||||
data object RoomList : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class Room(
|
||||
@@ -190,19 +171,19 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||
) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object Settings : NavTarget
|
||||
data object Settings : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object CreateRoom : NavTarget
|
||||
data object CreateRoom : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object VerifySession : NavTarget
|
||||
data object VerifySession : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object InviteList : NavTarget
|
||||
data object InviteList : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object Ftue : NavTarget
|
||||
data object Ftue : NavTarget
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
@@ -351,4 +332,3 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||
backstack.push(NavTarget.InviteList)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ class NotLoggedInFlowNode @AssistedInject constructor(
|
||||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object OnBoarding : NavTarget
|
||||
data object OnBoarding : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class LoginFlow(
|
||||
|
||||
@@ -72,15 +72,14 @@ class RootFlowNode @AssistedInject constructor(
|
||||
private val bugReportEntryPoint: BugReportEntryPoint,
|
||||
private val intentResolver: IntentResolver,
|
||||
private val oidcActionFlow: OidcActionFlow,
|
||||
) :
|
||||
BackstackNode<RootFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.SplashScreen,
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins
|
||||
) {
|
||||
) : BackstackNode<RootFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.SplashScreen,
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins
|
||||
) {
|
||||
|
||||
override fun onBuilt() {
|
||||
matrixClientsHolder.restoreWithSavedState(buildContext.savedStateMap)
|
||||
@@ -170,10 +169,10 @@ class RootFlowNode @AssistedInject constructor(
|
||||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object SplashScreen : NavTarget
|
||||
data object SplashScreen : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object NotLoggedInFlow : NavTarget
|
||||
data object NotLoggedInFlow : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class LoggedInFlow(
|
||||
@@ -182,7 +181,7 @@ class RootFlowNode @AssistedInject constructor(
|
||||
) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object BugReport : NavTarget
|
||||
data object BugReport : NavTarget
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
@@ -191,14 +190,14 @@ class RootFlowNode @AssistedInject constructor(
|
||||
val matrixClient = matrixClientsHolder.getOrNull(navTarget.sessionId) ?: return splashNode(buildContext).also {
|
||||
Timber.w("Couldn't find any session, go through SplashScreen")
|
||||
}
|
||||
val inputs = LoggedInFlowNode.Inputs(matrixClient)
|
||||
val callback = object : LoggedInFlowNode.Callback {
|
||||
val inputs = LoggedInAppScopeFlowNode.Inputs(matrixClient)
|
||||
val callback = object : LoggedInAppScopeFlowNode.Callback {
|
||||
override fun onOpenBugReport() {
|
||||
backstack.push(NavTarget.BugReport)
|
||||
}
|
||||
}
|
||||
val nodeLifecycleCallbacks = plugins<NodeLifecycleCallback>()
|
||||
createNode<LoggedInFlowNode>(buildContext, plugins = listOf(inputs, callback) + nodeLifecycleCallbacks)
|
||||
createNode<LoggedInAppScopeFlowNode>(buildContext, plugins = listOf(inputs, callback) + nodeLifecycleCallbacks)
|
||||
}
|
||||
NavTarget.NotLoggedInFlow -> createNode<NotLoggedInFlowNode>(buildContext)
|
||||
NavTarget.SplashScreen -> splashNode(buildContext)
|
||||
@@ -233,6 +232,7 @@ class RootFlowNode @AssistedInject constructor(
|
||||
private suspend fun navigateTo(deeplinkData: DeeplinkData) {
|
||||
Timber.d("Navigating to $deeplinkData")
|
||||
attachSession(deeplinkData.sessionId)
|
||||
.attachSession()
|
||||
.apply {
|
||||
when (deeplinkData) {
|
||||
is DeeplinkData.Root -> attachRoot()
|
||||
@@ -246,7 +246,7 @@ class RootFlowNode @AssistedInject constructor(
|
||||
oidcActionFlow.post(oidcAction)
|
||||
}
|
||||
|
||||
private suspend fun attachSession(sessionId: SessionId): LoggedInFlowNode {
|
||||
private suspend fun attachSession(sessionId: SessionId): LoggedInAppScopeFlowNode {
|
||||
//TODO handle multi-session
|
||||
return waitForChildAttached { navTarget ->
|
||||
navTarget is NavTarget.LoggedInFlow && navTarget.sessionId == sessionId
|
||||
|
||||
@@ -17,5 +17,5 @@
|
||||
package io.element.android.appnav.loggedin
|
||||
|
||||
// sealed interface LoggedInEvents {
|
||||
// object MyEvent : LoggedInEvents
|
||||
// data object MyEvent : LoggedInEvents
|
||||
// }
|
||||
|
||||
@@ -32,8 +32,8 @@ import kotlinx.coroutines.flow.stateIn
|
||||
import javax.inject.Inject
|
||||
|
||||
sealed interface LoadingRoomState {
|
||||
object Loading : LoadingRoomState
|
||||
object Error : LoadingRoomState
|
||||
data object Loading : LoadingRoomState
|
||||
data object Error : LoadingRoomState
|
||||
data class Loaded(val room: MatrixRoom) : LoadingRoomState
|
||||
}
|
||||
|
||||
|
||||
@@ -77,10 +77,10 @@ class RoomFlowNode @AssistedInject constructor(
|
||||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object Loading : NavTarget
|
||||
data object Loading : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object Loaded : NavTarget
|
||||
data object Loaded : NavTarget
|
||||
}
|
||||
|
||||
override fun onBuilt() {
|
||||
|
||||
@@ -152,10 +152,10 @@ class RoomLoadedFlowNode @AssistedInject constructor(
|
||||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object Messages : NavTarget
|
||||
data object Messages : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object RoomDetails : NavTarget
|
||||
data object RoomDetails : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class RoomMemberDetails(val userId: UserId) : NavTarget
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
2
fastlane/metadata/android/en-US/changelogs/40001040.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40001040.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Main changes in this version: bug fixes and add OIDC support.
|
||||
Full changelog: https://github.com/vector-im/element-x-android/releases
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -21,8 +21,8 @@ import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
||||
import io.element.android.features.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
@@ -21,8 +21,8 @@ import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
||||
import io.element.android.features.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ dependencies {
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(projects.features.analytics.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.mediapickers.test)
|
||||
testImplementation(projects.libraries.mediaupload.test)
|
||||
|
||||
@@ -63,10 +63,10 @@ class ConfigureRoomFlowNode @AssistedInject constructor(
|
||||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object Root : NavTarget
|
||||
data object Root : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object ConfigureRoom : NavTarget
|
||||
data object ConfigureRoom : NavTarget
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
|
||||
@@ -54,10 +54,10 @@ class CreateRoomFlowNode @AssistedInject constructor(
|
||||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object Root : NavTarget
|
||||
data object Root : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object NewRoom : NavTarget
|
||||
data object NewRoom : NavTarget
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,5 +27,5 @@ sealed interface ConfigureRoomEvents {
|
||||
data class RemoveFromSelection(val matrixUser: MatrixUser) : ConfigureRoomEvents
|
||||
data class CreateRoom(val config: CreateRoomConfig) : ConfigureRoomEvents
|
||||
data class HandleAvatarAction(val action: AvatarAction) : ConfigureRoomEvents
|
||||
object CancelCreateRoom : ConfigureRoomEvents
|
||||
data object CancelCreateRoom : ConfigureRoomEvents
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
||||
sealed interface CreateRoomRootEvents {
|
||||
data class StartDM(val matrixUser: MatrixUser) : CreateRoomRootEvents
|
||||
object CancelStartDM : CreateRoomRootEvents
|
||||
data object CancelStartDM : CreateRoomRootEvents
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -22,7 +22,6 @@ import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.CreatedRoom
|
||||
import io.element.android.features.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.features.createroom.impl.CreateRoomConfig
|
||||
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
||||
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
||||
@@ -38,6 +37,7 @@ import io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||
import io.element.android.libraries.mediapickers.test.FakePickerProvider
|
||||
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
|
||||
@@ -21,7 +21,6 @@ import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.CreatedRoom
|
||||
import io.element.android.features.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.features.createroom.impl.userlist.FakeUserListPresenter
|
||||
import io.element.android.features.createroom.impl.userlist.FakeUserListPresenterFactory
|
||||
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
||||
@@ -35,6 +34,7 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.usersearch.test.FakeUserRepository
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
|
||||
@@ -33,6 +33,7 @@ dependencies {
|
||||
implementation(projects.anvilannotations)
|
||||
anvil(projects.anvilcodegen)
|
||||
api(projects.features.ftue.api)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
@@ -49,7 +50,7 @@ dependencies {
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.features.analytics.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.analytics.api.AnalyticsEntryPoint
|
||||
import io.element.android.features.ftue.api.FtueEntryPoint
|
||||
import io.element.android.features.ftue.impl.migration.MigrationScreenNode
|
||||
import io.element.android.features.ftue.impl.state.DefaultFtueState
|
||||
import io.element.android.features.ftue.impl.state.FtueStep
|
||||
import io.element.android.features.ftue.impl.welcome.WelcomeNode
|
||||
@@ -41,6 +42,7 @@ import io.element.android.libraries.architecture.BackstackNode
|
||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -50,7 +52,7 @@ import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
@ContributesNode(SessionScope::class)
|
||||
class FtueFlowNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
@@ -69,13 +71,16 @@ class FtueFlowNode @AssistedInject constructor(
|
||||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object Placeholder : NavTarget
|
||||
data object Placeholder : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object WelcomeScreen : NavTarget
|
||||
data object MigrationScreen : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object AnalyticsOptIn : NavTarget
|
||||
data object WelcomeScreen : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object AnalyticsOptIn : NavTarget
|
||||
}
|
||||
|
||||
private val callback = plugins.filterIsInstance<FtueEntryPoint.Callback>().firstOrNull()
|
||||
@@ -102,6 +107,14 @@ class FtueFlowNode @AssistedInject constructor(
|
||||
NavTarget.Placeholder -> {
|
||||
createNode<PlaceholderNode>(buildContext)
|
||||
}
|
||||
NavTarget.MigrationScreen -> {
|
||||
val callback = object : MigrationScreenNode.Callback {
|
||||
override fun onMigrationFinished() {
|
||||
lifecycleScope.launch { moveToNextStep() }
|
||||
}
|
||||
}
|
||||
createNode<MigrationScreenNode>(buildContext, listOf(callback))
|
||||
}
|
||||
NavTarget.WelcomeScreen -> {
|
||||
val callback = object : WelcomeNode.Callback {
|
||||
override fun onContinueClicked() {
|
||||
@@ -117,12 +130,15 @@ class FtueFlowNode @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun moveToNextStep() {
|
||||
private fun moveToNextStep() {
|
||||
when (ftueState.getNextStep()) {
|
||||
is FtueStep.WelcomeScreen -> {
|
||||
FtueStep.MigrationScreen -> {
|
||||
backstack.newRoot(NavTarget.MigrationScreen)
|
||||
}
|
||||
FtueStep.WelcomeScreen -> {
|
||||
backstack.newRoot(NavTarget.WelcomeScreen)
|
||||
}
|
||||
is FtueStep.AnalyticsOptIn -> {
|
||||
FtueStep.AnalyticsOptIn -> {
|
||||
backstack.replace(NavTarget.AnalyticsOptIn)
|
||||
}
|
||||
null -> callback?.onFtueFlowFinished()
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.ftue.impl.migration
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import javax.inject.Inject
|
||||
|
||||
class MigrationScreenPresenter @Inject constructor(
|
||||
private val matrixClient: MatrixClient,
|
||||
private val migrationScreenStore: MigrationScreenStore,
|
||||
) : Presenter<MigrationScreenState> {
|
||||
@Composable
|
||||
override fun present(): MigrationScreenState {
|
||||
val roomListState by matrixClient.roomListService.state.collectAsState()
|
||||
if (roomListState == RoomListService.State.Running) {
|
||||
LaunchedEffect(Unit) {
|
||||
migrationScreenStore.setMigrationScreenShown(matrixClient.sessionId)
|
||||
}
|
||||
}
|
||||
return MigrationScreenState(
|
||||
isMigrating = roomListState != RoomListService.State.Running
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.ftue.impl.migration
|
||||
|
||||
data class MigrationScreenState(
|
||||
val isMigrating: Boolean
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.ftue.impl.migration
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
interface MigrationScreenStore {
|
||||
fun isMigrationScreenNeeded(sessionId: SessionId): Boolean
|
||||
fun setMigrationScreenShown(sessionId: SessionId)
|
||||
fun reset()
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.ftue.impl.migration
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import io.element.android.features.ftue.impl.R
|
||||
import io.element.android.libraries.designsystem.atomic.pages.SunsetPage
|
||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
|
||||
@Composable
|
||||
fun MigrationScreenView(
|
||||
migrationState: MigrationScreenState,
|
||||
onMigrationFinished: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (migrationState.isMigrating.not()) {
|
||||
LaunchedEffect(Unit) {
|
||||
onMigrationFinished()
|
||||
}
|
||||
}
|
||||
SunsetPage(
|
||||
modifier = modifier,
|
||||
isLoading = true,
|
||||
title = stringResource(id = R.string.screen_migration_title),
|
||||
subtitle = stringResource(id = R.string.screen_migration_message),
|
||||
overallContent = {}
|
||||
)
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
internal fun MigrationViewPreview() = ElementPreview {
|
||||
MigrationScreenView(
|
||||
migrationState = MigrationScreenState(isMigrating = true),
|
||||
onMigrationFinished = {})
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.ftue.impl.migration
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.androidutils.hash.hash
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.DefaultPreferences
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class SharedPrefsMigrationScreenStore @Inject constructor(
|
||||
@DefaultPreferences private val sharedPreferences: SharedPreferences,
|
||||
) : MigrationScreenStore {
|
||||
|
||||
override fun isMigrationScreenNeeded(sessionId: SessionId): Boolean {
|
||||
return sharedPreferences.getBoolean(sessionId.toKey(), false).not()
|
||||
}
|
||||
|
||||
override fun setMigrationScreenShown(sessionId: SessionId) {
|
||||
sharedPreferences.edit().putBoolean(sessionId.toKey(), true).apply()
|
||||
}
|
||||
|
||||
override fun reset() {
|
||||
sharedPreferences.edit {
|
||||
sharedPreferences.all.keys
|
||||
.filter { it.startsWith(IS_MIGRATION_SCREEN_SHOWN_PREFIX) }
|
||||
.forEach {
|
||||
remove(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun SessionId.toKey(): String {
|
||||
// Hash the sessionId to get rid of exotic char and take only the first 16 chars,
|
||||
// The risk of collision is not high.
|
||||
return IS_MIGRATION_SCREEN_SHOWN_PREFIX + value.hash().take(16)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val IS_MIGRATION_SCREEN_SHOWN_PREFIX = "is_migration_screen_shown_"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,10 @@ package io.element.android.features.ftue.impl.state
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.ftue.api.state.FtueState
|
||||
import io.element.android.features.ftue.impl.migration.MigrationScreenStore
|
||||
import io.element.android.features.ftue.impl.welcome.state.WelcomeScreenState
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -30,11 +32,13 @@ import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultFtueState @Inject constructor(
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val welcomeScreenState: WelcomeScreenState,
|
||||
private val migrationScreenStore: MigrationScreenStore,
|
||||
private val matrixClient: MatrixClient,
|
||||
) : FtueState {
|
||||
|
||||
override val shouldDisplayFlow = MutableStateFlow(isAnyStepIncomplete())
|
||||
@@ -42,6 +46,7 @@ class DefaultFtueState @Inject constructor(
|
||||
override suspend fun reset() {
|
||||
welcomeScreenState.reset()
|
||||
analyticsService.reset()
|
||||
migrationScreenStore.reset()
|
||||
}
|
||||
|
||||
init {
|
||||
@@ -52,7 +57,10 @@ class DefaultFtueState @Inject constructor(
|
||||
|
||||
fun getNextStep(currentStep: FtueStep? = null): FtueStep? =
|
||||
when (currentStep) {
|
||||
null -> if (shouldDisplayWelcomeScreen()) FtueStep.WelcomeScreen else getNextStep(
|
||||
null -> if (shouldDisplayMigrationScreen()) FtueStep.MigrationScreen else getNextStep(
|
||||
FtueStep.MigrationScreen
|
||||
)
|
||||
FtueStep.MigrationScreen -> if (shouldDisplayWelcomeScreen()) FtueStep.WelcomeScreen else getNextStep(
|
||||
FtueStep.WelcomeScreen
|
||||
)
|
||||
FtueStep.WelcomeScreen -> if (needsAnalyticsOptIn()) FtueStep.AnalyticsOptIn else getNextStep(
|
||||
@@ -63,11 +71,16 @@ class DefaultFtueState @Inject constructor(
|
||||
|
||||
private fun isAnyStepIncomplete(): Boolean {
|
||||
return listOf(
|
||||
shouldDisplayMigrationScreen(),
|
||||
shouldDisplayWelcomeScreen(),
|
||||
needsAnalyticsOptIn()
|
||||
).any { it }
|
||||
}
|
||||
|
||||
private fun shouldDisplayMigrationScreen(): Boolean {
|
||||
return migrationScreenStore.isMigrationScreenNeeded(matrixClient.sessionId)
|
||||
}
|
||||
|
||||
private fun needsAnalyticsOptIn(): Boolean {
|
||||
// We need this function to not be suspend, so we need to load the value through runBlocking
|
||||
return runBlocking { analyticsService.didAskUserConsent().first().not() }
|
||||
@@ -89,6 +102,7 @@ class DefaultFtueState @Inject constructor(
|
||||
}
|
||||
|
||||
sealed interface FtueStep {
|
||||
object WelcomeScreen : FtueStep
|
||||
object AnalyticsOptIn : FtueStep
|
||||
data object MigrationScreen : FtueStep
|
||||
data object WelcomeScreen : FtueStep
|
||||
data object AnalyticsOptIn : FtueStep
|
||||
}
|
||||
|
||||
11
features/ftue/impl/src/main/res/values-cs/translations.xml
Normal file
11
features/ftue/impl/src/main/res/values-cs/translations.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_migration_message">"Toto je jednorázový proces, děkujeme za čekání."</string>
|
||||
<string name="screen_migration_title">"Nastavení vašeho účtu"</string>
|
||||
<string name="screen_welcome_bullet_1">"Hovory, hlasování, vyhledávání a další budou přidány koncem tohoto roku."</string>
|
||||
<string name="screen_welcome_bullet_2">"Historie zpráv šifrovaných místností nebude v této aktualizaci k dispozici."</string>
|
||||
<string name="screen_welcome_bullet_3">"Rádi bychom se od vás dozvěděli, co si o tom myslíte, dejte nám vědět prostřednictvím stránky s nastavením."</string>
|
||||
<string name="screen_welcome_button">"Jdeme na to!"</string>
|
||||
<string name="screen_welcome_subtitle">"Zde je to, co potřebujete vědět:"</string>
|
||||
<string name="screen_welcome_title">"Vítá vás %1$s!"</string>
|
||||
</resources>
|
||||
@@ -1,6 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_welcome_bullet_1">"Anrufe, Standortfreigabe, Suche und mehr werden später in diesem Jahr hinzugefügt."</string>
|
||||
<string name="screen_migration_message">"Dies ist ein einmaliger Vorgang, danke fürs Warten."</string>
|
||||
<string name="screen_migration_title">"Dein Konto einrichten"</string>
|
||||
<string name="screen_welcome_bullet_1">"Anrufe, Umfragen, Suche und mehr werden später in diesem Jahr hinzugefügt."</string>
|
||||
<string name="screen_welcome_bullet_2">"Der Nachrichtenverlauf für verschlüsselte Räume wird in diesem Update nicht verfügbar sein."</string>
|
||||
<string name="screen_welcome_bullet_3">"Wir würden uns freuen, wenn du uns über die Einstellungsseite deine Meinung mitteilst."</string>
|
||||
<string name="screen_welcome_button">"Los geht\'s!"</string>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_migration_message">"Ce processus n’a besoin d’être fait qu’une seule fois, merci de patienter."</string>
|
||||
<string name="screen_migration_title">"Configuration de votre compte."</string>
|
||||
<string name="screen_welcome_bullet_2">"L’historique des messages pour les salons chiffrés ne sera pas disponible dans cette mise à jour."</string>
|
||||
<string name="screen_welcome_bullet_3">"Nous serions ravis d’avoir votre avis, n’hésitez pas à nous le partager via la page des paramètres."</string>
|
||||
<string name="screen_welcome_button">"C’est parti !"</string>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_migration_message">"Это одноразовый процесс, спасибо, что подождали."</string>
|
||||
<string name="screen_migration_title">"Настройка учетной записи."</string>
|
||||
<string name="screen_welcome_bullet_1">"Звонки, опросы, поиск и многое другое будут добавлены позже в этом году."</string>
|
||||
<string name="screen_welcome_bullet_2">"История сообщений для зашифрованных комнат в этом обновлении будет недоступна."</string>
|
||||
<string name="screen_welcome_bullet_3">"Мы будем рады услышать ваше мнение, сообщите нам об этом через страницу настроек."</string>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_migration_message">"Ide o jednorazový proces, ďakujeme za trpezlivosť."</string>
|
||||
<string name="screen_migration_title">"Nastavenie vášho účtu."</string>
|
||||
<string name="screen_welcome_bullet_1">"Hovory, ankety, vyhľadávanie a ďalšie funkcie pribudnú neskôr v tomto roku."</string>
|
||||
<string name="screen_welcome_bullet_2">"História správ pre zašifrované miestnosti nebude v tejto aktualizácii k dispozícii."</string>
|
||||
<string name="screen_welcome_bullet_3">"Radi by sme od vás počuli, dajte nám vedieť, čo si myslíte, prostredníctvom stránky nastavení."</string>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_migration_title">"設定您的帳號"</string>
|
||||
<string name="screen_welcome_button">"開始吧!"</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_migration_message">"This is a one time process, thanks for waiting."</string>
|
||||
<string name="screen_migration_title">"Setting up your account."</string>
|
||||
<string name="screen_welcome_bullet_1">"Calls, polls, search and more will be added later this year."</string>
|
||||
<string name="screen_welcome_bullet_2">"Message history for encrypted rooms won’t be available in this update."</string>
|
||||
<string name="screen_welcome_bullet_3">"We’d love to hear from you, let us know what you think via the settings page."</string>
|
||||
|
||||
@@ -17,11 +17,16 @@
|
||||
package io.element.android.features.ftue.impl
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.features.ftue.impl.migration.InMemoryMigrationScreenStore
|
||||
import io.element.android.features.ftue.impl.migration.MigrationScreenStore
|
||||
import io.element.android.features.ftue.impl.state.DefaultFtueState
|
||||
import io.element.android.features.ftue.impl.state.FtueStep
|
||||
import io.element.android.features.ftue.impl.welcome.state.FakeWelcomeState
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
@@ -45,12 +50,14 @@ class DefaultFtueStateTests {
|
||||
fun `given all checks being true, should display flow is false`() = runTest {
|
||||
val welcomeState = FakeWelcomeState()
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val migrationScreenStore = InMemoryMigrationScreenStore()
|
||||
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||
|
||||
val state = createState(coroutineScope, welcomeState, analyticsService)
|
||||
val state = createState(coroutineScope, welcomeState, analyticsService, migrationScreenStore)
|
||||
|
||||
welcomeState.setWelcomeScreenShown()
|
||||
analyticsService.setDidAskUserConsent()
|
||||
migrationScreenStore.setMigrationScreenShown(A_SESSION_ID)
|
||||
state.updateState()
|
||||
|
||||
assertThat(state.shouldDisplayFlow.value).isFalse()
|
||||
@@ -63,16 +70,21 @@ class DefaultFtueStateTests {
|
||||
fun `traverse flow`() = runTest {
|
||||
val welcomeState = FakeWelcomeState()
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val migrationScreenStore = InMemoryMigrationScreenStore()
|
||||
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||
|
||||
val state = createState(coroutineScope, welcomeState, analyticsService)
|
||||
val state = createState(coroutineScope, welcomeState, analyticsService, migrationScreenStore)
|
||||
val steps = mutableListOf<FtueStep?>()
|
||||
|
||||
// First step, welcome screen
|
||||
// First step, migration screen
|
||||
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||
migrationScreenStore.setMigrationScreenShown(A_SESSION_ID)
|
||||
|
||||
// Second step, welcome screen
|
||||
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||
welcomeState.setWelcomeScreenShown()
|
||||
|
||||
// Second step, analytics opt in
|
||||
// Third step, analytics opt in
|
||||
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||
analyticsService.setDidAskUserConsent()
|
||||
|
||||
@@ -80,6 +92,7 @@ class DefaultFtueStateTests {
|
||||
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||
|
||||
assertThat(steps).containsExactly(
|
||||
FtueStep.MigrationScreen,
|
||||
FtueStep.WelcomeScreen,
|
||||
FtueStep.AnalyticsOptIn,
|
||||
null, // Final state
|
||||
@@ -93,7 +106,16 @@ class DefaultFtueStateTests {
|
||||
fun `if a check for a step is true, start from the next one`() = runTest {
|
||||
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val state = createState(coroutineScope = coroutineScope, analyticsService = analyticsService)
|
||||
val migrationScreenStore = InMemoryMigrationScreenStore()
|
||||
|
||||
val state = createState(
|
||||
coroutineScope = coroutineScope,
|
||||
analyticsService = analyticsService,
|
||||
migrationScreenStore = migrationScreenStore,
|
||||
)
|
||||
|
||||
migrationScreenStore.setMigrationScreenShown(A_SESSION_ID)
|
||||
assertThat(state.getNextStep()).isEqualTo(FtueStep.WelcomeScreen)
|
||||
|
||||
state.setWelcomeScreenShown()
|
||||
assertThat(state.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn)
|
||||
@@ -108,7 +130,14 @@ class DefaultFtueStateTests {
|
||||
private fun createState(
|
||||
coroutineScope: CoroutineScope,
|
||||
welcomeState: FakeWelcomeState = FakeWelcomeState(),
|
||||
analyticsService: AnalyticsService = FakeAnalyticsService()
|
||||
) = DefaultFtueState(coroutineScope, analyticsService, welcomeState)
|
||||
|
||||
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
||||
migrationScreenStore: MigrationScreenStore = InMemoryMigrationScreenStore(),
|
||||
matrixClient: MatrixClient = FakeMatrixClient(),
|
||||
) = DefaultFtueState(
|
||||
coroutineScope = coroutineScope,
|
||||
analyticsService = analyticsService,
|
||||
welcomeScreenState = welcomeState,
|
||||
migrationScreenStore = migrationScreenStore,
|
||||
matrixClient = matrixClient,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.ftue.impl.migration
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
class InMemoryMigrationScreenStore : MigrationScreenStore {
|
||||
private val store = mutableMapOf<SessionId, Boolean>()
|
||||
|
||||
override fun isMigrationScreenNeeded(sessionId: SessionId): Boolean {
|
||||
// If store does not have key return true, else return the opposite of the value
|
||||
return store[sessionId]?.not() ?: true
|
||||
}
|
||||
|
||||
override fun setMigrationScreenShown(sessionId: SessionId) {
|
||||
store[sessionId] = true
|
||||
}
|
||||
|
||||
override fun reset() {
|
||||
store.clear()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.ftue.impl.migration
|
||||
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class MigrationScreenPresenterTest {
|
||||
@Test
|
||||
fun `present - initial`() = runTest {
|
||||
val presenter = createPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.isMigrating).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - migration end`() = runTest {
|
||||
val matrixClient = FakeMatrixClient()
|
||||
val migrationScreenStore = InMemoryMigrationScreenStore()
|
||||
val presenter = createPresenter(matrixClient, migrationScreenStore)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.isMigrating).isTrue()
|
||||
assertThat(migrationScreenStore.isMigrationScreenNeeded(A_SESSION_ID)).isTrue()
|
||||
// Simulate room list loaded
|
||||
(matrixClient.roomListService as FakeRoomListService).postState(RoomListService.State.Running)
|
||||
val nextState = awaitItem()
|
||||
assertThat(nextState.isMigrating).isFalse()
|
||||
assertThat(migrationScreenStore.isMigrationScreenNeeded(A_SESSION_ID)).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPresenter(
|
||||
matrixClient: MatrixClient = FakeMatrixClient(),
|
||||
migrationScreenStore: MigrationScreenStore = InMemoryMigrationScreenStore(),
|
||||
) = MigrationScreenPresenter(
|
||||
matrixClient,
|
||||
migrationScreenStore,
|
||||
)
|
||||
}
|
||||
@@ -53,7 +53,7 @@ dependencies {
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.push.test)
|
||||
testImplementation(projects.features.invitelist.test)
|
||||
testImplementation(projects.features.analytics.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
}
|
||||
|
||||
@@ -19,14 +19,12 @@ package io.element.android.features.invitelist.impl
|
||||
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
|
||||
|
||||
sealed interface InviteListEvents {
|
||||
|
||||
data class AcceptInvite(val invite: InviteListInviteSummary) : InviteListEvents
|
||||
data class DeclineInvite(val invite: InviteListInviteSummary) : InviteListEvents
|
||||
|
||||
object ConfirmDeclineInvite: InviteListEvents
|
||||
object CancelDeclineInvite: InviteListEvents
|
||||
|
||||
object DismissAcceptError: InviteListEvents
|
||||
object DismissDeclineError: InviteListEvents
|
||||
data object ConfirmDeclineInvite: InviteListEvents
|
||||
data object CancelDeclineInvite: InviteListEvents
|
||||
|
||||
data object DismissAcceptError: InviteListEvents
|
||||
data object DismissDeclineError: InviteListEvents
|
||||
}
|
||||
|
||||
@@ -32,6 +32,6 @@ data class InviteListState(
|
||||
)
|
||||
|
||||
sealed interface InviteDeclineConfirmationDialog {
|
||||
object Hidden : InviteDeclineConfirmationDialog
|
||||
data object Hidden : InviteDeclineConfirmationDialog
|
||||
data class Visible(val isDirect: Boolean, val name: String) : InviteDeclineConfirmationDialog
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -20,7 +20,6 @@ import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth
|
||||
import io.element.android.features.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.features.invitelist.api.SeenInvitesStore
|
||||
import io.element.android.features.invitelist.test.FakeSeenInvitesStore
|
||||
import io.element.android.libraries.architecture.Async
|
||||
@@ -44,6 +43,7 @@ import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
||||
import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
sealed interface LeaveRoomEvent {
|
||||
data class ShowConfirmation(val roomId: RoomId) : LeaveRoomEvent
|
||||
object HideConfirmation : LeaveRoomEvent
|
||||
data object HideConfirmation : LeaveRoomEvent
|
||||
data class LeaveRoom(val roomId: RoomId) : LeaveRoomEvent
|
||||
object HideError : LeaveRoomEvent
|
||||
data object HideError : LeaveRoomEvent
|
||||
}
|
||||
|
||||
@@ -25,19 +25,19 @@ data class LeaveRoomState(
|
||||
val eventSink: (LeaveRoomEvent) -> Unit = {},
|
||||
) {
|
||||
sealed interface Confirmation {
|
||||
object Hidden : Confirmation
|
||||
data object Hidden : Confirmation
|
||||
data class Generic(val roomId: RoomId) : Confirmation
|
||||
data class PrivateRoom(val roomId: RoomId) : Confirmation
|
||||
data class LastUserInRoom(val roomId: RoomId) : Confirmation
|
||||
}
|
||||
|
||||
sealed interface Progress {
|
||||
object Hidden : Progress
|
||||
object Shown : Progress
|
||||
data object Hidden : Progress
|
||||
data object Shown : Progress
|
||||
}
|
||||
|
||||
sealed interface Error {
|
||||
object Hidden : Error
|
||||
object Shown : Error
|
||||
data object Hidden : Error
|
||||
data object Shown : Error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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++ }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user