Merge branch 'release/0.1.4' into main
This commit is contained in:
commit
b01854c9cd
2064 changed files with 8670 additions and 4141 deletions
|
|
@ -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
|
## Type of change
|
||||||
|
|
||||||
|
|
@ -17,13 +17,17 @@
|
||||||
|
|
||||||
## Screenshots / GIFs
|
## 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.
|
You can use a table like this to show screenshots comparison.
|
||||||
Uncomment this markdown table below and edit the last line `|||`:
|
Uncomment this markdown table below and edit the last line `|||`:
|
||||||
|copy screenshot of before here|copy screenshot of after here|
|
|copy screenshot of before here|copy screenshot of after here|
|
||||||
-->
|
|
||||||
|
|
||||||
<!--
|
|
||||||
|Before|After|
|
|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. -->
|
<!-- 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
|
- [ ] 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 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 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)
|
- [ ] 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
|
- [ ] 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:
|
debug:
|
||||||
name: Build APKs
|
name: Build APKs
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Skip for `main` and the merge queue if the branch is up to date with `develop`
|
# Skip for `main`
|
||||||
if: github.ref != 'refs/heads/main' && github.event.merge_group.base_ref != 'refs/heads/develop'
|
if: github.ref != 'refs/heads/main'
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
variant: [debug, release, nightly, samples]
|
variant: [debug, release, nightly, samples]
|
||||||
|
|
@ -38,7 +38,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/gradle-build-action@v2.7.0
|
uses: gradle/gradle-build-action@v2.7.1
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Assemble debug APK
|
- name: Assemble debug APK
|
||||||
|
|
|
||||||
2
.github/workflows/danger.yml
vendored
2
.github/workflows/danger.yml
vendored
|
|
@ -5,8 +5,6 @@ on: [pull_request, merge_group]
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
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
|
name: Danger main check
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,6 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
validation:
|
validation:
|
||||||
name: "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
|
runs-on: ubuntu-latest
|
||||||
# No concurrency required, this is a prerequisite to other actions and should run every time.
|
# No concurrency required, this is a prerequisite to other actions and should run every time.
|
||||||
steps:
|
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' }}
|
if: ${{ github.repository == 'vector-im/element-x-android' }}
|
||||||
steps:
|
steps:
|
||||||
- name: ⏬ Checkout with LFS
|
- 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
|
- name: Use JDK 17
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v3
|
||||||
|
|
@ -62,7 +62,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/gradle-build-action@v2.7.0
|
uses: gradle/gradle-build-action@v2.7.1
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Dependency analysis
|
- name: Dependency analysis
|
||||||
|
|
|
||||||
6
.github/workflows/quality.yml
vendored
6
.github/workflows/quality.yml
vendored
|
|
@ -16,8 +16,6 @@ jobs:
|
||||||
checkScript:
|
checkScript:
|
||||||
name: Search for forbidden patterns
|
name: Search for forbidden patterns
|
||||||
runs-on: ubuntu-latest
|
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:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Run code quality check suite
|
- name: Run code quality check suite
|
||||||
|
|
@ -26,8 +24,6 @@ jobs:
|
||||||
check:
|
check:
|
||||||
name: Project Check Suite
|
name: Project Check Suite
|
||||||
runs-on: ubuntu-latest
|
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.
|
# Allow all jobs on main and develop. Just one per PR.
|
||||||
concurrency:
|
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) }}
|
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
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/gradle-build-action@v2.7.0
|
uses: gradle/gradle-build-action@v2.7.1
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Run code quality check suite
|
- name: Run code quality check suite
|
||||||
|
|
|
||||||
4
.github/workflows/recordScreenshots.yml
vendored
4
.github/workflows/recordScreenshots.yml
vendored
|
|
@ -14,7 +14,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: ⏬ Checkout with LFS
|
- name: ⏬ Checkout with LFS
|
||||||
uses: nschloe/action-cached-lfs-checkout@v1.2.1
|
uses: nschloe/action-cached-lfs-checkout@v1.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: ☕️ Use JDK 17
|
- name: ☕️ Use JDK 17
|
||||||
|
|
@ -24,7 +24,7 @@ jobs:
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
# Add gradle cache, this should speed up the process
|
# Add gradle cache, this should speed up the process
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/gradle-build-action@v2.7.0
|
uses: gradle/gradle-build-action@v2.7.1
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Record screenshots
|
- name: Record screenshots
|
||||||
|
|
|
||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -25,7 +25,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/gradle-build-action@v2.7.0
|
uses: gradle/gradle-build-action@v2.7.1
|
||||||
- name: Create app bundle
|
- name: Create app bundle
|
||||||
env:
|
env:
|
||||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||||
|
|
|
||||||
4
.github/workflows/sonar.yml
vendored
4
.github/workflows/sonar.yml
vendored
|
|
@ -16,8 +16,6 @@ jobs:
|
||||||
sonar:
|
sonar:
|
||||||
name: Project Check Suite
|
name: Project Check Suite
|
||||||
runs-on: ubuntu-latest
|
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.
|
# Allow all jobs on main and develop. Just one per PR.
|
||||||
concurrency:
|
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) }}
|
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
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/gradle-build-action@v2.7.0
|
uses: gradle/gradle-build-action@v2.7.1
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: 🔊 Publish results to Sonar
|
- name: 🔊 Publish results to Sonar
|
||||||
|
|
|
||||||
24
.github/workflows/tests.yml
vendored
24
.github/workflows/tests.yml
vendored
|
|
@ -16,8 +16,6 @@ jobs:
|
||||||
tests:
|
tests:
|
||||||
name: Runs unit tests
|
name: Runs unit tests
|
||||||
runs-on: ubuntu-latest
|
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.
|
# Allow all jobs on main and develop. Just one per PR.
|
||||||
concurrency:
|
concurrency:
|
||||||
|
|
@ -25,7 +23,7 @@ jobs:
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- name: ⏬ Checkout with LFS
|
- name: ⏬ Checkout with LFS
|
||||||
uses: nschloe/action-cached-lfs-checkout@v1.2.1
|
uses: nschloe/action-cached-lfs-checkout@v1.2.2
|
||||||
with:
|
with:
|
||||||
# Ensure we are building the branch and not the branch after being merged on develop
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
# https://github.com/actions/checkout/issues/881
|
# https://github.com/actions/checkout/issues/881
|
||||||
|
|
@ -36,7 +34,7 @@ jobs:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Configure gradle
|
- name: Configure gradle
|
||||||
uses: gradle/gradle-build-action@v2.7.0
|
uses: gradle/gradle-build-action@v2.7.1
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
|
|
||||||
|
|
@ -57,22 +55,6 @@ jobs:
|
||||||
path: |
|
path: |
|
||||||
**/kover/merged/verification/errors.txt
|
**/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)
|
- name: ✅ Upload kover report (disabled)
|
||||||
if: always()
|
if: always()
|
||||||
run: echo "This is now done only once a day, see nightlyReports.yml"
|
run: echo "This is now done only once a day, see nightlyReports.yml"
|
||||||
|
|
@ -83,7 +65,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
name: tests-and-screenshot-tests-results
|
name: tests-and-screenshot-tests-results
|
||||||
path: |
|
path: |
|
||||||
**/out/failures/
|
**/build/paparazzi/failures/
|
||||||
**/build/reports/tests/*UnitTest/
|
**/build/reports/tests/*UnitTest/
|
||||||
|
|
||||||
# https://github.com/codecov/codecov-action
|
# 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:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
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
|
name: Validate
|
||||||
steps:
|
steps:
|
||||||
- uses: nschloe/action-cached-lfs-checkout@v1.2.1
|
- uses: nschloe/action-cached-lfs-checkout@v1.2.2
|
||||||
|
|
||||||
- run: |
|
- run: |
|
||||||
./tools/git/validate_lfs.sh
|
./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"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="KotlinJpsPluginSettings">
|
<component name="KotlinJpsPluginSettings">
|
||||||
<option name="version" value="1.8.22" />
|
<option name="version" value="1.9.0" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
appId: ${APP_ID}
|
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/init.yaml
|
||||||
- runFlow: tests/account/login.yaml
|
- runFlow: tests/account/login.yaml
|
||||||
- runFlow: tests/settings/settings.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: "Other"
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "change_server-server"
|
id: "change_server-server"
|
||||||
- inputText: "element"
|
# Test server that does not support sliding sync.
|
||||||
|
- inputText: "gnuradio"
|
||||||
- hideKeyboard
|
- hideKeyboard
|
||||||
- tapOn: "element.io"
|
- tapOn: "gnuradio.org"
|
||||||
|
- extendedWaitUntil:
|
||||||
|
visible: "This server currently doesn’t support sliding sync."
|
||||||
|
timeout: 10_000
|
||||||
- tapOn: "Cancel"
|
- tapOn: "Cancel"
|
||||||
- back
|
- back
|
||||||
- back
|
- back
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ appId: ${APP_ID}
|
||||||
- takeScreenshot: build/maestro/320-createAndDeleteRoom
|
- takeScreenshot: build/maestro/320-createAndDeleteRoom
|
||||||
- tapOn: "aRoomName"
|
- tapOn: "aRoomName"
|
||||||
- tapOn: "Invite people"
|
- tapOn: "Invite people"
|
||||||
# assert there's 1 memeber and 1 invitee
|
# assert there's 1 member and 1 invitee
|
||||||
- tapOn: "Search for someone"
|
- tapOn: "Search for someone"
|
||||||
- inputText: ${INVITEE2_MXID}
|
- inputText: ${INVITEE2_MXID}
|
||||||
- tapOn:
|
- tapOn:
|
||||||
|
|
@ -27,7 +27,7 @@ appId: ${APP_ID}
|
||||||
- tapOn: "Back"
|
- tapOn: "Back"
|
||||||
- tapOn: "aRoomName"
|
- tapOn: "aRoomName"
|
||||||
- tapOn: "People"
|
- tapOn: "People"
|
||||||
# assert there's 1 memeber and 2 invitees
|
# assert there's 1 member and 2 invitees
|
||||||
- tapOn: "Back"
|
- tapOn: "Back"
|
||||||
- tapOn: "Leave room"
|
- tapOn: "Leave room"
|
||||||
- tapOn: "Leave"
|
- tapOn: "Leave"
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ appId: ${APP_ID}
|
||||||
- tapOn: ${ROOM_NAME}
|
- tapOn: ${ROOM_NAME}
|
||||||
# Back from timeline
|
# Back from timeline
|
||||||
- back
|
- back
|
||||||
|
- assertVisible: "MyR"
|
||||||
# Close keyboard
|
# Close keyboard
|
||||||
- hideKeyboard
|
- hideKeyboard
|
||||||
# Back from search
|
# 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)
|
Changes in Element X v0.1.2 (2023-08-16)
|
||||||
========================================
|
========================================
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# Contributing to Element Android
|
# Contributing to Element X Android
|
||||||
|
|
||||||
<!--- TOC -->
|
<!--- TOC -->
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,6 @@ plugins {
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.x"
|
namespace = "io.element.android.x"
|
||||||
|
|
||||||
testOptions { unitTests.isIncludeAndroidResources = true }
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "io.element.android.x"
|
applicationId = "io.element.android.x"
|
||||||
targetSdk = Versions.targetSdk
|
targetSdk = Versions.targetSdk
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,7 @@
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.startup.InitializationProvider"
|
android:name="androidx.startup.InitializationProvider"
|
||||||
android:authorities="${applicationId}.androidx-startup"
|
android:authorities="${applicationId}.androidx-startup"
|
||||||
android:exported="false"
|
android:exported="false">
|
||||||
tools:node="merge">
|
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name='androidx.lifecycle.ProcessLifecycleInitializer'
|
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.Node
|
||||||
import com.bumble.appyx.core.node.ParentNode
|
import com.bumble.appyx.core.node.ParentNode
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import io.element.android.appnav.LoggedInFlowNode
|
import io.element.android.appnav.LoggedInAppScopeFlowNode
|
||||||
import io.element.android.appnav.room.RoomLoadedFlowNode
|
import io.element.android.appnav.room.RoomLoadedFlowNode
|
||||||
import io.element.android.appnav.RootFlowNode
|
import io.element.android.appnav.RootFlowNode
|
||||||
import io.element.android.libraries.architecture.bindings
|
import io.element.android.libraries.architecture.bindings
|
||||||
|
|
@ -56,7 +56,7 @@ class MainNode(
|
||||||
),
|
),
|
||||||
DaggerComponentOwner by mainDaggerComponentOwner {
|
DaggerComponentOwner by mainDaggerComponentOwner {
|
||||||
|
|
||||||
private val loggedInFlowNodeCallback = object : LoggedInFlowNode.LifecycleCallback {
|
private val loggedInFlowNodeCallback = object : LoggedInAppScopeFlowNode.LifecycleCallback {
|
||||||
override fun onFlowCreated(identifier: String, client: MatrixClient) {
|
override fun onFlowCreated(identifier: String, client: MatrixClient) {
|
||||||
val component = bindings<SessionComponent.ParentBindings>().sessionComponentBuilder().client(client).build()
|
val component = bindings<SessionComponent.ParentBindings>().sessionComponentBuilder().client(client).build()
|
||||||
mainDaggerComponentOwner.addComponent(identifier, component)
|
mainDaggerComponentOwner.addComponent(identifier, component)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,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.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import coil.Coil
|
|
||||||
import com.bumble.appyx.core.composable.Children
|
import com.bumble.appyx.core.composable.Children
|
||||||
import com.bumble.appyx.core.lifecycle.subscribe
|
import com.bumble.appyx.core.lifecycle.subscribe
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
|
@ -53,19 +52,15 @@ import io.element.android.features.preferences.api.PreferencesEntryPoint
|
||||||
import io.element.android.features.roomlist.api.RoomListEntryPoint
|
import io.element.android.features.roomlist.api.RoomListEntryPoint
|
||||||
import io.element.android.features.verifysession.api.VerifySessionEntryPoint
|
import io.element.android.features.verifysession.api.VerifySessionEntryPoint
|
||||||
import io.element.android.libraries.architecture.BackstackNode
|
import io.element.android.libraries.architecture.BackstackNode
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
|
||||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||||
import io.element.android.libraries.architecture.bindings
|
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.architecture.inputs
|
|
||||||
import io.element.android.libraries.deeplink.DeeplinkData
|
import io.element.android.libraries.deeplink.DeeplinkData
|
||||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
|
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||||
import io.element.android.libraries.matrix.ui.di.MatrixUIBindings
|
|
||||||
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
||||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -76,7 +71,7 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@ContributesNode(AppScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
class LoggedInFlowNode @AssistedInject constructor(
|
class LoggedInFlowNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
|
@ -91,6 +86,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
private val notificationDrawerManager: NotificationDrawerManager,
|
private val notificationDrawerManager: NotificationDrawerManager,
|
||||||
private val ftueState: FtueState,
|
private val ftueState: FtueState,
|
||||||
|
private val matrixClient: MatrixClient,
|
||||||
snackbarDispatcher: SnackbarDispatcher,
|
snackbarDispatcher: SnackbarDispatcher,
|
||||||
) : BackstackNode<LoggedInFlowNode.NavTarget>(
|
) : BackstackNode<LoggedInFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
|
|
@ -105,32 +101,18 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
fun onOpenBugReport()
|
fun onOpenBugReport()
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LifecycleCallback : NodeLifecycleCallback {
|
private val syncService = matrixClient.syncService()
|
||||||
fun onFlowCreated(identifier: String, client: MatrixClient)
|
|
||||||
|
|
||||||
fun onFlowReleased(identifier: String, client: MatrixClient)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Inputs(
|
|
||||||
val matrixClient: MatrixClient
|
|
||||||
) : NodeInputs
|
|
||||||
|
|
||||||
private val inputs: Inputs = inputs()
|
|
||||||
private val syncService = inputs.matrixClient.syncService()
|
|
||||||
private val loggedInFlowProcessor = LoggedInEventProcessor(
|
private val loggedInFlowProcessor = LoggedInEventProcessor(
|
||||||
snackbarDispatcher,
|
snackbarDispatcher,
|
||||||
inputs.matrixClient.roomMembershipObserver(),
|
matrixClient.roomMembershipObserver(),
|
||||||
inputs.matrixClient.sessionVerificationService(),
|
matrixClient.sessionVerificationService(),
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
super.onBuilt()
|
super.onBuilt()
|
||||||
lifecycle.subscribe(
|
lifecycle.subscribe(
|
||||||
onCreate = {
|
onCreate = {
|
||||||
plugins<LifecycleCallback>().forEach { it.onFlowCreated(id, inputs.matrixClient) }
|
appNavigationStateService.onNavigateToSession(id, matrixClient.sessionId)
|
||||||
val imageLoaderFactory = bindings<MatrixUIBindings>().loggedInImageLoaderFactory()
|
|
||||||
Coil.setImageLoader(imageLoaderFactory)
|
|
||||||
appNavigationStateService.onNavigateToSession(id, inputs.matrixClient.sessionId)
|
|
||||||
// TODO We do not support Space yet, so directly navigate to main space
|
// TODO We do not support Space yet, so directly navigate to main space
|
||||||
appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE)
|
appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE)
|
||||||
loggedInFlowProcessor.observeEvents(coroutineScope)
|
loggedInFlowProcessor.observeEvents(coroutineScope)
|
||||||
|
|
@ -146,7 +128,6 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDestroy = {
|
onDestroy = {
|
||||||
plugins<LifecycleCallback>().forEach { it.onFlowReleased(id, inputs.matrixClient) }
|
|
||||||
appNavigationStateService.onLeavingSpace(id)
|
appNavigationStateService.onLeavingSpace(id)
|
||||||
appNavigationStateService.onLeavingSession(id)
|
appNavigationStateService.onLeavingSession(id)
|
||||||
loggedInFlowProcessor.stopObserving()
|
loggedInFlowProcessor.stopObserving()
|
||||||
|
|
@ -178,10 +159,10 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object Permanent : NavTarget
|
data object Permanent : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object RoomList : NavTarget
|
data object RoomList : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Room(
|
data class Room(
|
||||||
|
|
@ -190,19 +171,19 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
) : NavTarget
|
) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object Settings : NavTarget
|
data object Settings : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object CreateRoom : NavTarget
|
data object CreateRoom : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object VerifySession : NavTarget
|
data object VerifySession : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object InviteList : NavTarget
|
data object InviteList : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object Ftue : NavTarget
|
data object Ftue : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
|
@ -351,4 +332,3 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
backstack.push(NavTarget.InviteList)
|
backstack.push(NavTarget.InviteList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ class NotLoggedInFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object OnBoarding : NavTarget
|
data object OnBoarding : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class LoginFlow(
|
data class LoginFlow(
|
||||||
|
|
|
||||||
|
|
@ -72,15 +72,14 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
private val bugReportEntryPoint: BugReportEntryPoint,
|
private val bugReportEntryPoint: BugReportEntryPoint,
|
||||||
private val intentResolver: IntentResolver,
|
private val intentResolver: IntentResolver,
|
||||||
private val oidcActionFlow: OidcActionFlow,
|
private val oidcActionFlow: OidcActionFlow,
|
||||||
) :
|
) : BackstackNode<RootFlowNode.NavTarget>(
|
||||||
BackstackNode<RootFlowNode.NavTarget>(
|
backstack = BackStack(
|
||||||
backstack = BackStack(
|
initialElement = NavTarget.SplashScreen,
|
||||||
initialElement = NavTarget.SplashScreen,
|
savedStateMap = buildContext.savedStateMap,
|
||||||
savedStateMap = buildContext.savedStateMap,
|
),
|
||||||
),
|
buildContext = buildContext,
|
||||||
buildContext = buildContext,
|
plugins = plugins
|
||||||
plugins = plugins
|
) {
|
||||||
) {
|
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
matrixClientsHolder.restoreWithSavedState(buildContext.savedStateMap)
|
matrixClientsHolder.restoreWithSavedState(buildContext.savedStateMap)
|
||||||
|
|
@ -170,10 +169,10 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object SplashScreen : NavTarget
|
data object SplashScreen : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object NotLoggedInFlow : NavTarget
|
data object NotLoggedInFlow : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class LoggedInFlow(
|
data class LoggedInFlow(
|
||||||
|
|
@ -182,7 +181,7 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
) : NavTarget
|
) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object BugReport : NavTarget
|
data object BugReport : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
|
@ -191,14 +190,14 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
val matrixClient = matrixClientsHolder.getOrNull(navTarget.sessionId) ?: return splashNode(buildContext).also {
|
val matrixClient = matrixClientsHolder.getOrNull(navTarget.sessionId) ?: return splashNode(buildContext).also {
|
||||||
Timber.w("Couldn't find any session, go through SplashScreen")
|
Timber.w("Couldn't find any session, go through SplashScreen")
|
||||||
}
|
}
|
||||||
val inputs = LoggedInFlowNode.Inputs(matrixClient)
|
val inputs = LoggedInAppScopeFlowNode.Inputs(matrixClient)
|
||||||
val callback = object : LoggedInFlowNode.Callback {
|
val callback = object : LoggedInAppScopeFlowNode.Callback {
|
||||||
override fun onOpenBugReport() {
|
override fun onOpenBugReport() {
|
||||||
backstack.push(NavTarget.BugReport)
|
backstack.push(NavTarget.BugReport)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val nodeLifecycleCallbacks = plugins<NodeLifecycleCallback>()
|
val nodeLifecycleCallbacks = plugins<NodeLifecycleCallback>()
|
||||||
createNode<LoggedInFlowNode>(buildContext, plugins = listOf(inputs, callback) + nodeLifecycleCallbacks)
|
createNode<LoggedInAppScopeFlowNode>(buildContext, plugins = listOf(inputs, callback) + nodeLifecycleCallbacks)
|
||||||
}
|
}
|
||||||
NavTarget.NotLoggedInFlow -> createNode<NotLoggedInFlowNode>(buildContext)
|
NavTarget.NotLoggedInFlow -> createNode<NotLoggedInFlowNode>(buildContext)
|
||||||
NavTarget.SplashScreen -> splashNode(buildContext)
|
NavTarget.SplashScreen -> splashNode(buildContext)
|
||||||
|
|
@ -233,6 +232,7 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
private suspend fun navigateTo(deeplinkData: DeeplinkData) {
|
private suspend fun navigateTo(deeplinkData: DeeplinkData) {
|
||||||
Timber.d("Navigating to $deeplinkData")
|
Timber.d("Navigating to $deeplinkData")
|
||||||
attachSession(deeplinkData.sessionId)
|
attachSession(deeplinkData.sessionId)
|
||||||
|
.attachSession()
|
||||||
.apply {
|
.apply {
|
||||||
when (deeplinkData) {
|
when (deeplinkData) {
|
||||||
is DeeplinkData.Root -> attachRoot()
|
is DeeplinkData.Root -> attachRoot()
|
||||||
|
|
@ -246,7 +246,7 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
oidcActionFlow.post(oidcAction)
|
oidcActionFlow.post(oidcAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun attachSession(sessionId: SessionId): LoggedInFlowNode {
|
private suspend fun attachSession(sessionId: SessionId): LoggedInAppScopeFlowNode {
|
||||||
//TODO handle multi-session
|
//TODO handle multi-session
|
||||||
return waitForChildAttached { navTarget ->
|
return waitForChildAttached { navTarget ->
|
||||||
navTarget is NavTarget.LoggedInFlow && navTarget.sessionId == sessionId
|
navTarget is NavTarget.LoggedInFlow && navTarget.sessionId == sessionId
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,5 @@
|
||||||
package io.element.android.appnav.loggedin
|
package io.element.android.appnav.loggedin
|
||||||
|
|
||||||
// sealed interface LoggedInEvents {
|
// sealed interface LoggedInEvents {
|
||||||
// object MyEvent : LoggedInEvents
|
// data object MyEvent : LoggedInEvents
|
||||||
// }
|
// }
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,8 @@ import kotlinx.coroutines.flow.stateIn
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
sealed interface LoadingRoomState {
|
sealed interface LoadingRoomState {
|
||||||
object Loading : LoadingRoomState
|
data object Loading : LoadingRoomState
|
||||||
object Error : LoadingRoomState
|
data object Error : LoadingRoomState
|
||||||
data class Loaded(val room: MatrixRoom) : LoadingRoomState
|
data class Loaded(val room: MatrixRoom) : LoadingRoomState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,10 +77,10 @@ class RoomFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object Loading : NavTarget
|
data object Loading : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object Loaded : NavTarget
|
data object Loaded : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
|
|
|
||||||
|
|
@ -152,10 +152,10 @@ class RoomLoadedFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object Messages : NavTarget
|
data object Messages : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object RoomDetails : NavTarget
|
data object RoomDetails : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class RoomMemberDetails(val userId: UserId) : NavTarget
|
data class RoomMemberDetails(val userId: UserId) : NavTarget
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
|
import com.google.devtools.ksp.gradle.KspTask
|
||||||
import kotlinx.kover.api.KoverTaskExtension
|
import kotlinx.kover.api.KoverTaskExtension
|
||||||
|
import org.apache.tools.ant.taskdefs.optional.ReplaceRegExp
|
||||||
import org.jetbrains.kotlin.cli.common.toBooleanLenient
|
import org.jetbrains.kotlin.cli.common.toBooleanLenient
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
dependencies {
|
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")
|
classpath("com.google.gms:google-services:4.3.15")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -34,6 +36,7 @@ plugins {
|
||||||
alias(libs.plugins.kotlin.jvm) apply false
|
alias(libs.plugins.kotlin.jvm) apply false
|
||||||
alias(libs.plugins.kapt) apply false
|
alias(libs.plugins.kapt) apply false
|
||||||
alias(libs.plugins.dependencycheck) apply false
|
alias(libs.plugins.dependencycheck) apply false
|
||||||
|
alias(libs.plugins.dependencyanalysis)
|
||||||
alias(libs.plugins.detekt)
|
alias(libs.plugins.detekt)
|
||||||
alias(libs.plugins.ktlint)
|
alias(libs.plugins.ktlint)
|
||||||
alias(libs.plugins.dependencygraph)
|
alias(libs.plugins.dependencygraph)
|
||||||
|
|
@ -59,7 +62,7 @@ allprojects {
|
||||||
config.from(files("$rootDir/tools/detekt/detekt.yml"))
|
config.from(files("$rootDir/tools/detekt/detekt.yml"))
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
detektPlugins("io.nlopez.compose.rules:detekt:0.1.12")
|
detektPlugins("io.nlopez.compose.rules:detekt:0.2.1")
|
||||||
}
|
}
|
||||||
|
|
||||||
// KtLint
|
// KtLint
|
||||||
|
|
@ -98,6 +101,22 @@ allprojects {
|
||||||
// Or add a line with "allWarningsAsErrors=true" in your ~/.gradle/gradle.properties file
|
// Or add a line with "allWarningsAsErrors=true" in your ~/.gradle/gradle.properties file
|
||||||
kotlinOptions.allWarningsAsErrors = project.properties["allWarningsAsErrors"] == "true"
|
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:
|
// To run a sonar analysis:
|
||||||
|
|
@ -150,7 +169,7 @@ allprojects {
|
||||||
maxHeapSize = "1g"
|
maxHeapSize = "1g"
|
||||||
} else {
|
} else {
|
||||||
// Disable screenshot tests by default
|
// 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.findByPath("$path:lint")?.let { dependsOn(it) }
|
||||||
tasks.findByName("detekt")?.let { dependsOn(it) }
|
tasks.findByName("detekt")?.let { dependsOn(it) }
|
||||||
tasks.findByName("ktlintCheck")?.let { dependsOn(it) }
|
tasks.findByName("ktlintCheck")?.let { dependsOn(it) }
|
||||||
|
// tasks.findByName("buildHealth")?.let { dependsOn(it) }
|
||||||
}
|
}
|
||||||
dependsOn(":app:knitCheck")
|
dependsOn(":app:knitCheck")
|
||||||
}
|
}
|
||||||
|
|
@ -343,3 +363,21 @@ subprojects {
|
||||||
tasks.findByName("recordPaparazziDebug")?.dependsOn(removeOldScreenshotsTask)
|
tasks.findByName("recordPaparazziDebug")?.dependsOn(removeOldScreenshotsTask)
|
||||||
tasks.findByName("recordPaparazziRelease")?.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 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
|
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
|
./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
|
## 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.turbine)
|
||||||
testImplementation(libs.test.mockk)
|
testImplementation(libs.test.mockk)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.features.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.features.analytics.impl)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,6 @@
|
||||||
<string name="screen_analytics_prompt_read_terms">"Du kannst alle unsere Nutzerbedingungen %1$s lesen."</string>
|
<string name="screen_analytics_prompt_read_terms">"Du kannst alle unsere Nutzerbedingungen %1$s lesen."</string>
|
||||||
<string name="screen_analytics_prompt_read_terms_content_link">"hier"</string>
|
<string name="screen_analytics_prompt_read_terms_content_link">"hier"</string>
|
||||||
<string name="screen_analytics_prompt_settings">"Du kannst dies jederzeit deaktivieren"</string>
|
<string name="screen_analytics_prompt_settings">"Du kannst dies jederzeit deaktivieren"</string>
|
||||||
<string name="screen_analytics_prompt_third_party_sharing">"Wir geben "<b>"keine"</b>" Informationen an Dritte weiter"</string>
|
<string name="screen_analytics_prompt_third_party_sharing">"Wir geben deine Daten nicht an Dritte weiter"</string>
|
||||||
<string name="screen_analytics_prompt_title">"Hilf uns, %1$s zu verbessern"</string>
|
<string name="screen_analytics_prompt_title">"Hilf uns, %1$s zu verbessern"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_analytics_prompt_settings">"您可以在任何時候關閉它"</string>
|
<string name="screen_analytics_prompt_settings">"您可以在任何時候關閉它"</string>
|
||||||
|
<string name="screen_analytics_prompt_third_party_sharing">"我們不會和第三方分享您的資料"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
||||||
import io.element.android.features.analytics.test.FakeAnalyticsService
|
|
||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
||||||
import io.element.android.features.analytics.test.FakeAnalyticsService
|
|
||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ dependencies {
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(libs.test.robolectric)
|
testImplementation(libs.test.robolectric)
|
||||||
testImplementation(projects.features.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.libraries.mediapickers.test)
|
testImplementation(projects.libraries.mediapickers.test)
|
||||||
testImplementation(projects.libraries.mediaupload.test)
|
testImplementation(projects.libraries.mediaupload.test)
|
||||||
|
|
|
||||||
|
|
@ -63,10 +63,10 @@ class ConfigureRoomFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object Root : NavTarget
|
data object Root : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object ConfigureRoom : NavTarget
|
data object ConfigureRoom : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
|
|
||||||
|
|
@ -54,10 +54,10 @@ class CreateRoomFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object Root : NavTarget
|
data object Root : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object NewRoom : NavTarget
|
data object NewRoom : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
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.CheckableMatrixUserRow
|
||||||
import io.element.android.libraries.matrix.ui.components.CheckableUnresolvedUserRow
|
import io.element.android.libraries.matrix.ui.components.CheckableUnresolvedUserRow
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
|
|
@ -63,11 +63,11 @@ internal fun SearchMultipleUsersResultItemPreview() = ElementThemedPreview { Con
|
||||||
private fun ContentToPreview() {
|
private fun ContentToPreview() {
|
||||||
Column {
|
Column {
|
||||||
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false), isUserSelected = false)
|
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false), isUserSelected = false)
|
||||||
Divider()
|
HorizontalDivider()
|
||||||
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false), isUserSelected = true)
|
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false), isUserSelected = true)
|
||||||
Divider()
|
HorizontalDivider()
|
||||||
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true), isUserSelected = false)
|
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true), isUserSelected = false)
|
||||||
Divider()
|
HorizontalDivider()
|
||||||
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true), isUserSelected = true)
|
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 androidx.compose.ui.tooling.preview.Preview
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
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.MatrixUserRow
|
||||||
import io.element.android.libraries.matrix.ui.components.UnresolvedUserRow
|
import io.element.android.libraries.matrix.ui.components.UnresolvedUserRow
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
|
|
@ -59,7 +59,7 @@ internal fun SearchSingleUserResultItemPreview() = ElementThemedPreview { Conten
|
||||||
private fun ContentToPreview() {
|
private fun ContentToPreview() {
|
||||||
Column {
|
Column {
|
||||||
SearchSingleUserResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false))
|
SearchSingleUserResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false))
|
||||||
Divider()
|
HorizontalDivider()
|
||||||
SearchSingleUserResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true))
|
SearchSingleUserResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
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.SearchBar
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
|
@ -117,7 +117,7 @@ fun SearchUserBar(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (index < users.lastIndex) {
|
if (index < users.lastIndex) {
|
||||||
Divider()
|
HorizontalDivider()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -128,7 +128,7 @@ fun SearchUserBar(
|
||||||
onClick = { onUserSelected(searchResult.matrixUser) }
|
onClick = { onUserSelected(searchResult.matrixUser) }
|
||||||
)
|
)
|
||||||
if (index < users.lastIndex) {
|
if (index < users.lastIndex) {
|
||||||
Divider()
|
HorizontalDivider()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,5 +27,5 @@ sealed interface ConfigureRoomEvents {
|
||||||
data class RemoveFromSelection(val matrixUser: MatrixUser) : ConfigureRoomEvents
|
data class RemoveFromSelection(val matrixUser: MatrixUser) : ConfigureRoomEvents
|
||||||
data class CreateRoom(val config: CreateRoomConfig) : ConfigureRoomEvents
|
data class CreateRoom(val config: CreateRoomConfig) : ConfigureRoomEvents
|
||||||
data class HandleAvatarAction(val action: AvatarAction) : ConfigureRoomEvents
|
data class HandleAvatarAction(val action: AvatarAction) : ConfigureRoomEvents
|
||||||
object CancelCreateRoom : ConfigureRoomEvents
|
data object CancelCreateRoom : ConfigureRoomEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,5 +20,5 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
|
||||||
sealed interface CreateRoomRootEvents {
|
sealed interface CreateRoomRootEvents {
|
||||||
data class StartDM(val matrixUser: MatrixUser) : CreateRoomRootEvents
|
data class StartDM(val matrixUser: MatrixUser) : CreateRoomRootEvents
|
||||||
object CancelStartDM : CreateRoomRootEvents
|
data object CancelStartDM : CreateRoomRootEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_create_room_action_create_room">"建立聊天室"</string>
|
||||||
<string name="screen_create_room_action_invite_people">"邀請朋友使用 Element"</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_room_name_label">"聊天室名稱"</string>
|
||||||
<string name="screen_create_room_topic_label">"主題(非必填)"</string>
|
<string name="screen_create_room_topic_label">"主題(非必填)"</string>
|
||||||
<string name="screen_create_room_title">"建立聊天室"</string>
|
<string name="screen_create_room_title">"建立聊天室"</string>
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import im.vector.app.features.analytics.plan.CreatedRoom
|
import im.vector.app.features.analytics.plan.CreatedRoom
|
||||||
import io.element.android.features.analytics.test.FakeAnalyticsService
|
|
||||||
import io.element.android.features.createroom.impl.CreateRoomConfig
|
import io.element.android.features.createroom.impl.CreateRoomConfig
|
||||||
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
||||||
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
||||||
|
|
@ -38,6 +37,7 @@ import io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||||
import io.element.android.libraries.mediapickers.test.FakePickerProvider
|
import io.element.android.libraries.mediapickers.test.FakePickerProvider
|
||||||
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||||
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.mockkStatic
|
import io.mockk.mockkStatic
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import im.vector.app.features.analytics.plan.CreatedRoom
|
import im.vector.app.features.analytics.plan.CreatedRoom
|
||||||
import io.element.android.features.analytics.test.FakeAnalyticsService
|
|
||||||
import io.element.android.features.createroom.impl.userlist.FakeUserListPresenter
|
import io.element.android.features.createroom.impl.userlist.FakeUserListPresenter
|
||||||
import io.element.android.features.createroom.impl.userlist.FakeUserListPresenterFactory
|
import io.element.android.features.createroom.impl.userlist.FakeUserListPresenterFactory
|
||||||
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
||||||
|
|
@ -35,6 +34,7 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.usersearch.test.FakeUserRepository
|
import io.element.android.libraries.usersearch.test.FakeUserRepository
|
||||||
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ dependencies {
|
||||||
implementation(projects.anvilannotations)
|
implementation(projects.anvilannotations)
|
||||||
anvil(projects.anvilcodegen)
|
anvil(projects.anvilcodegen)
|
||||||
api(projects.features.ftue.api)
|
api(projects.features.ftue.api)
|
||||||
|
implementation(projects.libraries.androidutils)
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
|
|
@ -49,7 +50,7 @@ dependencies {
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.features.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
|
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.features.analytics.api.AnalyticsEntryPoint
|
import io.element.android.features.analytics.api.AnalyticsEntryPoint
|
||||||
import io.element.android.features.ftue.api.FtueEntryPoint
|
import io.element.android.features.ftue.api.FtueEntryPoint
|
||||||
|
import io.element.android.features.ftue.impl.migration.MigrationScreenNode
|
||||||
import io.element.android.features.ftue.impl.state.DefaultFtueState
|
import io.element.android.features.ftue.impl.state.DefaultFtueState
|
||||||
import io.element.android.features.ftue.impl.state.FtueStep
|
import io.element.android.features.ftue.impl.state.FtueStep
|
||||||
import io.element.android.features.ftue.impl.welcome.WelcomeNode
|
import io.element.android.features.ftue.impl.welcome.WelcomeNode
|
||||||
|
|
@ -41,6 +42,7 @@ import io.element.android.libraries.architecture.BackstackNode
|
||||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
@ -50,7 +52,7 @@ import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@ContributesNode(AppScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
class FtueFlowNode @AssistedInject constructor(
|
class FtueFlowNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
|
@ -69,13 +71,16 @@ class FtueFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object Placeholder : NavTarget
|
data object Placeholder : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object WelcomeScreen : NavTarget
|
data object MigrationScreen : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object AnalyticsOptIn : NavTarget
|
data object WelcomeScreen : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object AnalyticsOptIn : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
private val callback = plugins.filterIsInstance<FtueEntryPoint.Callback>().firstOrNull()
|
private val callback = plugins.filterIsInstance<FtueEntryPoint.Callback>().firstOrNull()
|
||||||
|
|
@ -102,6 +107,14 @@ class FtueFlowNode @AssistedInject constructor(
|
||||||
NavTarget.Placeholder -> {
|
NavTarget.Placeholder -> {
|
||||||
createNode<PlaceholderNode>(buildContext)
|
createNode<PlaceholderNode>(buildContext)
|
||||||
}
|
}
|
||||||
|
NavTarget.MigrationScreen -> {
|
||||||
|
val callback = object : MigrationScreenNode.Callback {
|
||||||
|
override fun onMigrationFinished() {
|
||||||
|
lifecycleScope.launch { moveToNextStep() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createNode<MigrationScreenNode>(buildContext, listOf(callback))
|
||||||
|
}
|
||||||
NavTarget.WelcomeScreen -> {
|
NavTarget.WelcomeScreen -> {
|
||||||
val callback = object : WelcomeNode.Callback {
|
val callback = object : WelcomeNode.Callback {
|
||||||
override fun onContinueClicked() {
|
override fun onContinueClicked() {
|
||||||
|
|
@ -117,12 +130,15 @@ class FtueFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun moveToNextStep() {
|
private fun moveToNextStep() {
|
||||||
when (ftueState.getNextStep()) {
|
when (ftueState.getNextStep()) {
|
||||||
is FtueStep.WelcomeScreen -> {
|
FtueStep.MigrationScreen -> {
|
||||||
|
backstack.newRoot(NavTarget.MigrationScreen)
|
||||||
|
}
|
||||||
|
FtueStep.WelcomeScreen -> {
|
||||||
backstack.newRoot(NavTarget.WelcomeScreen)
|
backstack.newRoot(NavTarget.WelcomeScreen)
|
||||||
}
|
}
|
||||||
is FtueStep.AnalyticsOptIn -> {
|
FtueStep.AnalyticsOptIn -> {
|
||||||
backstack.replace(NavTarget.AnalyticsOptIn)
|
backstack.replace(NavTarget.AnalyticsOptIn)
|
||||||
}
|
}
|
||||||
null -> callback?.onFtueFlowFinished()
|
null -> callback?.onFtueFlowFinished()
|
||||||
|
|
|
||||||
|
|
@ -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 androidx.annotation.VisibleForTesting
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.features.ftue.api.state.FtueState
|
import io.element.android.features.ftue.api.state.FtueState
|
||||||
|
import io.element.android.features.ftue.impl.migration.MigrationScreenStore
|
||||||
import io.element.android.features.ftue.impl.welcome.state.WelcomeScreenState
|
import io.element.android.features.ftue.impl.welcome.state.WelcomeScreenState
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
@ -30,11 +32,13 @@ import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(SessionScope::class)
|
||||||
class DefaultFtueState @Inject constructor(
|
class DefaultFtueState @Inject constructor(
|
||||||
private val coroutineScope: CoroutineScope,
|
private val coroutineScope: CoroutineScope,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
private val welcomeScreenState: WelcomeScreenState,
|
private val welcomeScreenState: WelcomeScreenState,
|
||||||
|
private val migrationScreenStore: MigrationScreenStore,
|
||||||
|
private val matrixClient: MatrixClient,
|
||||||
) : FtueState {
|
) : FtueState {
|
||||||
|
|
||||||
override val shouldDisplayFlow = MutableStateFlow(isAnyStepIncomplete())
|
override val shouldDisplayFlow = MutableStateFlow(isAnyStepIncomplete())
|
||||||
|
|
@ -42,6 +46,7 @@ class DefaultFtueState @Inject constructor(
|
||||||
override suspend fun reset() {
|
override suspend fun reset() {
|
||||||
welcomeScreenState.reset()
|
welcomeScreenState.reset()
|
||||||
analyticsService.reset()
|
analyticsService.reset()
|
||||||
|
migrationScreenStore.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
@ -52,7 +57,10 @@ class DefaultFtueState @Inject constructor(
|
||||||
|
|
||||||
fun getNextStep(currentStep: FtueStep? = null): FtueStep? =
|
fun getNextStep(currentStep: FtueStep? = null): FtueStep? =
|
||||||
when (currentStep) {
|
when (currentStep) {
|
||||||
null -> if (shouldDisplayWelcomeScreen()) FtueStep.WelcomeScreen else getNextStep(
|
null -> if (shouldDisplayMigrationScreen()) FtueStep.MigrationScreen else getNextStep(
|
||||||
|
FtueStep.MigrationScreen
|
||||||
|
)
|
||||||
|
FtueStep.MigrationScreen -> if (shouldDisplayWelcomeScreen()) FtueStep.WelcomeScreen else getNextStep(
|
||||||
FtueStep.WelcomeScreen
|
FtueStep.WelcomeScreen
|
||||||
)
|
)
|
||||||
FtueStep.WelcomeScreen -> if (needsAnalyticsOptIn()) FtueStep.AnalyticsOptIn else getNextStep(
|
FtueStep.WelcomeScreen -> if (needsAnalyticsOptIn()) FtueStep.AnalyticsOptIn else getNextStep(
|
||||||
|
|
@ -63,11 +71,16 @@ class DefaultFtueState @Inject constructor(
|
||||||
|
|
||||||
private fun isAnyStepIncomplete(): Boolean {
|
private fun isAnyStepIncomplete(): Boolean {
|
||||||
return listOf(
|
return listOf(
|
||||||
|
shouldDisplayMigrationScreen(),
|
||||||
shouldDisplayWelcomeScreen(),
|
shouldDisplayWelcomeScreen(),
|
||||||
needsAnalyticsOptIn()
|
needsAnalyticsOptIn()
|
||||||
).any { it }
|
).any { it }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun shouldDisplayMigrationScreen(): Boolean {
|
||||||
|
return migrationScreenStore.isMigrationScreenNeeded(matrixClient.sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
private fun needsAnalyticsOptIn(): Boolean {
|
private fun needsAnalyticsOptIn(): Boolean {
|
||||||
// We need this function to not be suspend, so we need to load the value through runBlocking
|
// We need this function to not be suspend, so we need to load the value through runBlocking
|
||||||
return runBlocking { analyticsService.didAskUserConsent().first().not() }
|
return runBlocking { analyticsService.didAskUserConsent().first().not() }
|
||||||
|
|
@ -89,6 +102,7 @@ class DefaultFtueState @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface FtueStep {
|
sealed interface FtueStep {
|
||||||
object WelcomeScreen : FtueStep
|
data object MigrationScreen : FtueStep
|
||||||
object AnalyticsOptIn : FtueStep
|
data object WelcomeScreen : FtueStep
|
||||||
|
data object AnalyticsOptIn : FtueStep
|
||||||
}
|
}
|
||||||
|
|
|
||||||
11
features/ftue/impl/src/main/res/values-cs/translations.xml
Normal file
11
features/ftue/impl/src/main/res/values-cs/translations.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_migration_message">"Toto je jednorázový proces, děkujeme za čekání."</string>
|
||||||
|
<string name="screen_migration_title">"Nastavení vašeho účtu"</string>
|
||||||
|
<string name="screen_welcome_bullet_1">"Hovory, hlasování, vyhledávání a další budou přidány koncem tohoto roku."</string>
|
||||||
|
<string name="screen_welcome_bullet_2">"Historie zpráv šifrovaných místností nebude v této aktualizaci k dispozici."</string>
|
||||||
|
<string name="screen_welcome_bullet_3">"Rádi bychom se od vás dozvěděli, co si o tom myslíte, dejte nám vědět prostřednictvím stránky s nastavením."</string>
|
||||||
|
<string name="screen_welcome_button">"Jdeme na to!"</string>
|
||||||
|
<string name="screen_welcome_subtitle">"Zde je to, co potřebujete vědět:"</string>
|
||||||
|
<string name="screen_welcome_title">"Vítá vás %1$s!"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_welcome_bullet_1">"Anrufe, Standortfreigabe, Suche und mehr werden später in diesem Jahr hinzugefügt."</string>
|
<string name="screen_migration_message">"Dies ist ein einmaliger Vorgang, danke fürs Warten."</string>
|
||||||
|
<string name="screen_migration_title">"Dein Konto einrichten"</string>
|
||||||
|
<string name="screen_welcome_bullet_1">"Anrufe, Umfragen, Suche und mehr werden später in diesem Jahr hinzugefügt."</string>
|
||||||
<string name="screen_welcome_bullet_2">"Der Nachrichtenverlauf für verschlüsselte Räume wird in diesem Update nicht verfügbar sein."</string>
|
<string name="screen_welcome_bullet_2">"Der Nachrichtenverlauf für verschlüsselte Räume wird in diesem Update nicht verfügbar sein."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Wir würden uns freuen, wenn du uns über die Einstellungsseite deine Meinung mitteilst."</string>
|
<string name="screen_welcome_bullet_3">"Wir würden uns freuen, wenn du uns über die Einstellungsseite deine Meinung mitteilst."</string>
|
||||||
<string name="screen_welcome_button">"Los geht\'s!"</string>
|
<string name="screen_welcome_button">"Los geht\'s!"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_migration_message">"Ce processus n’a besoin d’être fait qu’une seule fois, merci de patienter."</string>
|
||||||
|
<string name="screen_migration_title">"Configuration de votre compte."</string>
|
||||||
<string name="screen_welcome_bullet_2">"L’historique des messages pour les salons chiffrés ne sera pas disponible dans cette mise à jour."</string>
|
<string name="screen_welcome_bullet_2">"L’historique des messages pour les salons chiffrés ne sera pas disponible dans cette mise à jour."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Nous serions ravis d’avoir votre avis, n’hésitez pas à nous le partager via la page des paramètres."</string>
|
<string name="screen_welcome_bullet_3">"Nous serions ravis d’avoir votre avis, n’hésitez pas à nous le partager via la page des paramètres."</string>
|
||||||
<string name="screen_welcome_button">"C’est parti !"</string>
|
<string name="screen_welcome_button">"C’est parti !"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_migration_message">"Это одноразовый процесс, спасибо, что подождали."</string>
|
||||||
|
<string name="screen_migration_title">"Настройка учетной записи."</string>
|
||||||
<string name="screen_welcome_bullet_1">"Звонки, опросы, поиск и многое другое будут добавлены позже в этом году."</string>
|
<string name="screen_welcome_bullet_1">"Звонки, опросы, поиск и многое другое будут добавлены позже в этом году."</string>
|
||||||
<string name="screen_welcome_bullet_2">"История сообщений для зашифрованных комнат в этом обновлении будет недоступна."</string>
|
<string name="screen_welcome_bullet_2">"История сообщений для зашифрованных комнат в этом обновлении будет недоступна."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Мы будем рады услышать ваше мнение, сообщите нам об этом через страницу настроек."</string>
|
<string name="screen_welcome_bullet_3">"Мы будем рады услышать ваше мнение, сообщите нам об этом через страницу настроек."</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_migration_message">"Ide o jednorazový proces, ďakujeme za trpezlivosť."</string>
|
||||||
|
<string name="screen_migration_title">"Nastavenie vášho účtu."</string>
|
||||||
<string name="screen_welcome_bullet_1">"Hovory, ankety, vyhľadávanie a ďalšie funkcie pribudnú neskôr v tomto roku."</string>
|
<string name="screen_welcome_bullet_1">"Hovory, ankety, vyhľadávanie a ďalšie funkcie pribudnú neskôr v tomto roku."</string>
|
||||||
<string name="screen_welcome_bullet_2">"História správ pre zašifrované miestnosti nebude v tejto aktualizácii k dispozícii."</string>
|
<string name="screen_welcome_bullet_2">"História správ pre zašifrované miestnosti nebude v tejto aktualizácii k dispozícii."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Radi by sme od vás počuli, dajte nám vedieť, čo si myslíte, prostredníctvom stránky nastavení."</string>
|
<string name="screen_welcome_bullet_3">"Radi by sme od vás počuli, dajte nám vedieť, čo si myslíte, prostredníctvom stránky nastavení."</string>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_migration_title">"設定您的帳號"</string>
|
||||||
<string name="screen_welcome_button">"開始吧!"</string>
|
<string name="screen_welcome_button">"開始吧!"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_migration_message">"This is a one time process, thanks for waiting."</string>
|
||||||
|
<string name="screen_migration_title">"Setting up your account."</string>
|
||||||
<string name="screen_welcome_bullet_1">"Calls, polls, search and more will be added later this year."</string>
|
<string name="screen_welcome_bullet_1">"Calls, polls, search and more will be added later this year."</string>
|
||||||
<string name="screen_welcome_bullet_2">"Message history for encrypted rooms won’t be available in this update."</string>
|
<string name="screen_welcome_bullet_2">"Message history for encrypted rooms won’t be available in this update."</string>
|
||||||
<string name="screen_welcome_bullet_3">"We’d love to hear from you, let us know what you think via the settings page."</string>
|
<string name="screen_welcome_bullet_3">"We’d love to hear from you, let us know what you think via the settings page."</string>
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,16 @@
|
||||||
package io.element.android.features.ftue.impl
|
package io.element.android.features.ftue.impl
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.analytics.test.FakeAnalyticsService
|
import io.element.android.features.ftue.impl.migration.InMemoryMigrationScreenStore
|
||||||
|
import io.element.android.features.ftue.impl.migration.MigrationScreenStore
|
||||||
import io.element.android.features.ftue.impl.state.DefaultFtueState
|
import io.element.android.features.ftue.impl.state.DefaultFtueState
|
||||||
import io.element.android.features.ftue.impl.state.FtueStep
|
import io.element.android.features.ftue.impl.state.FtueStep
|
||||||
import io.element.android.features.ftue.impl.welcome.state.FakeWelcomeState
|
import io.element.android.features.ftue.impl.welcome.state.FakeWelcomeState
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
|
|
@ -45,12 +50,14 @@ class DefaultFtueStateTests {
|
||||||
fun `given all checks being true, should display flow is false`() = runTest {
|
fun `given all checks being true, should display flow is false`() = runTest {
|
||||||
val welcomeState = FakeWelcomeState()
|
val welcomeState = FakeWelcomeState()
|
||||||
val analyticsService = FakeAnalyticsService()
|
val analyticsService = FakeAnalyticsService()
|
||||||
|
val migrationScreenStore = InMemoryMigrationScreenStore()
|
||||||
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||||
|
|
||||||
val state = createState(coroutineScope, welcomeState, analyticsService)
|
val state = createState(coroutineScope, welcomeState, analyticsService, migrationScreenStore)
|
||||||
|
|
||||||
welcomeState.setWelcomeScreenShown()
|
welcomeState.setWelcomeScreenShown()
|
||||||
analyticsService.setDidAskUserConsent()
|
analyticsService.setDidAskUserConsent()
|
||||||
|
migrationScreenStore.setMigrationScreenShown(A_SESSION_ID)
|
||||||
state.updateState()
|
state.updateState()
|
||||||
|
|
||||||
assertThat(state.shouldDisplayFlow.value).isFalse()
|
assertThat(state.shouldDisplayFlow.value).isFalse()
|
||||||
|
|
@ -63,16 +70,21 @@ class DefaultFtueStateTests {
|
||||||
fun `traverse flow`() = runTest {
|
fun `traverse flow`() = runTest {
|
||||||
val welcomeState = FakeWelcomeState()
|
val welcomeState = FakeWelcomeState()
|
||||||
val analyticsService = FakeAnalyticsService()
|
val analyticsService = FakeAnalyticsService()
|
||||||
|
val migrationScreenStore = InMemoryMigrationScreenStore()
|
||||||
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||||
|
|
||||||
val state = createState(coroutineScope, welcomeState, analyticsService)
|
val state = createState(coroutineScope, welcomeState, analyticsService, migrationScreenStore)
|
||||||
val steps = mutableListOf<FtueStep?>()
|
val steps = mutableListOf<FtueStep?>()
|
||||||
|
|
||||||
// First step, welcome screen
|
// First step, migration screen
|
||||||
|
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||||
|
migrationScreenStore.setMigrationScreenShown(A_SESSION_ID)
|
||||||
|
|
||||||
|
// Second step, welcome screen
|
||||||
steps.add(state.getNextStep(steps.lastOrNull()))
|
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||||
welcomeState.setWelcomeScreenShown()
|
welcomeState.setWelcomeScreenShown()
|
||||||
|
|
||||||
// Second step, analytics opt in
|
// Third step, analytics opt in
|
||||||
steps.add(state.getNextStep(steps.lastOrNull()))
|
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||||
analyticsService.setDidAskUserConsent()
|
analyticsService.setDidAskUserConsent()
|
||||||
|
|
||||||
|
|
@ -80,6 +92,7 @@ class DefaultFtueStateTests {
|
||||||
steps.add(state.getNextStep(steps.lastOrNull()))
|
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||||
|
|
||||||
assertThat(steps).containsExactly(
|
assertThat(steps).containsExactly(
|
||||||
|
FtueStep.MigrationScreen,
|
||||||
FtueStep.WelcomeScreen,
|
FtueStep.WelcomeScreen,
|
||||||
FtueStep.AnalyticsOptIn,
|
FtueStep.AnalyticsOptIn,
|
||||||
null, // Final state
|
null, // Final state
|
||||||
|
|
@ -93,7 +106,16 @@ class DefaultFtueStateTests {
|
||||||
fun `if a check for a step is true, start from the next one`() = runTest {
|
fun `if a check for a step is true, start from the next one`() = runTest {
|
||||||
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||||
val analyticsService = FakeAnalyticsService()
|
val analyticsService = FakeAnalyticsService()
|
||||||
val state = createState(coroutineScope = coroutineScope, analyticsService = analyticsService)
|
val migrationScreenStore = InMemoryMigrationScreenStore()
|
||||||
|
|
||||||
|
val state = createState(
|
||||||
|
coroutineScope = coroutineScope,
|
||||||
|
analyticsService = analyticsService,
|
||||||
|
migrationScreenStore = migrationScreenStore,
|
||||||
|
)
|
||||||
|
|
||||||
|
migrationScreenStore.setMigrationScreenShown(A_SESSION_ID)
|
||||||
|
assertThat(state.getNextStep()).isEqualTo(FtueStep.WelcomeScreen)
|
||||||
|
|
||||||
state.setWelcomeScreenShown()
|
state.setWelcomeScreenShown()
|
||||||
assertThat(state.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn)
|
assertThat(state.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn)
|
||||||
|
|
@ -108,7 +130,14 @@ class DefaultFtueStateTests {
|
||||||
private fun createState(
|
private fun createState(
|
||||||
coroutineScope: CoroutineScope,
|
coroutineScope: CoroutineScope,
|
||||||
welcomeState: FakeWelcomeState = FakeWelcomeState(),
|
welcomeState: FakeWelcomeState = FakeWelcomeState(),
|
||||||
analyticsService: AnalyticsService = FakeAnalyticsService()
|
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
||||||
) = DefaultFtueState(coroutineScope, analyticsService, welcomeState)
|
migrationScreenStore: MigrationScreenStore = InMemoryMigrationScreenStore(),
|
||||||
|
matrixClient: MatrixClient = FakeMatrixClient(),
|
||||||
|
) = DefaultFtueState(
|
||||||
|
coroutineScope = coroutineScope,
|
||||||
|
analyticsService = analyticsService,
|
||||||
|
welcomeScreenState = welcomeState,
|
||||||
|
migrationScreenStore = migrationScreenStore,
|
||||||
|
matrixClient = matrixClient,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.ftue.impl.migration
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
|
||||||
|
class InMemoryMigrationScreenStore : MigrationScreenStore {
|
||||||
|
private val store = mutableMapOf<SessionId, Boolean>()
|
||||||
|
|
||||||
|
override fun isMigrationScreenNeeded(sessionId: SessionId): Boolean {
|
||||||
|
// If store does not have key return true, else return the opposite of the value
|
||||||
|
return store[sessionId]?.not() ?: true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setMigrationScreenShown(sessionId: SessionId) {
|
||||||
|
store[sessionId] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun reset() {
|
||||||
|
store.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.ftue.impl.migration
|
||||||
|
|
||||||
|
import app.cash.molecule.RecompositionMode
|
||||||
|
import app.cash.molecule.moleculeFlow
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class MigrationScreenPresenterTest {
|
||||||
|
@Test
|
||||||
|
fun `present - initial`() = runTest {
|
||||||
|
val presenter = createPresenter()
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.isMigrating).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - migration end`() = runTest {
|
||||||
|
val matrixClient = FakeMatrixClient()
|
||||||
|
val migrationScreenStore = InMemoryMigrationScreenStore()
|
||||||
|
val presenter = createPresenter(matrixClient, migrationScreenStore)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.isMigrating).isTrue()
|
||||||
|
assertThat(migrationScreenStore.isMigrationScreenNeeded(A_SESSION_ID)).isTrue()
|
||||||
|
// Simulate room list loaded
|
||||||
|
(matrixClient.roomListService as FakeRoomListService).postState(RoomListService.State.Running)
|
||||||
|
val nextState = awaitItem()
|
||||||
|
assertThat(nextState.isMigrating).isFalse()
|
||||||
|
assertThat(migrationScreenStore.isMigrationScreenNeeded(A_SESSION_ID)).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createPresenter(
|
||||||
|
matrixClient: MatrixClient = FakeMatrixClient(),
|
||||||
|
migrationScreenStore: MigrationScreenStore = InMemoryMigrationScreenStore(),
|
||||||
|
) = MigrationScreenPresenter(
|
||||||
|
matrixClient,
|
||||||
|
migrationScreenStore,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -53,7 +53,7 @@ dependencies {
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.libraries.push.test)
|
testImplementation(projects.libraries.push.test)
|
||||||
testImplementation(projects.features.invitelist.test)
|
testImplementation(projects.features.invitelist.test)
|
||||||
testImplementation(projects.features.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
|
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,14 +19,12 @@ package io.element.android.features.invitelist.impl
|
||||||
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
|
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
|
||||||
|
|
||||||
sealed interface InviteListEvents {
|
sealed interface InviteListEvents {
|
||||||
|
|
||||||
data class AcceptInvite(val invite: InviteListInviteSummary) : InviteListEvents
|
data class AcceptInvite(val invite: InviteListInviteSummary) : InviteListEvents
|
||||||
data class DeclineInvite(val invite: InviteListInviteSummary) : InviteListEvents
|
data class DeclineInvite(val invite: InviteListInviteSummary) : InviteListEvents
|
||||||
|
|
||||||
object ConfirmDeclineInvite: InviteListEvents
|
data object ConfirmDeclineInvite: InviteListEvents
|
||||||
object CancelDeclineInvite: InviteListEvents
|
data object CancelDeclineInvite: InviteListEvents
|
||||||
|
|
||||||
object DismissAcceptError: InviteListEvents
|
|
||||||
object DismissDeclineError: InviteListEvents
|
|
||||||
|
|
||||||
|
data object DismissAcceptError: InviteListEvents
|
||||||
|
data object DismissDeclineError: InviteListEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,6 @@ data class InviteListState(
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed interface InviteDeclineConfirmationDialog {
|
sealed interface InviteDeclineConfirmationDialog {
|
||||||
object Hidden : InviteDeclineConfirmationDialog
|
data object Hidden : InviteDeclineConfirmationDialog
|
||||||
data class Visible(val isDirect: Boolean, val name: String) : InviteDeclineConfirmationDialog
|
data class Visible(val isDirect: Boolean, val name: String) : InviteDeclineConfirmationDialog
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
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.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
|
@ -161,7 +161,7 @@ fun InviteListContent(
|
||||||
)
|
)
|
||||||
|
|
||||||
if (index != state.inviteList.lastIndex) {
|
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.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth
|
import com.google.common.truth.Truth
|
||||||
import io.element.android.features.analytics.test.FakeAnalyticsService
|
|
||||||
import io.element.android.features.invitelist.api.SeenInvitesStore
|
import io.element.android.features.invitelist.api.SeenInvitesStore
|
||||||
import io.element.android.features.invitelist.test.FakeSeenInvitesStore
|
import io.element.android.features.invitelist.test.FakeSeenInvitesStore
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
|
|
@ -44,6 +43,7 @@ import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
||||||
import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager
|
import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
sealed interface LeaveRoomEvent {
|
sealed interface LeaveRoomEvent {
|
||||||
data class ShowConfirmation(val roomId: RoomId) : LeaveRoomEvent
|
data class ShowConfirmation(val roomId: RoomId) : LeaveRoomEvent
|
||||||
object HideConfirmation : LeaveRoomEvent
|
data object HideConfirmation : LeaveRoomEvent
|
||||||
data class LeaveRoom(val roomId: RoomId) : LeaveRoomEvent
|
data class LeaveRoom(val roomId: RoomId) : LeaveRoomEvent
|
||||||
object HideError : LeaveRoomEvent
|
data object HideError : LeaveRoomEvent
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,19 +25,19 @@ data class LeaveRoomState(
|
||||||
val eventSink: (LeaveRoomEvent) -> Unit = {},
|
val eventSink: (LeaveRoomEvent) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
sealed interface Confirmation {
|
sealed interface Confirmation {
|
||||||
object Hidden : Confirmation
|
data object Hidden : Confirmation
|
||||||
data class Generic(val roomId: RoomId) : Confirmation
|
data class Generic(val roomId: RoomId) : Confirmation
|
||||||
data class PrivateRoom(val roomId: RoomId) : Confirmation
|
data class PrivateRoom(val roomId: RoomId) : Confirmation
|
||||||
data class LastUserInRoom(val roomId: RoomId) : Confirmation
|
data class LastUserInRoom(val roomId: RoomId) : Confirmation
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface Progress {
|
sealed interface Progress {
|
||||||
object Hidden : Progress
|
data object Hidden : Progress
|
||||||
object Shown : Progress
|
data object Shown : Progress
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface Error {
|
sealed interface Error {
|
||||||
object Hidden : Error
|
data object Hidden : Error
|
||||||
object Shown : Error
|
data object Shown : Error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,8 @@ fun StaticMapView(
|
||||||
StaticMapPlaceholder(
|
StaticMapPlaceholder(
|
||||||
showProgress = painter.state is AsyncImagePainter.State.Loading,
|
showProgress = painter.state is AsyncImagePainter.State.Loading,
|
||||||
contentDescription = contentDescription,
|
contentDescription = contentDescription,
|
||||||
modifier = Modifier.size(width = maxWidth, height = maxHeight),
|
width = maxWidth,
|
||||||
|
height = maxHeight,
|
||||||
onLoadMapClick = { retryHash++ }
|
onLoadMapClick = { retryHash++ }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.features.location.api.R
|
import io.element.android.features.location.api.R
|
||||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||||
|
|
@ -44,34 +45,34 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
internal fun StaticMapPlaceholder(
|
internal fun StaticMapPlaceholder(
|
||||||
showProgress: Boolean,
|
showProgress: Boolean,
|
||||||
contentDescription: String?,
|
contentDescription: String?,
|
||||||
|
width: Dp,
|
||||||
|
height: Dp,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onLoadMapClick: () -> Unit,
|
onLoadMapClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = modifier
|
||||||
|
.size(width = width, height = height)
|
||||||
|
.then(if (showProgress) Modifier else Modifier.clickable(onClick = onLoadMapClick))
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.drawable.blurred_map),
|
painter = painterResource(id = R.drawable.blurred_map),
|
||||||
contentDescription = contentDescription,
|
contentDescription = contentDescription,
|
||||||
modifier = modifier,
|
|
||||||
contentScale = ContentScale.FillBounds,
|
contentScale = ContentScale.FillBounds,
|
||||||
|
modifier = Modifier.size(width = width, height = height)
|
||||||
)
|
)
|
||||||
if (showProgress) {
|
if (showProgress) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
} else {
|
} else {
|
||||||
Box(
|
Column(
|
||||||
modifier = modifier.clickable(onClick = onLoadMapClick),
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Icon(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
imageVector = Icons.Default.Refresh,
|
||||||
) {
|
contentDescription = null
|
||||||
Icon(
|
)
|
||||||
imageVector = Icons.Default.Refresh,
|
Text(text = stringResource(id = CommonStrings.action_static_map_load))
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
Text(text = stringResource(id = CommonStrings.action_static_map_load))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -85,7 +86,8 @@ internal fun StaticMapPlaceholderPreview(
|
||||||
StaticMapPlaceholder(
|
StaticMapPlaceholder(
|
||||||
showProgress = values,
|
showProgress = values,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(400.dp),
|
width = 400.dp,
|
||||||
|
height = 400.dp,
|
||||||
onLoadMapClick = {},
|
onLoadMapClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,6 @@ dependencies {
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.features.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.features.messages.test)
|
testImplementation(projects.features.messages.test)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.location.impl
|
package io.element.android.features.location.impl.common
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.view.Gravity
|
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.
|
* 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.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
|
@ -22,7 +22,6 @@ import android.net.Uri
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.features.location.api.Location
|
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.androidutils.system.openAppSettingsPage
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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
|
import io.element.android.features.location.api.Location
|
||||||
|
|
||||||
|
|
@ -14,8 +14,8 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.location.impl.permissions
|
package io.element.android.features.location.impl.common.permissions
|
||||||
|
|
||||||
sealed interface PermissionsEvents {
|
sealed interface PermissionsEvents {
|
||||||
object RequestPermissions : PermissionsEvents
|
data object RequestPermissions : PermissionsEvents
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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 androidx.compose.runtime.Composable
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.location.impl.permissions
|
package io.element.android.features.location.impl.common.permissions
|
||||||
|
|
||||||
data class PermissionsState(
|
data class PermissionsState(
|
||||||
val permissions: Permissions = Permissions.NoneGranted,
|
val permissions: Permissions = Permissions.NoneGranted,
|
||||||
|
|
@ -22,9 +22,9 @@ data class PermissionsState(
|
||||||
val eventSink: (PermissionsEvents) -> Unit = {},
|
val eventSink: (PermissionsEvents) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
sealed interface Permissions {
|
sealed interface Permissions {
|
||||||
object AllGranted : Permissions
|
data object AllGranted : Permissions
|
||||||
object SomeGranted : Permissions
|
data object SomeGranted : Permissions
|
||||||
object NoneGranted : Permissions
|
data object NoneGranted : Permissions
|
||||||
}
|
}
|
||||||
|
|
||||||
val isAnyGranted: Boolean
|
val isAnyGranted: Boolean
|
||||||
|
|
@ -30,13 +30,9 @@ sealed interface SendLocationEvents {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
object SwitchToMyLocationMode : SendLocationEvents
|
data object SwitchToMyLocationMode : SendLocationEvents
|
||||||
|
data object SwitchToPinLocationMode : SendLocationEvents
|
||||||
object SwitchToPinLocationMode : SendLocationEvents
|
data object DismissDialog : SendLocationEvents
|
||||||
|
data object RequestPermissions : SendLocationEvents
|
||||||
object DismissDialog : SendLocationEvents
|
data object OpenAppSettings : SendLocationEvents
|
||||||
|
|
||||||
object RequestPermissions : SendLocationEvents
|
|
||||||
|
|
||||||
object OpenAppSettings : SendLocationEvents
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,11 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import im.vector.app.features.analytics.plan.Composer
|
import im.vector.app.features.analytics.plan.Composer
|
||||||
import io.element.android.features.location.impl.MapDefaults
|
import io.element.android.features.location.impl.common.MapDefaults
|
||||||
import io.element.android.features.location.impl.permissions.PermissionsEvents
|
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
|
||||||
import io.element.android.features.location.impl.permissions.PermissionsPresenter
|
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
|
||||||
import io.element.android.features.location.impl.permissions.PermissionsState
|
import io.element.android.features.location.impl.common.permissions.PermissionsState
|
||||||
import io.element.android.features.location.impl.show.LocationActions
|
import io.element.android.features.location.impl.common.actions.LocationActions
|
||||||
import io.element.android.features.messages.api.MessageComposerContext
|
import io.element.android.features.messages.api.MessageComposerContext
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
|
|
|
||||||
|
|
@ -24,13 +24,13 @@ data class SendLocationState(
|
||||||
val eventSink: (SendLocationEvents) -> Unit = {},
|
val eventSink: (SendLocationEvents) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
sealed interface Mode {
|
sealed interface Mode {
|
||||||
object SenderLocation : Mode
|
data object SenderLocation : Mode
|
||||||
object PinLocation : Mode
|
data object PinLocation : Mode
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface Dialog {
|
sealed interface Dialog {
|
||||||
object None : Dialog
|
data object None : Dialog
|
||||||
object PermissionRationale : Dialog
|
data object PermissionRationale : Dialog
|
||||||
object PermissionDenied : Dialog
|
data object PermissionDenied : Dialog
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.Location
|
||||||
import io.element.android.features.location.api.internal.centerBottomEdge
|
import io.element.android.features.location.api.internal.centerBottomEdge
|
||||||
import io.element.android.features.location.api.internal.rememberTileStyleUrl
|
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.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.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.DayNightPreviews
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
||||||
|
|
@ -232,33 +233,3 @@ internal fun SendLocationViewPreview(
|
||||||
navigateUp = {},
|
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
|
package io.element.android.features.location.impl.show
|
||||||
|
|
||||||
sealed interface ShowLocationEvents {
|
sealed interface ShowLocationEvents {
|
||||||
object Share : ShowLocationEvents
|
data object Share : ShowLocationEvents
|
||||||
data class TrackMyLocation(val enabled: Boolean) : ShowLocationEvents
|
data class TrackMyLocation(val enabled: Boolean) : ShowLocationEvents
|
||||||
|
data object DismissDialog : ShowLocationEvents
|
||||||
|
data object RequestPermissions : ShowLocationEvents
|
||||||
|
data object OpenAppSettings : ShowLocationEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@
|
||||||
package io.element.android.features.location.impl.show
|
package io.element.android.features.location.impl.show
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
|
@ -25,14 +27,18 @@ import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.features.location.api.Location
|
import io.element.android.features.location.api.Location
|
||||||
import io.element.android.features.location.impl.MapDefaults
|
import io.element.android.features.location.impl.common.MapDefaults
|
||||||
import io.element.android.features.location.impl.permissions.PermissionsPresenter
|
import io.element.android.features.location.impl.common.actions.LocationActions
|
||||||
import io.element.android.features.location.impl.permissions.PermissionsState
|
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.architecture.Presenter
|
||||||
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
|
|
||||||
class ShowLocationPresenter @AssistedInject constructor(
|
class ShowLocationPresenter @AssistedInject constructor(
|
||||||
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||||
private val actions: LocationActions,
|
private val locationActions: LocationActions,
|
||||||
|
private val buildMeta: BuildMeta,
|
||||||
@Assisted private val location: Location,
|
@Assisted private val location: Location,
|
||||||
@Assisted private val description: String?
|
@Assisted private val description: String?
|
||||||
) : Presenter<ShowLocationState> {
|
) : Presenter<ShowLocationState> {
|
||||||
|
|
@ -48,19 +54,47 @@ class ShowLocationPresenter @AssistedInject constructor(
|
||||||
override fun present(): ShowLocationState {
|
override fun present(): ShowLocationState {
|
||||||
val permissionsState: PermissionsState = permissionsPresenter.present()
|
val permissionsState: PermissionsState = permissionsPresenter.present()
|
||||||
var isTrackMyLocation by remember { mutableStateOf(false) }
|
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) {
|
fun handleEvents(event: ShowLocationEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
ShowLocationEvents.Share -> actions.share(location, description)
|
ShowLocationEvents.Share -> locationActions.share(location, description)
|
||||||
is ShowLocationEvents.TrackMyLocation -> isTrackMyLocation = event.enabled
|
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(
|
return ShowLocationState(
|
||||||
|
permissionDialog = permissionDialog,
|
||||||
location = location,
|
location = location,
|
||||||
description = description,
|
description = description,
|
||||||
hasLocationPermission = permissionsState.isAnyGranted,
|
hasLocationPermission = permissionsState.isAnyGranted,
|
||||||
isTrackMyLocation = isTrackMyLocation,
|
isTrackMyLocation = isTrackMyLocation,
|
||||||
|
appName = appName,
|
||||||
eventSink = ::handleEvents,
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,17 @@ package io.element.android.features.location.impl.show
|
||||||
import io.element.android.features.location.api.Location
|
import io.element.android.features.location.api.Location
|
||||||
|
|
||||||
data class ShowLocationState(
|
data class ShowLocationState(
|
||||||
|
val permissionDialog: Dialog,
|
||||||
val location: Location,
|
val location: Location,
|
||||||
val description: String?,
|
val description: String?,
|
||||||
val hasLocationPermission: Boolean,
|
val hasLocationPermission: Boolean,
|
||||||
val isTrackMyLocation: Boolean,
|
val isTrackMyLocation: Boolean,
|
||||||
|
val appName: String,
|
||||||
val eventSink: (ShowLocationEvents) -> Unit,
|
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 androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.location.api.Location
|
import io.element.android.features.location.api.Location
|
||||||
|
|
||||||
|
private const val APP_NAME = "ApplicationName"
|
||||||
|
|
||||||
class ShowLocationStateProvider : PreviewParameterProvider<ShowLocationState> {
|
class ShowLocationStateProvider : PreviewParameterProvider<ShowLocationState> {
|
||||||
override val values: Sequence<ShowLocationState>
|
override val values: Sequence<ShowLocationState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
ShowLocationState(
|
ShowLocationState(
|
||||||
|
ShowLocationState.Dialog.None,
|
||||||
Location(1.23, 2.34, 4f),
|
Location(1.23, 2.34, 4f),
|
||||||
description = null,
|
description = null,
|
||||||
hasLocationPermission = false,
|
hasLocationPermission = false,
|
||||||
isTrackMyLocation = false,
|
isTrackMyLocation = false,
|
||||||
|
appName = APP_NAME,
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
),
|
),
|
||||||
ShowLocationState(
|
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),
|
Location(1.23, 2.34, 4f),
|
||||||
description = null,
|
description = null,
|
||||||
hasLocationPermission = true,
|
hasLocationPermission = true,
|
||||||
isTrackMyLocation = false,
|
isTrackMyLocation = false,
|
||||||
|
appName = APP_NAME,
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
),
|
),
|
||||||
ShowLocationState(
|
ShowLocationState(
|
||||||
|
ShowLocationState.Dialog.None,
|
||||||
Location(1.23, 2.34, 4f),
|
Location(1.23, 2.34, 4f),
|
||||||
description = null,
|
description = null,
|
||||||
hasLocationPermission = true,
|
hasLocationPermission = true,
|
||||||
isTrackMyLocation = true,
|
isTrackMyLocation = true,
|
||||||
|
appName = APP_NAME,
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
),
|
),
|
||||||
ShowLocationState(
|
ShowLocationState(
|
||||||
|
ShowLocationState.Dialog.None,
|
||||||
Location(1.23, 2.34, 4f),
|
Location(1.23, 2.34, 4f),
|
||||||
description = "My favourite place!",
|
description = "My favourite place!",
|
||||||
hasLocationPermission = false,
|
hasLocationPermission = false,
|
||||||
isTrackMyLocation = false,
|
isTrackMyLocation = false,
|
||||||
|
appName = APP_NAME,
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
),
|
),
|
||||||
ShowLocationState(
|
ShowLocationState(
|
||||||
|
ShowLocationState.Dialog.None,
|
||||||
Location(1.23, 2.34, 4f),
|
Location(1.23, 2.34, 4f),
|
||||||
description = "For some reason I decided to to write a small essay that wraps at just two lines!",
|
description = "For some reason I decided to to write a small essay that wraps at just two lines!",
|
||||||
hasLocationPermission = false,
|
hasLocationPermission = false,
|
||||||
isTrackMyLocation = false,
|
isTrackMyLocation = false,
|
||||||
|
appName = APP_NAME,
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
),
|
),
|
||||||
ShowLocationState(
|
ShowLocationState(
|
||||||
|
ShowLocationState.Dialog.None,
|
||||||
Location(1.23, 2.34, 4f),
|
Location(1.23, 2.34, 4f),
|
||||||
description = "For some reason I decided to write a small essay in the location description. " +
|
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!",
|
"It is so long that it will wrap onto more than two lines!",
|
||||||
hasLocationPermission = false,
|
hasLocationPermission = false,
|
||||||
isTrackMyLocation = false,
|
isTrackMyLocation = false,
|
||||||
|
appName = APP_NAME,
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,9 @@ import androidx.compose.ui.unit.dp
|
||||||
import com.mapbox.mapboxsdk.camera.CameraPosition
|
import com.mapbox.mapboxsdk.camera.CameraPosition
|
||||||
import com.mapbox.mapboxsdk.geometry.LatLng
|
import com.mapbox.mapboxsdk.geometry.LatLng
|
||||||
import io.element.android.features.location.api.internal.rememberTileStyleUrl
|
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.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
|
@ -70,6 +72,20 @@ fun ShowLocationView(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onBackPressed: () -> Unit = {},
|
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 {
|
val cameraPositionState = rememberCameraPositionState {
|
||||||
position = CameraPosition.Builder()
|
position = CameraPosition.Builder()
|
||||||
.target(LatLng(state.location.lat, state.location.lon))
|
.target(LatLng(state.location.lat, state.location.lon))
|
||||||
|
|
@ -116,14 +132,12 @@ fun ShowLocationView(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
if (state.hasLocationPermission) {
|
FloatingActionButton(
|
||||||
FloatingActionButton(
|
onClick = { state.eventSink(ShowLocationEvents.TrackMyLocation(true)) },
|
||||||
onClick = { state.eventSink(ShowLocationEvents.TrackMyLocation(true)) },
|
) {
|
||||||
) {
|
when (state.isTrackMyLocation) {
|
||||||
when (state.isTrackMyLocation) {
|
false -> Icon(imageVector = Icons.Default.LocationSearching, contentDescription = null)
|
||||||
false -> Icon(imageVector = Icons.Default.LocationSearching, contentDescription = null)
|
true -> Icon(imageVector = Icons.Default.MyLocation, contentDescription = null)
|
||||||
true -> Icon(imageVector = Icons.Default.MyLocation, contentDescription = null)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,10 @@
|
||||||
* limitations under the License.
|
* 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 com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.location.api.Location
|
import io.element.android.features.location.api.Location
|
||||||
import io.element.android.features.location.impl.buildUrl
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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
|
import io.element.android.features.location.api.Location
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue