Merge branch 'release/0.5.0' into main
7
.github/pull_request_template.md
vendored
|
|
@ -1,12 +1,5 @@
|
||||||
<!-- Please read [CONTRIBUTING.md](https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->
|
<!-- Please read [CONTRIBUTING.md](https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->
|
||||||
|
|
||||||
## Type of change
|
|
||||||
|
|
||||||
- [ ] Feature
|
|
||||||
- [ ] Bugfix
|
|
||||||
- [ ] Technical
|
|
||||||
- [ ] Other :
|
|
||||||
|
|
||||||
## Content
|
## Content
|
||||||
|
|
||||||
<!-- Describe shortly what has been changed -->
|
<!-- Describe shortly what has been changed -->
|
||||||
|
|
|
||||||
2
.github/workflows/build_enterprise.yml
vendored
|
|
@ -17,7 +17,7 @@ jobs:
|
||||||
name: Build Enterprise APKs
|
name: Build Enterprise APKs
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Skip in forks
|
# Skip in forks
|
||||||
if: github.repository == 'element-hq/element-x-android'
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
variant: [debug, release, nightly]
|
variant: [debug, release, nightly]
|
||||||
|
|
|
||||||
4
.github/workflows/danger.yml
vendored
|
|
@ -6,6 +6,8 @@ jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Danger main check
|
name: Danger main check
|
||||||
|
# Skip in forks, it doesn't work even with the fallback token
|
||||||
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Add SSH private keys for submodule repositories
|
- name: Add SSH private keys for submodule repositories
|
||||||
|
|
@ -13,7 +15,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
|
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
|
||||||
- name: Clone submodules
|
- name: Clone submodules
|
||||||
if: github.repository == 'element-hq/element-x-android'
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- run: |
|
- run: |
|
||||||
npm install --save-dev @babel/plugin-transform-flow-strip-types
|
npm install --save-dev @babel/plugin-transform-flow-strip-types
|
||||||
|
|
|
||||||
2
.github/workflows/generate_github_pages.yml
vendored
|
|
@ -9,7 +9,7 @@ jobs:
|
||||||
generate-github-pages:
|
generate-github-pages:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Skip in forks
|
# Skip in forks
|
||||||
if: github.repository == 'element-hq/element-x-android'
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
steps:
|
steps:
|
||||||
- name: ⏬ Checkout with LFS
|
- name: ⏬ Checkout with LFS
|
||||||
uses: nschloe/action-cached-lfs-checkout@v1.2.2
|
uses: nschloe/action-cached-lfs-checkout@v1.2.2
|
||||||
|
|
|
||||||
2
.github/workflows/gradle-wrapper-update.yml
vendored
|
|
@ -12,7 +12,7 @@ jobs:
|
||||||
- name: Update Gradle Wrapper
|
- name: Update Gradle Wrapper
|
||||||
uses: gradle-update/update-gradle-wrapper-action@v1
|
uses: gradle-update/update-gradle-wrapper-action@v1
|
||||||
# Skip in forks
|
# Skip in forks
|
||||||
if: github.repository == 'element-hq/element-x-android'
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
target-branch: develop
|
target-branch: develop
|
||||||
|
|
|
||||||
19
.github/workflows/quality.yml
vendored
|
|
@ -20,10 +20,11 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Add SSH private keys for submodule repositories
|
- name: Add SSH private keys for submodule repositories
|
||||||
uses: webfactory/ssh-agent@v0.9.0
|
uses: webfactory/ssh-agent@v0.9.0
|
||||||
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
with:
|
with:
|
||||||
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
|
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
|
||||||
- name: Clone submodules
|
- name: Clone submodules
|
||||||
if: github.repository == 'element-hq/element-x-android'
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: Run code quality check suite
|
- name: Run code quality check suite
|
||||||
run: ./tools/check/check_code_quality.sh
|
run: ./tools/check/check_code_quality.sh
|
||||||
|
|
@ -77,10 +78,11 @@ jobs:
|
||||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||||
- name: Add SSH private keys for submodule repositories
|
- name: Add SSH private keys for submodule repositories
|
||||||
uses: webfactory/ssh-agent@v0.9.0
|
uses: webfactory/ssh-agent@v0.9.0
|
||||||
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
with:
|
with:
|
||||||
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
|
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
|
||||||
- name: Clone submodules
|
- name: Clone submodules
|
||||||
if: github.repository == 'element-hq/element-x-android'
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: Use JDK 17
|
- name: Use JDK 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
|
|
@ -116,10 +118,11 @@ jobs:
|
||||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||||
- name: Add SSH private keys for submodule repositories
|
- name: Add SSH private keys for submodule repositories
|
||||||
uses: webfactory/ssh-agent@v0.9.0
|
uses: webfactory/ssh-agent@v0.9.0
|
||||||
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
with:
|
with:
|
||||||
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
|
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
|
||||||
- name: Clone submodules
|
- name: Clone submodules
|
||||||
if: github.repository == 'element-hq/element-x-android'
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: Use JDK 17
|
- name: Use JDK 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
|
|
@ -159,10 +162,11 @@ jobs:
|
||||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||||
- name: Add SSH private keys for submodule repositories
|
- name: Add SSH private keys for submodule repositories
|
||||||
uses: webfactory/ssh-agent@v0.9.0
|
uses: webfactory/ssh-agent@v0.9.0
|
||||||
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
with:
|
with:
|
||||||
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
|
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
|
||||||
- name: Clone submodules
|
- name: Clone submodules
|
||||||
if: github.repository == 'element-hq/element-x-android'
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: Use JDK 17
|
- name: Use JDK 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
|
|
@ -198,10 +202,11 @@ jobs:
|
||||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||||
- name: Add SSH private keys for submodule repositories
|
- name: Add SSH private keys for submodule repositories
|
||||||
uses: webfactory/ssh-agent@v0.9.0
|
uses: webfactory/ssh-agent@v0.9.0
|
||||||
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
with:
|
with:
|
||||||
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
|
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
|
||||||
- name: Clone submodules
|
- name: Clone submodules
|
||||||
if: github.repository == 'element-hq/element-x-android'
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: Use JDK 17
|
- name: Use JDK 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
|
|
@ -237,10 +242,11 @@ jobs:
|
||||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||||
- name: Add SSH private keys for submodule repositories
|
- name: Add SSH private keys for submodule repositories
|
||||||
uses: webfactory/ssh-agent@v0.9.0
|
uses: webfactory/ssh-agent@v0.9.0
|
||||||
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
with:
|
with:
|
||||||
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
|
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
|
||||||
- name: Clone submodules
|
- name: Clone submodules
|
||||||
if: github.repository == 'element-hq/element-x-android'
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: Use JDK 17
|
- name: Use JDK 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
|
|
@ -271,6 +277,7 @@ jobs:
|
||||||
name: Project Check Suite
|
name: Project Check Suite
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [konsist, lint, ktlint, detekt]
|
needs: [konsist, lint, ktlint, detekt]
|
||||||
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
2
.github/workflows/release.yml
vendored
|
|
@ -42,6 +42,7 @@ jobs:
|
||||||
enterprise:
|
enterprise:
|
||||||
name: Create App Bundle Enterprise
|
name: Create App Bundle Enterprise
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ format('build-release-main-gplay-{0}', github.sha) }}
|
group: ${{ format('build-release-main-gplay-{0}', github.sha) }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
@ -49,6 +50,7 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Add SSH private keys for submodule repositories
|
- name: Add SSH private keys for submodule repositories
|
||||||
uses: webfactory/ssh-agent@v0.9.0
|
uses: webfactory/ssh-agent@v0.9.0
|
||||||
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
with:
|
with:
|
||||||
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
|
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
|
||||||
- name: Clone submodules
|
- name: Clone submodules
|
||||||
|
|
|
||||||
2
.github/workflows/sync-localazy.yml
vendored
|
|
@ -9,7 +9,7 @@ jobs:
|
||||||
sync-localazy:
|
sync-localazy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Skip in forks
|
# Skip in forks
|
||||||
if: github.repository == 'element-hq/element-x-android'
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Use JDK 17
|
- name: Use JDK 17
|
||||||
|
|
|
||||||
2
.github/workflows/sync-sas-strings.yml
vendored
|
|
@ -9,7 +9,7 @@ jobs:
|
||||||
sync-sas-strings:
|
sync-sas-strings:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Skip in forks
|
# Skip in forks
|
||||||
if: github.repository == 'element-hq/element-x-android'
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
# No concurrency required, runs every time on a schedule.
|
# No concurrency required, runs every time on a schedule.
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
|
||||||
3
.github/workflows/tests.yml
vendored
|
|
@ -40,10 +40,11 @@ jobs:
|
||||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||||
- name: Add SSH private keys for submodule repositories
|
- name: Add SSH private keys for submodule repositories
|
||||||
uses: webfactory/ssh-agent@v0.9.0
|
uses: webfactory/ssh-agent@v0.9.0
|
||||||
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
with:
|
with:
|
||||||
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
|
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
|
||||||
- name: Clone submodules
|
- name: Clone submodules
|
||||||
if: github.repository == 'element-hq/element-x-android'
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: ☕️ Use JDK 17
|
- name: ☕️ Use JDK 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
|
|
|
||||||
74
CHANGES.md
|
|
@ -1,3 +1,77 @@
|
||||||
|
Changes in Element X v0.4.16 (2024-07-05)
|
||||||
|
=========================================
|
||||||
|
|
||||||
|
### ✨ Features
|
||||||
|
* Avatar cluster for DM by @bmarty in https://github.com/element-hq/element-x-android/pull/3069
|
||||||
|
* Feature : Draft support by @ganfra in https://github.com/element-hq/element-x-android/pull/3099
|
||||||
|
* Timeline : re-enable edition of local echo by @ganfra in https://github.com/element-hq/element-x-android/pull/3126
|
||||||
|
* Draft : add volatile storage when moving to edit mode. by @ganfra in https://github.com/element-hq/element-x-android/pull/3132
|
||||||
|
|
||||||
|
### 🙌 Improvements
|
||||||
|
* Give locale and theme to Element Call by @bmarty in https://github.com/element-hq/element-x-android/pull/3118
|
||||||
|
* Let the SDK retrieve and parse Element well known content by @bmarty in https://github.com/element-hq/element-x-android/pull/3127
|
||||||
|
|
||||||
|
### 🐛 Bugfixes
|
||||||
|
* Let role and permissions screens works for invited room members too. by @bmarty in https://github.com/element-hq/element-x-android/pull/3081
|
||||||
|
* Fix image rendering after clear cache by @bmarty in https://github.com/element-hq/element-x-android/pull/3082
|
||||||
|
* Replace the 'answer' PendingIntent in ringing call notifications by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3093
|
||||||
|
* Use IO dispatcher for cleanup in bug reporter by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3092
|
||||||
|
* Fix `@room` mentions crashing in debug builds by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3107
|
||||||
|
* Auth : fix restore session when there is no network. by @ganfra in https://github.com/element-hq/element-x-android/pull/3109
|
||||||
|
* Alert for incoming call even if notifications are disabled - WAITING FOR FINAL PRODUCT DECISION by @bmarty in https://github.com/element-hq/element-x-android/pull/3053
|
||||||
|
* Fix incorrect 'device verified' screen when app was opened with no network connection by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3110
|
||||||
|
* Draft : also clear draft when composer is blank by @ganfra in https://github.com/element-hq/element-x-android/pull/3115
|
||||||
|
* Timeline : fix text item not refreshed when content change by @ganfra in https://github.com/element-hq/element-x-android/pull/3123
|
||||||
|
* FFs can now be toggled in release builds too by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3101
|
||||||
|
* Fix crash when getting the system ringtone for ringing calls by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3131
|
||||||
|
* Bugfix : avoid potential NPE on verification service. by @ganfra in https://github.com/element-hq/element-x-android/pull/3140
|
||||||
|
|
||||||
|
### 🗣 Translations
|
||||||
|
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3114
|
||||||
|
* Sync Strings - Add Greek translations by @ElementBot in https://github.com/element-hq/element-x-android/pull/3133
|
||||||
|
|
||||||
|
### 🧱 Build
|
||||||
|
* Let GitHub generates the release notes by @bmarty in https://github.com/element-hq/element-x-android/pull/3105
|
||||||
|
* Fix F-Droid reproducible build. by @bmarty in https://github.com/element-hq/element-x-android/pull/3106
|
||||||
|
* Element enterprise (EE) foundations by @bmarty in https://github.com/element-hq/element-x-android/pull/3025
|
||||||
|
* Fix Element Enterprise nightly build and publication using App Distribution by @bmarty in https://github.com/element-hq/element-x-android/pull/3130
|
||||||
|
* Improve screenshot testing with ComposablePreviewScanner by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3125
|
||||||
|
|
||||||
|
### Dependency upgrades
|
||||||
|
* Update dependency com.posthog:posthog-android to v3.4.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3060
|
||||||
|
* Update danger/danger-js action to v12.3.3 by @renovate in https://github.com/element-hq/element-x-android/pull/3059
|
||||||
|
* Update dependency com.freeletics.flowredux:compose to v1.2.2 by @renovate in https://github.com/element-hq/element-x-android/pull/3067
|
||||||
|
* Update dependency com.google.firebase:firebase-bom to v33.1.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3062
|
||||||
|
* Update dependency androidx.test.ext:junit to v1.2.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3088
|
||||||
|
* Update test.core to v1.6.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3090
|
||||||
|
* Remove dependencies androidx.test.espresso:espresso-core and androidx.appcompat:appcompat by @renovate in https://github.com/element-hq/element-x-android/pull/3087
|
||||||
|
* Update wysiwyg to v2.37.4 by @renovate in https://github.com/element-hq/element-x-android/pull/3094
|
||||||
|
* Update dependency androidx.test:runner to v1.6.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3089
|
||||||
|
* Update test.core to v1.6.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3104
|
||||||
|
* Update dependency androidx.test:runner to v1.6.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3103
|
||||||
|
* Update dependency androidx.test.ext:junit to v1.2.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3102
|
||||||
|
* Update dependency com.google.truth:truth to v1.4.3 by @renovate in https://github.com/element-hq/element-x-android/pull/3108
|
||||||
|
* Update dependency com.posthog:posthog-android to v3.4.2 by @renovate in https://github.com/element-hq/element-x-android/pull/3111
|
||||||
|
* Update dependency io.nlopez.compose.rules:detekt to v0.4.5 by @renovate in https://github.com/element-hq/element-x-android/pull/3116
|
||||||
|
* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.29 by @renovate in https://github.com/element-hq/element-x-android/pull/3119
|
||||||
|
* Update plugin dependencycheck to v10 by @renovate in https://github.com/element-hq/element-x-android/pull/3128
|
||||||
|
* Update plugin dependencycheck to v10.0.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3129
|
||||||
|
* Update dependency io.sentry:sentry-android to v7.11.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3122
|
||||||
|
* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.30 by @renovate in https://github.com/element-hq/element-x-android/pull/3138
|
||||||
|
|
||||||
|
### Others
|
||||||
|
* Feature/fga/sending queue iteration by @ganfra in https://github.com/element-hq/element-x-android/pull/3054
|
||||||
|
* Use full date format for day dividers in timeline by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3057
|
||||||
|
* Let Dms use other member color. by @bmarty in https://github.com/element-hq/element-x-android/pull/3058
|
||||||
|
* Resolve display names in mentions in real time by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3051
|
||||||
|
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3077
|
||||||
|
* Improve the way we cut the bubble layout to give space for the sender Avatar by @bmarty in https://github.com/element-hq/element-x-android/pull/3080
|
||||||
|
* Upgrade build tools and fix `pg-map-id` for F-Droid by @bmarty in https://github.com/element-hq/element-x-android/pull/3084
|
||||||
|
* Improve room filtering behavior. by @bmarty in https://github.com/element-hq/element-x-android/pull/3083
|
||||||
|
* Adapt our code to the new authentication APIs in the Rust SDK by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3068
|
||||||
|
* Add temporary icon for Element Enterprise by @bmarty in https://github.com/element-hq/element-x-android/pull/3134
|
||||||
|
* Improve click behavior on room timeline title by @bmarty in https://github.com/element-hq/element-x-android/pull/3064
|
||||||
|
|
||||||
Changes in Element X v0.4.15 (2024-06-19)
|
Changes in Element X v0.4.15 (2024-06-19)
|
||||||
=========================================
|
=========================================
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ plugins {
|
||||||
id(libs.plugins.firebaseAppDistribution.get().pluginId)
|
id(libs.plugins.firebaseAppDistribution.get().pluginId)
|
||||||
alias(libs.plugins.knit)
|
alias(libs.plugins.knit)
|
||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
|
id("com.google.android.gms.oss-licenses-plugin")
|
||||||
// To be able to update the firebase.xml files, uncomment and build the project
|
// To be able to update the firebase.xml files, uncomment and build the project
|
||||||
// id("com.google.gms.google-services")
|
// id("com.google.gms.google-services")
|
||||||
}
|
}
|
||||||
|
|
@ -250,6 +251,7 @@ dependencies {
|
||||||
implementation(projects.anvilannotations)
|
implementation(projects.anvilannotations)
|
||||||
implementation(projects.appnav)
|
implementation(projects.appnav)
|
||||||
implementation(projects.appconfig)
|
implementation(projects.appconfig)
|
||||||
|
implementation(projects.libraries.uiStrings)
|
||||||
anvil(projects.anvilcodegen)
|
anvil(projects.anvilcodegen)
|
||||||
|
|
||||||
// Comment to not include firebase in the project
|
// Comment to not include firebase in the project
|
||||||
|
|
@ -257,6 +259,8 @@ dependencies {
|
||||||
// Comment to not include unified push in the project
|
// Comment to not include unified push in the project
|
||||||
implementation(projects.libraries.pushproviders.unifiedpush)
|
implementation(projects.libraries.pushproviders.unifiedpush)
|
||||||
|
|
||||||
|
"gplayImplementation"(libs.play.services.oss.licenses)
|
||||||
|
|
||||||
implementation(libs.appyx.core)
|
implementation(libs.appyx.core)
|
||||||
implementation(libs.androidx.splash)
|
implementation(libs.androidx.splash)
|
||||||
implementation(libs.androidx.core)
|
implementation(libs.androidx.core)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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
|
||||||
|
*
|
||||||
|
* https://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.x.licenses
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.features.preferences.api.OpenSourceLicensesProvider
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class FdroidOpenSourceLicensesProvider @Inject constructor() : OpenSourceLicensesProvider {
|
||||||
|
override val hasOpenSourceLicenses: Boolean = false
|
||||||
|
|
||||||
|
override fun navigateToOpenSourceLicenses(activity: Activity) {
|
||||||
|
error("Not supported, please ensure that hasOpenSourcesLicenses is true before calling this method")
|
||||||
|
}
|
||||||
|
}
|
||||||
12
app/src/gplay/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<application>
|
||||||
|
<activity
|
||||||
|
android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"
|
||||||
|
android:theme="@style/Theme.OssLicenses" />
|
||||||
|
<activity
|
||||||
|
android:name="com.google.android.gms.oss.licenses.OssLicensesActivity"
|
||||||
|
android:theme="@style/Theme.OssLicenses" />
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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
|
||||||
|
*
|
||||||
|
* https://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.x.licenses
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.features.preferences.api.OpenSourceLicensesProvider
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class OssOpenSourcesLicensesProvider @Inject constructor() : OpenSourceLicensesProvider {
|
||||||
|
override val hasOpenSourceLicenses: Boolean = true
|
||||||
|
|
||||||
|
override fun navigateToOpenSourceLicenses(activity: Activity) {
|
||||||
|
val title = activity.getString(CommonStrings.common_open_source_licenses)
|
||||||
|
OssLicensesMenuActivity.setActivityTitle(title)
|
||||||
|
activity.startActivity(Intent(activity, OssLicensesMenuActivity::class.java))
|
||||||
|
}
|
||||||
|
}
|
||||||
32
app/src/gplay/res/values-night/colors.xml
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
~ Copyright (c) 2024 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
|
||||||
|
~
|
||||||
|
~ https://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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<!-- Use a few colors from compoundColorsDark -->
|
||||||
|
<!-- DarkColorTokens.colorThemeBg -->
|
||||||
|
<color name="colorThemeBg">#FF101317</color>
|
||||||
|
<!-- DarkColorTokens.colorGray1400 -->
|
||||||
|
<color name="textPrimary">#FFEBEEF2</color>
|
||||||
|
<!-- DarkColorTokens.colorGray900 -->
|
||||||
|
<color name="textSecondary">#ff808994</color>
|
||||||
|
<!-- DarkColorTokens.colorBlue900 -->
|
||||||
|
<color name="textLinkExternal">#FF4187EB</color>
|
||||||
|
|
||||||
|
<bool name="windowLightStatusBar">false</bool>
|
||||||
|
<bool name="windowLightNavigationBar">false</bool>
|
||||||
|
|
||||||
|
</resources>
|
||||||
25
app/src/gplay/res/values-v27/themes.xml
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
~ Copyright (c) 2024 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
|
||||||
|
~
|
||||||
|
~ https://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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<style name="Theme.OssLicenses.Light.v27" parent="Base.Theme.OssLicenses">
|
||||||
|
<item name="android:windowLightNavigationBar">@bool/windowLightNavigationBar</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Theme.OssLicenses" parent="Theme.OssLicenses.Light.v27"/>
|
||||||
|
|
||||||
|
</resources>
|
||||||
32
app/src/gplay/res/values/colors.xml
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
~ Copyright (c) 2024 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
|
||||||
|
~
|
||||||
|
~ https://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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<!-- Use a few colors from compoundColorsLight -->
|
||||||
|
<!-- LightColorTokens.colorThemeBg -->
|
||||||
|
<color name="colorThemeBg">#FFFFFFFF</color>
|
||||||
|
<!-- LightColorTokens.colorGray1400 -->
|
||||||
|
<color name="textPrimary">#FF1B1D22</color>
|
||||||
|
<!-- LightColorTokens.colorGray900 -->
|
||||||
|
<color name="textSecondary">#FF656D77</color>
|
||||||
|
<!-- LightColorTokens.colorBlue900 -->
|
||||||
|
<color name="textLinkExternal">#FF0467DD</color>
|
||||||
|
|
||||||
|
<bool name="windowLightStatusBar">true</bool>
|
||||||
|
<bool name="windowLightNavigationBar">true</bool>
|
||||||
|
|
||||||
|
</resources>
|
||||||
23
app/src/gplay/res/values/styles.xml
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
~ Copyright (c) 2024 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
|
||||||
|
~
|
||||||
|
~ https://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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<style name="NoElevationToolbar" parent="Widget.MaterialComponents.Toolbar">
|
||||||
|
<item name="android:elevation">0dp</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</resources>
|
||||||
41
app/src/gplay/res/values/themes.xml
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
~ Copyright (c) 2024 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
|
||||||
|
~
|
||||||
|
~ https://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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<style name="Base.Theme.OssLicenses" parent="Theme.MaterialComponents.DayNight">
|
||||||
|
<!-- Background of title bar -->
|
||||||
|
<item name="colorPrimary">@color/colorThemeBg</item>
|
||||||
|
<!-- Background of the screen -->
|
||||||
|
<item name="android:colorBackground">@color/colorThemeBg</item>
|
||||||
|
<!-- Text of the licenses -->
|
||||||
|
<item name="android:textColor">@color/textSecondary</item>
|
||||||
|
<!-- Link text color -->
|
||||||
|
<item name="android:textColorLink">@color/textLinkExternal</item>
|
||||||
|
<!-- Title, back button and license item text color -->
|
||||||
|
<item name="android:textColorPrimary">@color/textPrimary</item>
|
||||||
|
<!-- Background of status bar -->
|
||||||
|
<item name="android:statusBarColor">@color/colorThemeBg</item>
|
||||||
|
<item name="android:windowLightStatusBar">@bool/windowLightStatusBar</item>
|
||||||
|
<!-- Background of navigation bar -->
|
||||||
|
<item name="android:navigationBarColor">@color/colorThemeBg</item>
|
||||||
|
<!-- Try to remove Toolbar elevation, but it does not work :/ -->
|
||||||
|
<item name="toolbarStyle">@style/NoElevationToolbar</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Theme.OssLicenses" parent="Base.Theme.OssLicenses" />
|
||||||
|
|
||||||
|
</resources>
|
||||||
|
|
@ -26,9 +26,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
|
|
@ -38,16 +35,13 @@ import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.bumble.appyx.core.integration.NodeHost
|
import com.bumble.appyx.core.integration.NodeHost
|
||||||
import com.bumble.appyx.core.integrationpoint.NodeActivity
|
import com.bumble.appyx.core.integrationpoint.NodeActivity
|
||||||
import com.bumble.appyx.core.plugin.NodeReadyObserver
|
import com.bumble.appyx.core.plugin.NodeReadyObserver
|
||||||
import io.element.android.compound.theme.ElementTheme
|
|
||||||
import io.element.android.compound.theme.Theme
|
|
||||||
import io.element.android.compound.theme.isDark
|
|
||||||
import io.element.android.compound.theme.mapToTheme
|
|
||||||
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
||||||
import io.element.android.features.lockscreen.api.LockScreenLockState
|
import io.element.android.features.lockscreen.api.LockScreenLockState
|
||||||
import io.element.android.features.lockscreen.api.LockScreenService
|
import io.element.android.features.lockscreen.api.LockScreenService
|
||||||
import io.element.android.features.lockscreen.api.handleSecureFlag
|
import io.element.android.features.lockscreen.api.handleSecureFlag
|
||||||
import io.element.android.libraries.architecture.bindings
|
import io.element.android.libraries.architecture.bindings
|
||||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
|
import io.element.android.libraries.designsystem.theme.ElementThemeApp
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher
|
||||||
import io.element.android.x.di.AppBindings
|
import io.element.android.x.di.AppBindings
|
||||||
import io.element.android.x.intent.SafeUriHandler
|
import io.element.android.x.intent.SafeUriHandler
|
||||||
|
|
@ -74,14 +68,8 @@ class MainActivity : NodeActivity() {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MainContent(appBindings: AppBindings) {
|
private fun MainContent(appBindings: AppBindings) {
|
||||||
val theme by remember {
|
|
||||||
appBindings.preferencesStore().getThemeFlow().mapToTheme()
|
|
||||||
}
|
|
||||||
.collectAsState(initial = Theme.System)
|
|
||||||
val migrationState = appBindings.migrationEntryPoint().present()
|
val migrationState = appBindings.migrationEntryPoint().present()
|
||||||
ElementTheme(
|
ElementThemeApp(appBindings.preferencesStore()) {
|
||||||
darkTheme = theme.isDark()
|
|
||||||
) {
|
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalSnackbarDispatcher provides appBindings.snackbarDispatcher(),
|
LocalSnackbarDispatcher provides appBindings.snackbarDispatcher(),
|
||||||
LocalUriHandler provides SafeUriHandler(this),
|
LocalUriHandler provides SafeUriHandler(this),
|
||||||
|
|
|
||||||
|
|
@ -71,8 +71,7 @@ class TracingInitializer : Initializer<Unit> {
|
||||||
return WriteToFilesConfiguration.Enabled(
|
return WriteToFilesConfiguration.Enabled(
|
||||||
directory = bugReporter.logDirectory().absolutePath,
|
directory = bugReporter.logDirectory().absolutePath,
|
||||||
filenamePrefix = "logs",
|
filenamePrefix = "logs",
|
||||||
filenameSuffix = null,
|
// Keep a maximum of 1 week of log files.
|
||||||
// Keep a minimum of 1 week of log files.
|
|
||||||
numberOfFiles = 7 * 24,
|
numberOfFiles = 7 * 24,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@
|
||||||
<locale android:name="in"/>
|
<locale android:name="in"/>
|
||||||
<locale android:name="it"/>
|
<locale android:name="it"/>
|
||||||
<locale android:name="ka"/>
|
<locale android:name="ka"/>
|
||||||
|
<locale android:name="pl"/>
|
||||||
<locale android:name="pt"/>
|
<locale android:name="pt"/>
|
||||||
|
<locale android:name="pt_BR"/>
|
||||||
<locale android:name="ro"/>
|
<locale android:name="ro"/>
|
||||||
<locale android:name="ru"/>
|
<locale android:name="ru"/>
|
||||||
<locale android:name="sk"/>
|
<locale android:name="sk"/>
|
||||||
|
|
|
||||||
|
|
@ -15,20 +15,13 @@
|
||||||
*/
|
*/
|
||||||
plugins {
|
plugins {
|
||||||
id("io.element.android-library")
|
id("io.element.android-library")
|
||||||
alias(libs.plugins.anvil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.appconfig"
|
namespace = "io.element.android.appconfig"
|
||||||
}
|
}
|
||||||
|
|
||||||
anvil {
|
|
||||||
generateDaggerFactories.set(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.androidx.annotationjvm)
|
implementation(libs.androidx.annotationjvm)
|
||||||
implementation(libs.dagger)
|
|
||||||
implementation(projects.libraries.di)
|
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,44 +16,28 @@
|
||||||
|
|
||||||
package io.element.android.appconfig
|
package io.element.android.appconfig
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesTo
|
|
||||||
import dagger.Module
|
|
||||||
import dagger.Provides
|
|
||||||
import io.element.android.libraries.di.AppScope
|
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
/**
|
object LockScreenConfig {
|
||||||
* Configuration for the lock screen feature.
|
/** Whether the PIN is mandatory or not. */
|
||||||
* @property isPinMandatory Whether the PIN is mandatory or not.
|
const val IS_PIN_MANDATORY: Boolean = false
|
||||||
* @property pinBlacklist Some PINs are forbidden.
|
|
||||||
* @property pinSize The size of the PIN.
|
|
||||||
* @property maxPinCodeAttemptsBeforeLogout Number of attempts before the user is logged out.
|
|
||||||
* @property gracePeriod Time period before locking the app once backgrounded.
|
|
||||||
* @property isStrongBiometricsEnabled Authentication with strong methods (fingerprint, some face/iris unlock implementations) is supported.
|
|
||||||
* @property isWeakBiometricsEnabled Authentication with weak methods (most face/iris unlock implementations) is supported.
|
|
||||||
*/
|
|
||||||
data class LockScreenConfig(
|
|
||||||
val isPinMandatory: Boolean,
|
|
||||||
val pinBlacklist: Set<String>,
|
|
||||||
val pinSize: Int,
|
|
||||||
val maxPinCodeAttemptsBeforeLogout: Int,
|
|
||||||
val gracePeriod: Duration,
|
|
||||||
val isStrongBiometricsEnabled: Boolean,
|
|
||||||
val isWeakBiometricsEnabled: Boolean,
|
|
||||||
)
|
|
||||||
|
|
||||||
@ContributesTo(AppScope::class)
|
/** Set of forbidden PIN codes. */
|
||||||
@Module
|
val FORBIDDEN_PIN_CODES: Set<String> = setOf("0000", "1234")
|
||||||
object LockScreenConfigModule {
|
|
||||||
@Provides
|
/** The size of the PIN. */
|
||||||
fun providesLockScreenConfig(): LockScreenConfig = LockScreenConfig(
|
const val PIN_SIZE: Int = 4
|
||||||
isPinMandatory = false,
|
|
||||||
pinBlacklist = setOf("0000", "1234"),
|
/** Number of attempts before the user is logged out. */
|
||||||
pinSize = 4,
|
const val MAX_PIN_CODE_ATTEMPTS_BEFORE_LOGOUT: Int = 3
|
||||||
maxPinCodeAttemptsBeforeLogout = 3,
|
|
||||||
gracePeriod = 0.seconds,
|
/** Time period before locking the app once backgrounded. */
|
||||||
isStrongBiometricsEnabled = true,
|
val GRACE_PERIOD: Duration = 2.minutes
|
||||||
isWeakBiometricsEnabled = true,
|
|
||||||
)
|
/** Authentication with strong methods (fingerprint, some face/iris unlock implementations) is supported. */
|
||||||
|
const val IS_STRONG_BIOMETRICS_ENABLED: Boolean = true
|
||||||
|
|
||||||
|
/** Authentication with weak methods (most face/iris unlock implementations) is supported. */
|
||||||
|
const val IS_WEAK_BIOMETRICS_ENABLED: Boolean = true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 4 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -42,6 +42,7 @@ dependencies {
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.deeplink)
|
implementation(projects.libraries.deeplink)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
|
implementation(projects.libraries.preferences.api)
|
||||||
implementation(projects.libraries.push.api)
|
implementation(projects.libraries.push.api)
|
||||||
implementation(projects.libraries.pushproviders.api)
|
implementation(projects.libraries.pushproviders.api)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
|
|
|
||||||
|
|
@ -104,11 +104,6 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
|
||||||
inputs.room.subscribeToSync()
|
inputs.room.subscribeToSync()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onPause = {
|
|
||||||
appCoroutineScope.launch {
|
|
||||||
inputs.room.unsubscribeFromSync()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDestroy = {
|
onDestroy = {
|
||||||
Timber.v("OnDestroy")
|
Timber.v("OnDestroy")
|
||||||
appNavigationStateService.onLeavingRoom(id)
|
appNavigationStateService.onLeavingRoom(id)
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import io.element.android.features.login.api.LoginUserStory
|
||||||
import io.element.android.features.preferences.api.CacheService
|
import io.element.android.features.preferences.api.CacheService
|
||||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||||
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
|
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
|
||||||
|
import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory
|
||||||
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
|
@ -42,6 +43,7 @@ class RootNavStateFlowFactory @Inject constructor(
|
||||||
private val matrixClientsHolder: MatrixClientsHolder,
|
private val matrixClientsHolder: MatrixClientsHolder,
|
||||||
private val imageLoaderHolder: ImageLoaderHolder,
|
private val imageLoaderHolder: ImageLoaderHolder,
|
||||||
private val loginUserStory: LoginUserStory,
|
private val loginUserStory: LoginUserStory,
|
||||||
|
private val sessionPreferencesStoreFactory: SessionPreferencesStoreFactory,
|
||||||
) {
|
) {
|
||||||
private var currentCacheIndex = 0
|
private var currentCacheIndex = 0
|
||||||
|
|
||||||
|
|
@ -73,6 +75,8 @@ class RootNavStateFlowFactory @Inject constructor(
|
||||||
matrixClientsHolder.remove(sessionId)
|
matrixClientsHolder.remove(sessionId)
|
||||||
// Ensure image loader will be recreated with the new MatrixClient
|
// Ensure image loader will be recreated with the new MatrixClient
|
||||||
imageLoaderHolder.remove(sessionId)
|
imageLoaderHolder.remove(sessionId)
|
||||||
|
// Also remove cached value for SessionPreferencesStore
|
||||||
|
sessionPreferencesStoreFactory.remove(sessionId)
|
||||||
}
|
}
|
||||||
.toIndexFlow(initialCacheIndex)
|
.toIndexFlow(initialCacheIndex)
|
||||||
.onEach { cacheIndex ->
|
.onEach { cacheIndex ->
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,9 @@ class JoinRoomLoadedFlowNodeTest {
|
||||||
@Test
|
@Test
|
||||||
fun `given a room flow node when initialized then it loads messages entry point`() = runTest {
|
fun `given a room flow node when initialized then it loads messages entry point`() = runTest {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
val room = FakeMatrixRoom()
|
val room = FakeMatrixRoom(
|
||||||
|
updateMembersResult = { }
|
||||||
|
)
|
||||||
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||||
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())
|
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())
|
||||||
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
val roomFlowNode = createJoinedRoomLoadedFlowNode(
|
||||||
|
|
@ -144,7 +146,9 @@ class JoinRoomLoadedFlowNodeTest {
|
||||||
@Test
|
@Test
|
||||||
fun `given a room flow node when callback on room details is triggered then it loads room details entry point`() = runTest {
|
fun `given a room flow node when callback on room details is triggered then it loads room details entry point`() = runTest {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
val room = FakeMatrixRoom()
|
val room = FakeMatrixRoom(
|
||||||
|
updateMembersResult = { }
|
||||||
|
)
|
||||||
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||||
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
|
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
|
||||||
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())
|
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ import org.junit.Test
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class) class SendQueuesTest {
|
@OptIn(ExperimentalCoroutinesApi::class) class SendQueuesTest {
|
||||||
private val matrixClient = FakeMatrixClient()
|
private val matrixClient = FakeMatrixClient()
|
||||||
private val room = FakeMatrixRoom()
|
|
||||||
private val networkMonitor = FakeNetworkMonitor()
|
private val networkMonitor = FakeNetworkMonitor()
|
||||||
private val sut = SendQueues(matrixClient, networkMonitor)
|
private val sut = SendQueues(matrixClient, networkMonitor)
|
||||||
|
|
||||||
|
|
@ -43,11 +42,11 @@ import org.junit.Test
|
||||||
val setAllSendQueuesEnabledLambda = lambdaRecorder { _: Boolean -> }
|
val setAllSendQueuesEnabledLambda = lambdaRecorder { _: Boolean -> }
|
||||||
matrixClient.sendQueueDisabledFlow = sendQueueDisabledFlow
|
matrixClient.sendQueueDisabledFlow = sendQueueDisabledFlow
|
||||||
matrixClient.setAllSendQueuesEnabledLambda = setAllSendQueuesEnabledLambda
|
matrixClient.setAllSendQueuesEnabledLambda = setAllSendQueuesEnabledLambda
|
||||||
matrixClient.givenGetRoomResult(room.roomId, room)
|
|
||||||
|
|
||||||
val setRoomSendQueueEnabledLambda = lambdaRecorder { _: Boolean -> }
|
val setRoomSendQueueEnabledLambda = lambdaRecorder { _: Boolean -> }
|
||||||
room.setSendQueueEnabledLambda = setRoomSendQueueEnabledLambda
|
val room = FakeMatrixRoom(
|
||||||
|
setSendQueueEnabledLambda = setRoomSendQueueEnabledLambda
|
||||||
|
)
|
||||||
|
matrixClient.givenGetRoomResult(room.roomId, room)
|
||||||
sut.launchIn(backgroundScope)
|
sut.launchIn(backgroundScope)
|
||||||
|
|
||||||
sendQueueDisabledFlow.emit(room.roomId)
|
sendQueueDisabledFlow.emit(room.roomId)
|
||||||
|
|
@ -72,10 +71,11 @@ import org.junit.Test
|
||||||
matrixClient.sendQueueDisabledFlow = sendQueueDisabledFlow
|
matrixClient.sendQueueDisabledFlow = sendQueueDisabledFlow
|
||||||
matrixClient.setAllSendQueuesEnabledLambda = setAllSendQueuesEnabledLambda
|
matrixClient.setAllSendQueuesEnabledLambda = setAllSendQueuesEnabledLambda
|
||||||
networkMonitor.connectivity.value = NetworkStatus.Offline
|
networkMonitor.connectivity.value = NetworkStatus.Offline
|
||||||
matrixClient.givenGetRoomResult(room.roomId, room)
|
|
||||||
|
|
||||||
val setRoomSendQueueEnabledLambda = lambdaRecorder { _: Boolean -> }
|
val setRoomSendQueueEnabledLambda = lambdaRecorder { _: Boolean -> }
|
||||||
room.setSendQueueEnabledLambda = setRoomSendQueueEnabledLambda
|
val room = FakeMatrixRoom(
|
||||||
|
setSendQueueEnabledLambda = setRoomSendQueueEnabledLambda
|
||||||
|
)
|
||||||
|
matrixClient.givenGetRoomResult(room.roomId, room)
|
||||||
|
|
||||||
sut.launchIn(backgroundScope)
|
sut.launchIn(backgroundScope)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ buildscript {
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath(libs.kotlin.gradle.plugin)
|
classpath(libs.kotlin.gradle.plugin)
|
||||||
classpath(libs.gms.google.services)
|
classpath(libs.gms.google.services)
|
||||||
|
classpath(libs.oss.licenses.plugin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
1
changelog.d/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
||||||
!.gitignore
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Store and restore drafts for each room.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Use a more natural date format for day dividers in the timeline. Also improve the time format for last messages in the room list.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Resolve display names in mentions in real time, also send mentions with user ids as the fallback text for the link representation of the mentions.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Alert for incoming call even if notifications are disabled
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Updated Rust SDK to `v0.2.28`. Fixed incompatibilities.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Fix feature flags not being able to be toggle in developer settings in release builds.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Let roles and permissions screens work for invited room members too.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Fix image rendering after clear cache
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Improve room filters behavior
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Make sure we replace the 'answer call' pending intent on ringing call notifications.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Make sure we don't use the main dispatcher while closing the bug report request, as it can lead to crashes in strict mode.
|
|
||||||
2
fastlane/metadata/android/en-US/changelogs/40005000.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
Main changes in this version: mostly bug fixes and performance improvements.
|
||||||
|
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_analytics_settings_help_us_improve">"Udostępniaj anonimowe dane dotyczące użytkowania, aby pomóc nam identyfikować problemy."</string>
|
||||||
|
<string name="screen_analytics_settings_read_terms">"Możesz przeczytać wszystkie nasze warunki %1$s."</string>
|
||||||
|
<string name="screen_analytics_settings_read_terms_content_link">"tutaj"</string>
|
||||||
|
<string name="screen_analytics_settings_share_data">"Udostępniaj dane analityczne"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_analytics_settings_help_us_improve">"Compartilhe dados de uso anônimos para nos ajudar a identificar problemas."</string>
|
||||||
|
<string name="screen_analytics_settings_read_terms">"Você pode ler todos os nossos termos %1$s ."</string>
|
||||||
|
<string name="screen_analytics_settings_read_terms_content_link">"aqui"</string>
|
||||||
|
<string name="screen_analytics_settings_share_data">"Compartilhar dados de utilização"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_analytics_prompt_data_usage">"Nie będziemy rejestrować ani profilować żadnych danych osobistych"</string>
|
||||||
|
<string name="screen_analytics_prompt_help_us_improve">"Udostępniaj anonimowe dane dotyczące użytkowania, aby pomóc nam identyfikować problemy."</string>
|
||||||
|
<string name="screen_analytics_prompt_read_terms">"Możesz przeczytać wszystkie nasze warunki %1$s."</string>
|
||||||
|
<string name="screen_analytics_prompt_read_terms_content_link">"tutaj"</string>
|
||||||
|
<string name="screen_analytics_prompt_settings">"Możesz to wyłączyć w dowolnym momencie"</string>
|
||||||
|
<string name="screen_analytics_prompt_third_party_sharing">"Nie będziemy udostępniać Twoich danych podmiotom trzecim"</string>
|
||||||
|
<string name="screen_analytics_prompt_title">"Pomóż nam ulepszyć %1$s"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_analytics_prompt_data_usage">"Não registraremos nem criaremos perfil baseado em qualquer dado pessoal"</string>
|
||||||
|
<string name="screen_analytics_prompt_help_us_improve">"Compartilhe dados de uso anônimos para nos ajudar a identificar problemas."</string>
|
||||||
|
<string name="screen_analytics_prompt_read_terms">"Você pode ler todos os nossos termos %1$s ."</string>
|
||||||
|
<string name="screen_analytics_prompt_read_terms_content_link">"aqui"</string>
|
||||||
|
<string name="screen_analytics_prompt_settings">"Você pode desativar isso a qualquer momento"</string>
|
||||||
|
<string name="screen_analytics_prompt_third_party_sharing">"Não compartilharemos seus dados com terceiros"</string>
|
||||||
|
<string name="screen_analytics_prompt_title">"Ajude a melhorar o %1$s"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -43,6 +43,7 @@ dependencies {
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
|
implementation(projects.libraries.featureflag.api)
|
||||||
implementation(projects.libraries.matrix.impl)
|
implementation(projects.libraries.matrix.impl)
|
||||||
implementation(projects.libraries.matrixui)
|
implementation(projects.libraries.matrixui)
|
||||||
implementation(projects.libraries.network)
|
implementation(projects.libraries.network)
|
||||||
|
|
@ -70,4 +71,6 @@ dependencies {
|
||||||
testImplementation(projects.libraries.push.test)
|
testImplementation(projects.libraries.push.test)
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
|
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||||
|
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,11 @@
|
||||||
<application>
|
<application>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.ElementCallActivity"
|
android:name=".ui.ElementCallActivity"
|
||||||
android:configChanges="screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode"
|
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/element_call"
|
android:label="@string/element_call"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
|
android:supportsPictureInPicture="true"
|
||||||
android:taskAffinity="io.element.android.features.call">
|
android:taskAffinity="io.element.android.features.call">
|
||||||
|
|
||||||
<intent-filter android:autoVerify="true">
|
<intent-filter android:autoVerify="true">
|
||||||
|
|
@ -77,10 +78,11 @@
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity android:name=".ui.IncomingCallActivity"
|
<activity
|
||||||
|
android:name=".ui.IncomingCallActivity"
|
||||||
android:configChanges="screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode"
|
android:configChanges="screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode"
|
||||||
android:exported="false"
|
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
|
android:exported="false"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:taskAffinity="io.element.android.features.call" />
|
android:taskAffinity="io.element.android.features.call" />
|
||||||
|
|
||||||
|
|
@ -90,9 +92,10 @@
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="phoneCall" />
|
android:foregroundServiceType="phoneCall" />
|
||||||
|
|
||||||
<receiver android:name=".receivers.DeclineCallBroadcastReceiver"
|
<receiver
|
||||||
android:exported="false"
|
android:name=".receivers.DeclineCallBroadcastReceiver"
|
||||||
android:enabled="true" />
|
android:enabled="true"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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
|
||||||
|
*
|
||||||
|
* https://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.call.impl.pip
|
||||||
|
|
||||||
|
sealed interface PictureInPictureEvents {
|
||||||
|
data object EnterPictureInPicture : PictureInPictureEvents
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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
|
||||||
|
*
|
||||||
|
* https://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.call.impl.pip
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.PictureInPictureParams
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Rational
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private val loggerTag = LoggerTag("PiP")
|
||||||
|
|
||||||
|
class PictureInPicturePresenter @Inject constructor(
|
||||||
|
pipSupportProvider: PipSupportProvider,
|
||||||
|
) : Presenter<PictureInPictureState> {
|
||||||
|
private val isPipSupported = pipSupportProvider.isPipSupported()
|
||||||
|
private var isInPictureInPicture = mutableStateOf(false)
|
||||||
|
private var hostActivity: WeakReference<Activity>? = null
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun present(): PictureInPictureState {
|
||||||
|
fun handleEvent(event: PictureInPictureEvents) {
|
||||||
|
when (event) {
|
||||||
|
PictureInPictureEvents.EnterPictureInPicture -> switchToPip()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PictureInPictureState(
|
||||||
|
supportPip = isPipSupported,
|
||||||
|
isInPictureInPicture = isInPictureInPicture.value,
|
||||||
|
eventSink = ::handleEvent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onCreate(activity: Activity) {
|
||||||
|
if (isPipSupported) {
|
||||||
|
Timber.tag(loggerTag.value).d("onCreate: Setting PiP params")
|
||||||
|
hostActivity = WeakReference(activity)
|
||||||
|
hostActivity?.get()?.setPictureInPictureParams(getPictureInPictureParams())
|
||||||
|
} else {
|
||||||
|
Timber.tag(loggerTag.value).d("onCreate: PiP is not supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDestroy() {
|
||||||
|
Timber.tag(loggerTag.value).d("onDestroy")
|
||||||
|
hostActivity?.clear()
|
||||||
|
hostActivity = null
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
private fun getPictureInPictureParams(): PictureInPictureParams {
|
||||||
|
return PictureInPictureParams.Builder()
|
||||||
|
// Portrait for calls seems more appropriate
|
||||||
|
.setAspectRatio(Rational(3, 5))
|
||||||
|
.apply {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
setAutoEnterEnabled(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enters Picture-in-Picture mode.
|
||||||
|
*/
|
||||||
|
private fun switchToPip() {
|
||||||
|
if (isPipSupported) {
|
||||||
|
Timber.tag(loggerTag.value).d("Switch to PiP mode")
|
||||||
|
hostActivity?.get()?.enterPictureInPictureMode(getPictureInPictureParams())
|
||||||
|
?.also { Timber.tag(loggerTag.value).d("Switch to PiP mode result: $it") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
|
||||||
|
Timber.tag(loggerTag.value).d("onPictureInPictureModeChanged: $isInPictureInPictureMode")
|
||||||
|
isInPictureInPicture.value = isInPictureInPictureMode
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onUserLeaveHint() {
|
||||||
|
Timber.tag(loggerTag.value).d("onUserLeaveHint")
|
||||||
|
switchToPip()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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
|
||||||
|
*
|
||||||
|
* https://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.call.impl.pip
|
||||||
|
|
||||||
|
data class PictureInPictureState(
|
||||||
|
val supportPip: Boolean,
|
||||||
|
val isInPictureInPicture: Boolean,
|
||||||
|
val eventSink: (PictureInPictureEvents) -> Unit,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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
|
||||||
|
*
|
||||||
|
* https://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.call.impl.pip
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
|
||||||
|
open class PictureInPictureStateProvider : PreviewParameterProvider<PictureInPictureState> {
|
||||||
|
override val values: Sequence<PictureInPictureState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aPictureInPictureState(supportPip = true),
|
||||||
|
aPictureInPictureState(supportPip = true, isInPictureInPicture = true),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aPictureInPictureState(
|
||||||
|
supportPip: Boolean = false,
|
||||||
|
isInPictureInPicture: Boolean = false,
|
||||||
|
eventSink: (PictureInPictureEvents) -> Unit = {},
|
||||||
|
): PictureInPictureState {
|
||||||
|
return PictureInPictureState(
|
||||||
|
supportPip = supportPip,
|
||||||
|
isInPictureInPicture = isInPictureInPicture,
|
||||||
|
eventSink = eventSink,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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
|
||||||
|
*
|
||||||
|
* https://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.call.impl.pip
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.ChecksSdkIntAtLeast
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
interface PipSupportProvider {
|
||||||
|
@ChecksSdkIntAtLeast(Build.VERSION_CODES.O)
|
||||||
|
fun isPipSupported(): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultPipSupportProvider @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
private val featureFlagService: FeatureFlagService,
|
||||||
|
) : PipSupportProvider {
|
||||||
|
override fun isPipSupported(): Boolean {
|
||||||
|
val isSupportedByTheOs = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
|
||||||
|
context.packageManager?.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE).orFalse()
|
||||||
|
return if (isSupportedByTheOs) {
|
||||||
|
runBlocking { featureFlagService.isFeatureEnabled(FeatureFlags.PictureInPicture) }
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,11 +23,12 @@ open class CallScreenStateProvider : PreviewParameterProvider<CallScreenState> {
|
||||||
override val values: Sequence<CallScreenState>
|
override val values: Sequence<CallScreenState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aCallScreenState(),
|
aCallScreenState(),
|
||||||
|
aCallScreenState(urlState = AsyncData.Loading()),
|
||||||
aCallScreenState(urlState = AsyncData.Failure(Exception("An error occurred"))),
|
aCallScreenState(urlState = AsyncData.Failure(Exception("An error occurred"))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun aCallScreenState(
|
internal fun aCallScreenState(
|
||||||
urlState: AsyncData<String> = AsyncData.Success("https://call.element.io/some-actual-call?with=parameters"),
|
urlState: AsyncData<String> = AsyncData.Success("https://call.element.io/some-actual-call?with=parameters"),
|
||||||
userAgent: String = "",
|
userAgent: String = "",
|
||||||
isInWidgetMode: Boolean = false,
|
isInWidgetMode: Boolean = false,
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,10 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.call.impl.R
|
import io.element.android.features.call.impl.R
|
||||||
|
import io.element.android.features.call.impl.pip.PictureInPictureEvents
|
||||||
|
import io.element.android.features.call.impl.pip.PictureInPictureState
|
||||||
|
import io.element.android.features.call.impl.pip.PictureInPictureStateProvider
|
||||||
|
import io.element.android.features.call.impl.pip.aPictureInPictureState
|
||||||
import io.element.android.features.call.impl.utils.WebViewWidgetMessageInterceptor
|
import io.element.android.features.call.impl.utils.WebViewWidgetMessageInterceptor
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||||
|
|
@ -58,25 +62,36 @@ interface CallScreenNavigator {
|
||||||
@Composable
|
@Composable
|
||||||
internal fun CallScreenView(
|
internal fun CallScreenView(
|
||||||
state: CallScreenState,
|
state: CallScreenState,
|
||||||
|
pipState: PictureInPictureState,
|
||||||
requestPermissions: (Array<String>, RequestPermissionCallback) -> Unit,
|
requestPermissions: (Array<String>, RequestPermissionCallback) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
fun handleBack() {
|
||||||
|
if (pipState.supportPip) {
|
||||||
|
pipState.eventSink.invoke(PictureInPictureEvents.EnterPictureInPicture)
|
||||||
|
} else {
|
||||||
|
state.eventSink(CallScreenEvents.Hangup)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
if (!pipState.isInPictureInPicture) {
|
||||||
title = { Text(stringResource(R.string.element_call)) },
|
TopAppBar(
|
||||||
navigationIcon = {
|
title = { Text(stringResource(R.string.element_call)) },
|
||||||
BackButton(
|
navigationIcon = {
|
||||||
imageVector = CompoundIcons.Close(),
|
BackButton(
|
||||||
onClick = { state.eventSink(CallScreenEvents.Hangup) }
|
imageVector = if (pipState.supportPip) CompoundIcons.ArrowLeft() else CompoundIcons.Close(),
|
||||||
)
|
onClick = ::handleBack,
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
BackHandler {
|
BackHandler {
|
||||||
state.eventSink(CallScreenEvents.Hangup)
|
handleBack()
|
||||||
}
|
}
|
||||||
CallWebView(
|
CallWebView(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -177,6 +192,19 @@ internal fun CallScreenViewPreview(
|
||||||
) = ElementPreview {
|
) = ElementPreview {
|
||||||
CallScreenView(
|
CallScreenView(
|
||||||
state = state,
|
state = state,
|
||||||
|
pipState = aPictureInPictureState(),
|
||||||
|
requestPermissions = { _, _ -> },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun CallScreenPipViewPreview(
|
||||||
|
@PreviewParameter(PictureInPictureStateProvider::class) state: PictureInPictureState,
|
||||||
|
) = ElementPreview {
|
||||||
|
CallScreenView(
|
||||||
|
state = aCallScreenState(),
|
||||||
|
pipState = state,
|
||||||
requestPermissions = { _, _ -> },
|
requestPermissions = { _, _ -> },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,28 +30,26 @@ import androidx.activity.compose.setContent
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.core.content.IntentCompat
|
import androidx.core.content.IntentCompat
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import androidx.lifecycle.Lifecycle
|
||||||
import io.element.android.compound.theme.Theme
|
|
||||||
import io.element.android.compound.theme.isDark
|
|
||||||
import io.element.android.compound.theme.mapToTheme
|
|
||||||
import io.element.android.features.call.api.CallType
|
import io.element.android.features.call.api.CallType
|
||||||
import io.element.android.features.call.impl.DefaultElementCallEntryPoint
|
import io.element.android.features.call.impl.DefaultElementCallEntryPoint
|
||||||
import io.element.android.features.call.impl.di.CallBindings
|
import io.element.android.features.call.impl.di.CallBindings
|
||||||
|
import io.element.android.features.call.impl.pip.PictureInPicturePresenter
|
||||||
import io.element.android.features.call.impl.services.CallForegroundService
|
import io.element.android.features.call.impl.services.CallForegroundService
|
||||||
import io.element.android.features.call.impl.utils.CallIntentDataParser
|
import io.element.android.features.call.impl.utils.CallIntentDataParser
|
||||||
import io.element.android.libraries.architecture.bindings
|
import io.element.android.libraries.architecture.bindings
|
||||||
|
import io.element.android.libraries.designsystem.theme.ElementThemeApp
|
||||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
|
class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
|
||||||
@Inject lateinit var callIntentDataParser: CallIntentDataParser
|
@Inject lateinit var callIntentDataParser: CallIntentDataParser
|
||||||
@Inject lateinit var presenterFactory: CallScreenPresenter.Factory
|
@Inject lateinit var presenterFactory: CallScreenPresenter.Factory
|
||||||
@Inject lateinit var appPreferencesStore: AppPreferencesStore
|
@Inject lateinit var appPreferencesStore: AppPreferencesStore
|
||||||
|
@Inject lateinit var pictureInPicturePresenter: PictureInPicturePresenter
|
||||||
|
|
||||||
private lateinit var presenter: CallScreenPresenter
|
private lateinit var presenter: CallScreenPresenter
|
||||||
|
|
||||||
|
|
@ -67,6 +65,8 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
|
||||||
private var isDarkMode = false
|
private var isDarkMode = false
|
||||||
private val webViewTarget = mutableStateOf<CallType?>(null)
|
private val webViewTarget = mutableStateOf<CallType?>(null)
|
||||||
|
|
||||||
|
private var eventSink: ((CallScreenEvents) -> Unit)? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
|
@ -86,20 +86,19 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
|
||||||
updateUiMode(resources.configuration)
|
updateUiMode(resources.configuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pictureInPicturePresenter.onCreate(this)
|
||||||
|
|
||||||
audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
|
audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
|
||||||
requestAudioFocus()
|
requestAudioFocus()
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
val theme by remember {
|
val pipState = pictureInPicturePresenter.present()
|
||||||
appPreferencesStore.getThemeFlow().mapToTheme()
|
ElementThemeApp(appPreferencesStore) {
|
||||||
}
|
val state = presenter.present()
|
||||||
.collectAsState(initial = Theme.System)
|
eventSink = state.eventSink
|
||||||
val state = presenter.present()
|
|
||||||
ElementTheme(
|
|
||||||
darkTheme = theme.isDark()
|
|
||||||
) {
|
|
||||||
CallScreenView(
|
CallScreenView(
|
||||||
state = state,
|
state = state,
|
||||||
|
pipState = pipState,
|
||||||
requestPermissions = { permissions, callback ->
|
requestPermissions = { permissions, callback ->
|
||||||
requestPermissionCallback = callback
|
requestPermissionCallback = callback
|
||||||
requestPermissionsLauncher.launch(permissions)
|
requestPermissionsLauncher.launch(permissions)
|
||||||
|
|
@ -114,6 +113,16 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
|
||||||
updateUiMode(newConfig)
|
updateUiMode(newConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
|
||||||
|
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
||||||
|
pictureInPicturePresenter.onPictureInPictureModeChanged(isInPictureInPictureMode)
|
||||||
|
|
||||||
|
if (!isInPictureInPictureMode && !lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
|
||||||
|
Timber.d("Exiting PiP mode: Hangup the call")
|
||||||
|
eventSink?.invoke(CallScreenEvents.Hangup)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
setCallType(intent)
|
setCallType(intent)
|
||||||
|
|
@ -131,10 +140,16 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onUserLeaveHint() {
|
||||||
|
super.onUserLeaveHint()
|
||||||
|
pictureInPicturePresenter.onUserLeaveHint()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
releaseAudioFocus()
|
releaseAudioFocus()
|
||||||
CallForegroundService.stop(this)
|
CallForegroundService.stop(this)
|
||||||
|
pictureInPicturePresenter.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun finish() {
|
override fun finish() {
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ import io.element.android.features.call.impl.notifications.CallNotificationData
|
||||||
import io.element.android.features.call.impl.utils.ActiveCallManager
|
import io.element.android.features.call.impl.utils.ActiveCallManager
|
||||||
import io.element.android.features.call.impl.utils.CallState
|
import io.element.android.features.call.impl.utils.CallState
|
||||||
import io.element.android.libraries.architecture.bindings
|
import io.element.android.libraries.architecture.bindings
|
||||||
|
import io.element.android.libraries.designsystem.theme.ElementThemeApp
|
||||||
|
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
@ -51,6 +53,9 @@ class IncomingCallActivity : AppCompatActivity() {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var activeCallManager: ActiveCallManager
|
lateinit var activeCallManager: ActiveCallManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var appPreferencesStore: AppPreferencesStore
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
|
@ -68,11 +73,13 @@ class IncomingCallActivity : AppCompatActivity() {
|
||||||
val notificationData = intent?.let { IntentCompat.getParcelableExtra(it, EXTRA_NOTIFICATION_DATA, CallNotificationData::class.java) }
|
val notificationData = intent?.let { IntentCompat.getParcelableExtra(it, EXTRA_NOTIFICATION_DATA, CallNotificationData::class.java) }
|
||||||
if (notificationData != null) {
|
if (notificationData != null) {
|
||||||
setContent {
|
setContent {
|
||||||
IncomingCallScreen(
|
ElementThemeApp(appPreferencesStore) {
|
||||||
notificationData = notificationData,
|
IncomingCallScreen(
|
||||||
onAnswer = ::onAnswer,
|
notificationData = notificationData,
|
||||||
onCancel = ::onCancel,
|
onAnswer = ::onAnswer,
|
||||||
)
|
onCancel = ::onCancel,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No data, finish the activity
|
// No data, finish the activity
|
||||||
|
|
|
||||||
|
|
@ -64,67 +64,65 @@ internal fun IncomingCallScreen(
|
||||||
onAnswer: (CallNotificationData) -> Unit,
|
onAnswer: (CallNotificationData) -> Unit,
|
||||||
onCancel: () -> Unit,
|
onCancel: () -> Unit,
|
||||||
) {
|
) {
|
||||||
ElementTheme {
|
OnboardingBackground()
|
||||||
OnboardingBackground()
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Bottom
|
||||||
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
.fillMaxWidth()
|
||||||
verticalArrangement = Arrangement.Bottom
|
.padding(start = 20.dp, end = 20.dp, top = 124.dp)
|
||||||
|
.weight(1f),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Column(
|
Avatar(
|
||||||
modifier = Modifier
|
avatarData = AvatarData(
|
||||||
.fillMaxWidth()
|
id = notificationData.senderId.value,
|
||||||
.padding(start = 20.dp, end = 20.dp, top = 124.dp)
|
name = notificationData.senderName,
|
||||||
.weight(1f),
|
url = notificationData.avatarUrl,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
size = AvatarSize.IncomingCall,
|
||||||
) {
|
|
||||||
Avatar(
|
|
||||||
avatarData = AvatarData(
|
|
||||||
id = notificationData.senderId.value,
|
|
||||||
name = notificationData.senderName,
|
|
||||||
url = notificationData.avatarUrl,
|
|
||||||
size = AvatarSize.IncomingCall,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
Text(
|
|
||||||
text = notificationData.senderName ?: notificationData.senderId.value,
|
|
||||||
style = ElementTheme.typography.fontHeadingMdBold,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.screen_incoming_call_subtitle_android),
|
|
||||||
style = ElementTheme.typography.fontBodyLgRegular,
|
|
||||||
color = ElementTheme.colors.textSecondary,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(start = 24.dp, end = 24.dp, bottom = 64.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
ActionButton(
|
|
||||||
size = 64.dp,
|
|
||||||
onClick = { onAnswer(notificationData) },
|
|
||||||
icon = CompoundIcons.VoiceCall(),
|
|
||||||
title = stringResource(CommonStrings.action_accept),
|
|
||||||
backgroundColor = ElementTheme.colors.iconSuccessPrimary,
|
|
||||||
borderColor = ElementTheme.colors.borderSuccessSubtle
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
Text(
|
||||||
|
text = notificationData.senderName ?: notificationData.senderId.value,
|
||||||
|
style = ElementTheme.typography.fontHeadingMdBold,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.screen_incoming_call_subtitle_android),
|
||||||
|
style = ElementTheme.typography.fontBodyLgRegular,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 24.dp, end = 24.dp, bottom = 64.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
ActionButton(
|
||||||
|
size = 64.dp,
|
||||||
|
onClick = { onAnswer(notificationData) },
|
||||||
|
icon = CompoundIcons.VoiceCall(),
|
||||||
|
title = stringResource(CommonStrings.action_accept),
|
||||||
|
backgroundColor = ElementTheme.colors.iconSuccessPrimary,
|
||||||
|
borderColor = ElementTheme.colors.borderSuccessSubtle
|
||||||
|
)
|
||||||
|
|
||||||
ActionButton(
|
ActionButton(
|
||||||
size = 64.dp,
|
size = 64.dp,
|
||||||
onClick = onCancel,
|
onClick = onCancel,
|
||||||
icon = CompoundIcons.EndCall(),
|
icon = CompoundIcons.EndCall(),
|
||||||
title = stringResource(CommonStrings.action_reject),
|
title = stringResource(CommonStrings.action_reject),
|
||||||
backgroundColor = ElementTheme.colors.iconCriticalPrimary,
|
backgroundColor = ElementTheme.colors.iconCriticalPrimary,
|
||||||
borderColor = ElementTheme.colors.borderCriticalSubtle
|
borderColor = ElementTheme.colors.borderCriticalSubtle
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -145,7 +143,8 @@ private fun ActionButton(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
FilledIconButton(
|
FilledIconButton(
|
||||||
modifier = Modifier.size(size + borderSize)
|
modifier = Modifier
|
||||||
|
.size(size + borderSize)
|
||||||
.border(borderSize, borderColor, CircleShape),
|
.border(borderSize, borderColor, CircleShape),
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
colors = IconButtonDefaults.filledIconButtonColors(
|
colors = IconButtonDefaults.filledIconButtonColors(
|
||||||
|
|
@ -171,22 +170,20 @@ private fun ActionButton(
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun IncomingCallScreenPreview() {
|
internal fun IncomingCallScreenPreview() = ElementPreview {
|
||||||
ElementPreview {
|
IncomingCallScreen(
|
||||||
IncomingCallScreen(
|
notificationData = CallNotificationData(
|
||||||
notificationData = CallNotificationData(
|
sessionId = SessionId("@alice:matrix.org"),
|
||||||
sessionId = SessionId("@alice:matrix.org"),
|
roomId = RoomId("!1234:matrix.org"),
|
||||||
roomId = RoomId("!1234:matrix.org"),
|
eventId = EventId("\$asdadadsad:matrix.org"),
|
||||||
eventId = EventId("\$asdadadsad:matrix.org"),
|
senderId = UserId("@bob:matrix.org"),
|
||||||
senderId = UserId("@bob:matrix.org"),
|
roomName = "A room",
|
||||||
roomName = "A room",
|
senderName = "Bob",
|
||||||
senderName = "Bob",
|
avatarUrl = null,
|
||||||
avatarUrl = null,
|
notificationChannelId = "incoming_call",
|
||||||
notificationChannelId = "incoming_call",
|
timestamp = 0L,
|
||||||
timestamp = 0L,
|
),
|
||||||
),
|
onAnswer = {},
|
||||||
onAnswer = {},
|
onCancel = {},
|
||||||
onCancel = {},
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,14 +25,25 @@ import io.element.android.features.call.impl.notifications.CallNotificationData
|
||||||
import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator
|
import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.di.SingleIn
|
import io.element.android.libraries.di.SingleIn
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||||
import io.element.android.libraries.push.api.notifications.ForegroundServiceType
|
import io.element.android.libraries.push.api.notifications.ForegroundServiceType
|
||||||
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
|
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
|
||||||
import io.element.android.libraries.push.api.notifications.OnMissedCallNotificationHandler
|
import io.element.android.libraries.push.api.notifications.OnMissedCallNotificationHandler
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.drop
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -79,11 +90,16 @@ class DefaultActiveCallManager @Inject constructor(
|
||||||
private val onMissedCallNotificationHandler: OnMissedCallNotificationHandler,
|
private val onMissedCallNotificationHandler: OnMissedCallNotificationHandler,
|
||||||
private val ringingCallNotificationCreator: RingingCallNotificationCreator,
|
private val ringingCallNotificationCreator: RingingCallNotificationCreator,
|
||||||
private val notificationManagerCompat: NotificationManagerCompat,
|
private val notificationManagerCompat: NotificationManagerCompat,
|
||||||
|
private val matrixClientProvider: MatrixClientProvider,
|
||||||
) : ActiveCallManager {
|
) : ActiveCallManager {
|
||||||
private var timedOutCallJob: Job? = null
|
private var timedOutCallJob: Job? = null
|
||||||
|
|
||||||
override val activeCall = MutableStateFlow<ActiveCall?>(null)
|
override val activeCall = MutableStateFlow<ActiveCall?>(null)
|
||||||
|
|
||||||
|
init {
|
||||||
|
observeRingingCall()
|
||||||
|
}
|
||||||
|
|
||||||
override fun registerIncomingCall(notificationData: CallNotificationData) {
|
override fun registerIncomingCall(notificationData: CallNotificationData) {
|
||||||
if (activeCall.value != null) {
|
if (activeCall.value != null) {
|
||||||
displayMissedCallNotification(notificationData)
|
displayMissedCallNotification(notificationData)
|
||||||
|
|
@ -173,6 +189,35 @@ class DefaultActiveCallManager @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
private fun observeRingingCall() {
|
||||||
|
// This will observe ringing calls and ensure they're terminated if the room call is cancelled
|
||||||
|
activeCall
|
||||||
|
.filterNotNull()
|
||||||
|
.filter { it.callState is CallState.Ringing && it.callType is CallType.RoomCall }
|
||||||
|
.flatMapLatest { activeCall ->
|
||||||
|
val callType = activeCall.callType as CallType.RoomCall
|
||||||
|
// Get a flow of updated `hasRoomCall` values for the room
|
||||||
|
matrixClientProvider.getOrRestore(callType.sessionId).getOrNull()
|
||||||
|
?.getRoom(callType.roomId)
|
||||||
|
?.roomInfoFlow
|
||||||
|
?.map { it.hasRoomCall }
|
||||||
|
?: flowOf()
|
||||||
|
}
|
||||||
|
// We only want to check if the room active call status changes
|
||||||
|
.distinctUntilChanged()
|
||||||
|
// Skip the first one, we're not interested in it (if the check below passes, it had to be active anyway)
|
||||||
|
.drop(1)
|
||||||
|
.onEach { roomHasActiveCall ->
|
||||||
|
if (!roomHasActiveCall) {
|
||||||
|
// The call was cancelled
|
||||||
|
timedOutCallJob?.cancel()
|
||||||
|
incomingCallTimedOut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.launchIn(coroutineScope)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,5 @@
|
||||||
<string name="call_foreground_service_channel_title_android">"Laufender Anruf"</string>
|
<string name="call_foreground_service_channel_title_android">"Laufender Anruf"</string>
|
||||||
<string name="call_foreground_service_message_android">"Tippen, um zum Anruf zurückzukehren"</string>
|
<string name="call_foreground_service_message_android">"Tippen, um zum Anruf zurückzukehren"</string>
|
||||||
<string name="call_foreground_service_title_android">"☎️ Anruf läuft"</string>
|
<string name="call_foreground_service_title_android">"☎️ Anruf läuft"</string>
|
||||||
|
<string name="screen_incoming_call_subtitle_android">"Eingehender Element Call"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="call_foreground_service_channel_title_android">"Połączenie w trakcie"</string>
|
||||||
|
<string name="call_foreground_service_message_android">"Stuknij, aby wrócić do rozmowy"</string>
|
||||||
|
<string name="call_foreground_service_title_android">"☎️ Rozmowa w toku"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="call_foreground_service_channel_title_android">"Chamada em andamento"</string>
|
||||||
|
<string name="call_foreground_service_message_android">"Toque para retornar à chamada"</string>
|
||||||
|
<string name="call_foreground_service_title_android">"☎️ Chamada em andamento"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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
|
||||||
|
*
|
||||||
|
* https://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.call.impl.pip
|
||||||
|
|
||||||
|
class FakePipSupportProvider(
|
||||||
|
private val isPipSupported: Boolean
|
||||||
|
) : PipSupportProvider {
|
||||||
|
override fun isPipSupported() = isPipSupported
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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
|
||||||
|
*
|
||||||
|
* https://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.call.impl.pip
|
||||||
|
|
||||||
|
import android.os.Build.VERSION_CODES
|
||||||
|
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.features.call.impl.ui.ElementCallActivity
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.Robolectric
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
class PictureInPicturePresenterTest {
|
||||||
|
@Test
|
||||||
|
@Config(sdk = [VERSION_CODES.O, VERSION_CODES.S])
|
||||||
|
fun `when pip is not supported, the state value supportPip is false`() = runTest {
|
||||||
|
val presenter = createPictureInPicturePresenter(supportPip = false)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.supportPip).isFalse()
|
||||||
|
}
|
||||||
|
presenter.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(sdk = [VERSION_CODES.O, VERSION_CODES.S])
|
||||||
|
fun `when pip is supported, the state value supportPip is true`() = runTest {
|
||||||
|
val presenter = createPictureInPicturePresenter(supportPip = true)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.supportPip).isTrue()
|
||||||
|
}
|
||||||
|
presenter.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(sdk = [VERSION_CODES.S])
|
||||||
|
fun `when entering pip is supported, the state value isInPictureInPicture is true`() = runTest {
|
||||||
|
val presenter = createPictureInPicturePresenter(supportPip = true)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.isInPictureInPicture).isFalse()
|
||||||
|
initialState.eventSink(PictureInPictureEvents.EnterPictureInPicture)
|
||||||
|
presenter.onPictureInPictureModeChanged(true)
|
||||||
|
val pipState = awaitItem()
|
||||||
|
assertThat(pipState.isInPictureInPicture).isTrue()
|
||||||
|
// User stops pip
|
||||||
|
presenter.onPictureInPictureModeChanged(false)
|
||||||
|
val finalState = awaitItem()
|
||||||
|
assertThat(finalState.isInPictureInPicture).isFalse()
|
||||||
|
}
|
||||||
|
presenter.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(sdk = [VERSION_CODES.S])
|
||||||
|
fun `when onUserLeaveHint is called, the state value isInPictureInPicture becomes true`() = runTest {
|
||||||
|
val presenter = createPictureInPicturePresenter(supportPip = true)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.isInPictureInPicture).isFalse()
|
||||||
|
presenter.onUserLeaveHint()
|
||||||
|
presenter.onPictureInPictureModeChanged(true)
|
||||||
|
val pipState = awaitItem()
|
||||||
|
assertThat(pipState.isInPictureInPicture).isTrue()
|
||||||
|
}
|
||||||
|
presenter.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createPictureInPicturePresenter(
|
||||||
|
supportPip: Boolean = true,
|
||||||
|
): PictureInPicturePresenter {
|
||||||
|
val activity = Robolectric.buildActivity(ElementCallActivity::class.java)
|
||||||
|
return PictureInPicturePresenter(
|
||||||
|
pipSupportProvider = FakePipSupportProvider(supportPip),
|
||||||
|
).apply {
|
||||||
|
onCreate(activity.get())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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
|
||||||
|
*
|
||||||
|
* https://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.call.impl.ui
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import io.element.android.features.call.impl.pip.PictureInPictureEvents
|
||||||
|
import io.element.android.features.call.impl.pip.PictureInPictureState
|
||||||
|
import io.element.android.features.call.impl.pip.aPictureInPictureState
|
||||||
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
|
import io.element.android.tests.testutils.pressBack
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class CallScreenViewTest {
|
||||||
|
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on back when pip is not supported hangs up`() {
|
||||||
|
val eventsRecorder = EventsRecorder<CallScreenEvents>()
|
||||||
|
val pipEventsRecorder = EventsRecorder<PictureInPictureEvents>(expectEvents = false)
|
||||||
|
rule.setCallScreenView(
|
||||||
|
aCallScreenState(
|
||||||
|
eventSink = eventsRecorder
|
||||||
|
),
|
||||||
|
aPictureInPictureState(
|
||||||
|
supportPip = false,
|
||||||
|
eventSink = pipEventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.pressBack()
|
||||||
|
eventsRecorder.assertSize(2)
|
||||||
|
eventsRecorder.assertTrue(0) { it is CallScreenEvents.SetupMessageChannels }
|
||||||
|
eventsRecorder.assertTrue(1) { it == CallScreenEvents.Hangup }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on back when pip is supported enables PiP`() {
|
||||||
|
val eventsRecorder = EventsRecorder<CallScreenEvents>()
|
||||||
|
val pipEventsRecorder = EventsRecorder<PictureInPictureEvents>()
|
||||||
|
rule.setCallScreenView(
|
||||||
|
aCallScreenState(
|
||||||
|
eventSink = eventsRecorder
|
||||||
|
),
|
||||||
|
aPictureInPictureState(
|
||||||
|
supportPip = true,
|
||||||
|
eventSink = pipEventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.pressBack()
|
||||||
|
eventsRecorder.assertSize(1)
|
||||||
|
eventsRecorder.assertTrue(0) { it is CallScreenEvents.SetupMessageChannels }
|
||||||
|
pipEventsRecorder.assertSingle(PictureInPictureEvents.EnterPictureInPicture)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setCallScreenView(
|
||||||
|
state: CallScreenState,
|
||||||
|
pipState: PictureInPictureState,
|
||||||
|
requestPermissions: (Array<String>, RequestPermissionCallback) -> Unit = { _, _ -> },
|
||||||
|
) {
|
||||||
|
setContent {
|
||||||
|
CallScreenView(
|
||||||
|
state = state,
|
||||||
|
pipState = pipState,
|
||||||
|
requestPermissions = requestPermissions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -32,7 +32,10 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
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.FakeMatrixClientProvider
|
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||||
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||||
import io.element.android.libraries.push.api.notifications.ForegroundServiceType
|
import io.element.android.libraries.push.api.notifications.ForegroundServiceType
|
||||||
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
|
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
|
||||||
import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder
|
import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder
|
||||||
|
|
@ -42,7 +45,11 @@ import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.advanceTimeBy
|
import kotlinx.coroutines.test.advanceTimeBy
|
||||||
import kotlinx.coroutines.test.runCurrent
|
import kotlinx.coroutines.test.runCurrent
|
||||||
|
|
@ -59,26 +66,28 @@ class DefaultActiveCallManagerTest {
|
||||||
@Test
|
@Test
|
||||||
fun `registerIncomingCall - sets the incoming call as active`() = runTest {
|
fun `registerIncomingCall - sets the incoming call as active`() = runTest {
|
||||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
inCancellableScope {
|
||||||
|
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
||||||
|
|
||||||
assertThat(manager.activeCall.value).isNull()
|
assertThat(manager.activeCall.value).isNull()
|
||||||
|
|
||||||
val callNotificationData = aCallNotificationData()
|
val callNotificationData = aCallNotificationData()
|
||||||
manager.registerIncomingCall(callNotificationData)
|
manager.registerIncomingCall(callNotificationData)
|
||||||
|
|
||||||
assertThat(manager.activeCall.value).isEqualTo(
|
assertThat(manager.activeCall.value).isEqualTo(
|
||||||
ActiveCall(
|
ActiveCall(
|
||||||
callType = CallType.RoomCall(
|
callType = CallType.RoomCall(
|
||||||
sessionId = callNotificationData.sessionId,
|
sessionId = callNotificationData.sessionId,
|
||||||
roomId = callNotificationData.roomId,
|
roomId = callNotificationData.roomId,
|
||||||
),
|
),
|
||||||
callState = CallState.Ringing(callNotificationData)
|
callState = CallState.Ringing(callNotificationData)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
runCurrent()
|
runCurrent()
|
||||||
|
|
||||||
verify { notificationManagerCompat.notify(notificationId, any()) }
|
verify { notificationManagerCompat.notify(notificationId, any()) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
@ -86,38 +95,42 @@ class DefaultActiveCallManagerTest {
|
||||||
fun `registerIncomingCall - when there is an already active call adds missed call notification`() = runTest {
|
fun `registerIncomingCall - when there is an already active call adds missed call notification`() = runTest {
|
||||||
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
|
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
|
||||||
val onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda)
|
val onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda)
|
||||||
val manager = createActiveCallManager(
|
inCancellableScope {
|
||||||
onMissedCallNotificationHandler = onMissedCallNotificationHandler,
|
val manager = createActiveCallManager(
|
||||||
)
|
onMissedCallNotificationHandler = onMissedCallNotificationHandler,
|
||||||
|
)
|
||||||
|
|
||||||
// Register existing call
|
// Register existing call
|
||||||
val callNotificationData = aCallNotificationData()
|
val callNotificationData = aCallNotificationData()
|
||||||
manager.registerIncomingCall(callNotificationData)
|
manager.registerIncomingCall(callNotificationData)
|
||||||
val activeCall = manager.activeCall.value
|
val activeCall = manager.activeCall.value
|
||||||
|
|
||||||
// Now add a new call
|
// Now add a new call
|
||||||
manager.registerIncomingCall(aCallNotificationData(roomId = A_ROOM_ID_2))
|
manager.registerIncomingCall(aCallNotificationData(roomId = A_ROOM_ID_2))
|
||||||
|
|
||||||
assertThat(manager.activeCall.value).isEqualTo(activeCall)
|
assertThat(manager.activeCall.value).isEqualTo(activeCall)
|
||||||
assertThat((manager.activeCall.value?.callType as? CallType.RoomCall)?.roomId).isNotEqualTo(A_ROOM_ID_2)
|
assertThat((manager.activeCall.value?.callType as? CallType.RoomCall)?.roomId).isNotEqualTo(A_ROOM_ID_2)
|
||||||
|
|
||||||
advanceTimeBy(1)
|
advanceTimeBy(1)
|
||||||
|
|
||||||
addMissedCallNotificationLambda.assertions()
|
addMissedCallNotificationLambda.assertions()
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
.with(value(A_SESSION_ID), value(A_ROOM_ID_2), value(AN_EVENT_ID))
|
.with(value(A_SESSION_ID), value(A_ROOM_ID_2), value(AN_EVENT_ID))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `incomingCallTimedOut - when there isn't an active call does nothing`() = runTest {
|
fun `incomingCallTimedOut - when there isn't an active call does nothing`() = runTest {
|
||||||
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
|
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
|
||||||
val manager = createActiveCallManager(
|
inCancellableScope {
|
||||||
onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda)
|
val manager = createActiveCallManager(
|
||||||
)
|
onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda)
|
||||||
|
)
|
||||||
|
|
||||||
manager.incomingCallTimedOut()
|
manager.incomingCallTimedOut()
|
||||||
|
|
||||||
addMissedCallNotificationLambda.assertions().isNeverCalled()
|
addMissedCallNotificationLambda.assertions().isNeverCalled()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
@ -125,82 +138,167 @@ class DefaultActiveCallManagerTest {
|
||||||
fun `incomingCallTimedOut - when there is an active call removes it and adds a missed call notification`() = runTest {
|
fun `incomingCallTimedOut - when there is an active call removes it and adds a missed call notification`() = runTest {
|
||||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||||
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
|
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
|
||||||
val manager = createActiveCallManager(
|
inCancellableScope {
|
||||||
onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda),
|
val manager = createActiveCallManager(
|
||||||
notificationManagerCompat = notificationManagerCompat,
|
onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda),
|
||||||
)
|
notificationManagerCompat = notificationManagerCompat,
|
||||||
|
)
|
||||||
|
|
||||||
manager.registerIncomingCall(aCallNotificationData())
|
manager.registerIncomingCall(aCallNotificationData())
|
||||||
assertThat(manager.activeCall.value).isNotNull()
|
assertThat(manager.activeCall.value).isNotNull()
|
||||||
|
|
||||||
manager.incomingCallTimedOut()
|
manager.incomingCallTimedOut()
|
||||||
advanceTimeBy(1)
|
advanceTimeBy(1)
|
||||||
|
|
||||||
assertThat(manager.activeCall.value).isNull()
|
assertThat(manager.activeCall.value).isNull()
|
||||||
addMissedCallNotificationLambda.assertions().isCalledOnce()
|
addMissedCallNotificationLambda.assertions().isCalledOnce()
|
||||||
verify { notificationManagerCompat.cancel(notificationId) }
|
verify { notificationManagerCompat.cancel(notificationId) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `hungUpCall - removes existing call if the CallType matches`() = runTest {
|
fun `hungUpCall - removes existing call if the CallType matches`() = runTest {
|
||||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
// Create a cancellable coroutine scope to cancel the test when needed
|
||||||
|
inCancellableScope {
|
||||||
|
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
||||||
|
|
||||||
val notificationData = aCallNotificationData()
|
val notificationData = aCallNotificationData()
|
||||||
manager.registerIncomingCall(notificationData)
|
manager.registerIncomingCall(notificationData)
|
||||||
assertThat(manager.activeCall.value).isNotNull()
|
assertThat(manager.activeCall.value).isNotNull()
|
||||||
|
|
||||||
manager.hungUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
|
manager.hungUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
|
||||||
assertThat(manager.activeCall.value).isNull()
|
assertThat(manager.activeCall.value).isNull()
|
||||||
|
|
||||||
verify { notificationManagerCompat.cancel(notificationId) }
|
verify { notificationManagerCompat.cancel(notificationId) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `hungUpCall - does nothing if the CallType doesn't match`() = runTest {
|
fun `hungUpCall - does nothing if the CallType doesn't match`() = runTest {
|
||||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||||
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
// Create a cancellable coroutine scope to cancel the test when needed
|
||||||
|
inCancellableScope {
|
||||||
|
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
||||||
|
|
||||||
manager.registerIncomingCall(aCallNotificationData())
|
manager.registerIncomingCall(aCallNotificationData())
|
||||||
assertThat(manager.activeCall.value).isNotNull()
|
assertThat(manager.activeCall.value).isNotNull()
|
||||||
|
|
||||||
manager.hungUpCall(CallType.ExternalUrl("https://example.com"))
|
manager.hungUpCall(CallType.ExternalUrl("https://example.com"))
|
||||||
assertThat(manager.activeCall.value).isNotNull()
|
assertThat(manager.activeCall.value).isNotNull()
|
||||||
|
|
||||||
verify(exactly = 0) { notificationManagerCompat.cancel(notificationId) }
|
verify(exactly = 0) { notificationManagerCompat.cancel(notificationId) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@Test
|
@Test
|
||||||
fun `joinedCall - register an ongoing call and tries sending the call notify event`() = runTest {
|
fun `joinedCall - register an ongoing call and tries sending the call notify event`() = runTest {
|
||||||
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
|
||||||
val manager = createActiveCallManager(
|
inCancellableScope {
|
||||||
notificationManagerCompat = notificationManagerCompat,
|
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
||||||
)
|
assertThat(manager.activeCall.value).isNull()
|
||||||
assertThat(manager.activeCall.value).isNull()
|
|
||||||
|
|
||||||
manager.joinedCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID))
|
manager.joinedCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID))
|
||||||
assertThat(manager.activeCall.value).isEqualTo(
|
assertThat(manager.activeCall.value).isEqualTo(
|
||||||
ActiveCall(
|
ActiveCall(
|
||||||
callType = CallType.RoomCall(
|
callType = CallType.RoomCall(
|
||||||
sessionId = A_SESSION_ID,
|
sessionId = A_SESSION_ID,
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
),
|
),
|
||||||
callState = CallState.InCall,
|
callState = CallState.InCall,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
runCurrent()
|
runCurrent()
|
||||||
|
|
||||||
verify { notificationManagerCompat.cancel(notificationId) }
|
verify { notificationManagerCompat.cancel(notificationId) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TestScope.createActiveCallManager(
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
@Test
|
||||||
|
fun `observeRingingCalls - will cancel the active ringing call if the call is cancelled`() = runTest {
|
||||||
|
val room = FakeMatrixRoom().apply {
|
||||||
|
givenRoomInfo(aRoomInfo())
|
||||||
|
}
|
||||||
|
val client = FakeMatrixClient().apply {
|
||||||
|
givenGetRoomResult(A_ROOM_ID, room)
|
||||||
|
}
|
||||||
|
// Create a cancellable coroutine scope to cancel the test when needed
|
||||||
|
inCancellableScope {
|
||||||
|
val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(client) })
|
||||||
|
val manager = createActiveCallManager(matrixClientProvider = matrixClientProvider)
|
||||||
|
|
||||||
|
manager.registerIncomingCall(aCallNotificationData())
|
||||||
|
|
||||||
|
// Call is active (the other user join the call)
|
||||||
|
room.givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
||||||
|
advanceTimeBy(1)
|
||||||
|
// Call is cancelled (the other user left the call)
|
||||||
|
room.givenRoomInfo(aRoomInfo(hasRoomCall = false))
|
||||||
|
advanceTimeBy(1)
|
||||||
|
|
||||||
|
assertThat(manager.activeCall.value).isNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
@Test
|
||||||
|
fun `observeRingingCalls - will do nothing if either the session or the room are not found`() = runTest {
|
||||||
|
val room = FakeMatrixRoom().apply {
|
||||||
|
givenRoomInfo(aRoomInfo())
|
||||||
|
}
|
||||||
|
val client = FakeMatrixClient().apply {
|
||||||
|
givenGetRoomResult(A_ROOM_ID, room)
|
||||||
|
}
|
||||||
|
// Create a cancellable coroutine scope to cancel the test when needed
|
||||||
|
inCancellableScope {
|
||||||
|
val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.failure(IllegalStateException("Matrix client not found")) })
|
||||||
|
val manager = createActiveCallManager(matrixClientProvider = matrixClientProvider)
|
||||||
|
|
||||||
|
// No matrix client
|
||||||
|
|
||||||
|
manager.registerIncomingCall(aCallNotificationData())
|
||||||
|
|
||||||
|
room.givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
||||||
|
advanceTimeBy(1)
|
||||||
|
room.givenRoomInfo(aRoomInfo(hasRoomCall = false))
|
||||||
|
advanceTimeBy(1)
|
||||||
|
|
||||||
|
// The call should still be active
|
||||||
|
assertThat(manager.activeCall.value).isNotNull()
|
||||||
|
|
||||||
|
// No room
|
||||||
|
client.givenGetRoomResult(A_ROOM_ID, null)
|
||||||
|
matrixClientProvider.getClient = { Result.success(client) }
|
||||||
|
|
||||||
|
manager.registerIncomingCall(aCallNotificationData())
|
||||||
|
|
||||||
|
room.givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
||||||
|
advanceTimeBy(1)
|
||||||
|
room.givenRoomInfo(aRoomInfo(hasRoomCall = false))
|
||||||
|
advanceTimeBy(1)
|
||||||
|
|
||||||
|
// The call should still be active
|
||||||
|
assertThat(manager.activeCall.value).isNotNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TestScope.inCancellableScope(block: suspend CoroutineScope.() -> Unit) {
|
||||||
|
launch(SupervisorJob()) {
|
||||||
|
block()
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.createActiveCallManager(
|
||||||
matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
|
matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
|
||||||
onMissedCallNotificationHandler: FakeOnMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(),
|
onMissedCallNotificationHandler: FakeOnMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(),
|
||||||
notificationManagerCompat: NotificationManagerCompat = mockk(relaxed = true),
|
notificationManagerCompat: NotificationManagerCompat = mockk(relaxed = true),
|
||||||
|
coroutineScope: CoroutineScope = this,
|
||||||
) = DefaultActiveCallManager(
|
) = DefaultActiveCallManager(
|
||||||
coroutineScope = this,
|
coroutineScope = coroutineScope,
|
||||||
onMissedCallNotificationHandler = onMissedCallNotificationHandler,
|
onMissedCallNotificationHandler = onMissedCallNotificationHandler,
|
||||||
ringingCallNotificationCreator = RingingCallNotificationCreator(
|
ringingCallNotificationCreator = RingingCallNotificationCreator(
|
||||||
context = InstrumentationRegistry.getInstrumentation().targetContext,
|
context = InstrumentationRegistry.getInstrumentation().targetContext,
|
||||||
|
|
@ -209,5 +307,6 @@ class DefaultActiveCallManagerTest {
|
||||||
notificationBitmapLoader = FakeNotificationBitmapLoader(),
|
notificationBitmapLoader = FakeNotificationBitmapLoader(),
|
||||||
),
|
),
|
||||||
notificationManagerCompat = notificationManagerCompat,
|
notificationManagerCompat = notificationManagerCompat,
|
||||||
|
matrixClientProvider = matrixClientProvider,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,9 +54,9 @@ class DefaultCallWidgetProviderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `getWidget - fails if it can't generate the URL for the widget`() = runTest {
|
fun `getWidget - fails if it can't generate the URL for the widget`() = runTest {
|
||||||
val room = FakeMatrixRoom().apply {
|
val room = FakeMatrixRoom(
|
||||||
givenGenerateWidgetWebViewUrlResult(Result.failure(Exception("Can't generate URL for widget")))
|
generateWidgetWebViewUrlResult = { _, _, _, _ -> Result.failure(Exception("Can't generate URL for widget")) }
|
||||||
}
|
)
|
||||||
val client = FakeMatrixClient().apply {
|
val client = FakeMatrixClient().apply {
|
||||||
givenGetRoomResult(A_ROOM_ID, room)
|
givenGetRoomResult(A_ROOM_ID, room)
|
||||||
}
|
}
|
||||||
|
|
@ -66,10 +66,10 @@ class DefaultCallWidgetProviderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `getWidget - fails if it can't get the widget driver`() = runTest {
|
fun `getWidget - fails if it can't get the widget driver`() = runTest {
|
||||||
val room = FakeMatrixRoom().apply {
|
val room = FakeMatrixRoom(
|
||||||
givenGenerateWidgetWebViewUrlResult(Result.success("url"))
|
generateWidgetWebViewUrlResult = { _, _, _, _ -> Result.success("url") },
|
||||||
givenGetWidgetDriverResult(Result.failure(Exception("Can't get a widget driver")))
|
getWidgetDriverResult = { Result.failure(Exception("Can't get a widget driver")) }
|
||||||
}
|
)
|
||||||
val client = FakeMatrixClient().apply {
|
val client = FakeMatrixClient().apply {
|
||||||
givenGetRoomResult(A_ROOM_ID, room)
|
givenGetRoomResult(A_ROOM_ID, room)
|
||||||
}
|
}
|
||||||
|
|
@ -79,10 +79,10 @@ class DefaultCallWidgetProviderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `getWidget - returns a widget driver when all steps are successful`() = runTest {
|
fun `getWidget - returns a widget driver when all steps are successful`() = runTest {
|
||||||
val room = FakeMatrixRoom().apply {
|
val room = FakeMatrixRoom(
|
||||||
givenGenerateWidgetWebViewUrlResult(Result.success("url"))
|
generateWidgetWebViewUrlResult = { _, _, _, _ -> Result.success("url") },
|
||||||
givenGetWidgetDriverResult(Result.success(FakeMatrixWidgetDriver()))
|
getWidgetDriverResult = { Result.success(FakeMatrixWidgetDriver()) },
|
||||||
}
|
)
|
||||||
val client = FakeMatrixClient().apply {
|
val client = FakeMatrixClient().apply {
|
||||||
givenGetRoomResult(A_ROOM_ID, room)
|
givenGetRoomResult(A_ROOM_ID, room)
|
||||||
}
|
}
|
||||||
|
|
@ -92,10 +92,10 @@ class DefaultCallWidgetProviderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `getWidget - will use a custom base url if it exists`() = runTest {
|
fun `getWidget - will use a custom base url if it exists`() = runTest {
|
||||||
val room = FakeMatrixRoom().apply {
|
val room = FakeMatrixRoom(
|
||||||
givenGenerateWidgetWebViewUrlResult(Result.success("url"))
|
generateWidgetWebViewUrlResult = { _, _, _, _ -> Result.success("url") },
|
||||||
givenGetWidgetDriverResult(Result.success(FakeMatrixWidgetDriver()))
|
getWidgetDriverResult = { Result.success(FakeMatrixWidgetDriver()) },
|
||||||
}
|
)
|
||||||
val client = FakeMatrixClient().apply {
|
val client = FakeMatrixClient().apply {
|
||||||
givenGetRoomResult(A_ROOM_ID, room)
|
givenGetRoomResult(A_ROOM_ID, room)
|
||||||
}
|
}
|
||||||
|
|
@ -120,10 +120,10 @@ class DefaultCallWidgetProviderTest {
|
||||||
val elementCallBaseUrlProvider = FakeElementCallBaseUrlProvider { matrixClient ->
|
val elementCallBaseUrlProvider = FakeElementCallBaseUrlProvider { matrixClient ->
|
||||||
providesLambda(matrixClient)
|
providesLambda(matrixClient)
|
||||||
}
|
}
|
||||||
val room = FakeMatrixRoom().apply {
|
val room = FakeMatrixRoom(
|
||||||
givenGenerateWidgetWebViewUrlResult(Result.success("url"))
|
generateWidgetWebViewUrlResult = { _, _, _, _ -> Result.success("url") },
|
||||||
givenGetWidgetDriverResult(Result.success(FakeMatrixWidgetDriver()))
|
getWidgetDriverResult = { Result.success(FakeMatrixWidgetDriver()) },
|
||||||
}
|
)
|
||||||
val client = FakeMatrixClient().apply {
|
val client = FakeMatrixClient().apply {
|
||||||
givenGetRoomResult(A_ROOM_ID, room)
|
givenGetRoomResult(A_ROOM_ID, room)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_create_room_action_create_room">"Nowy pokój"</string>
|
||||||
|
<string name="screen_create_room_add_people_title">"Zaproś znajomych"</string>
|
||||||
|
<string name="screen_create_room_error_creating_room">"Wystąpił błąd podczas tworzenia pokoju"</string>
|
||||||
|
<string name="screen_create_room_private_option_description">"Wiadomości w tym pokoju są szyfrowane. Szyfrowania nie można później wyłączyć."</string>
|
||||||
|
<string name="screen_create_room_private_option_title">"Pokój prywatny (tylko zaproszenie)"</string>
|
||||||
|
<string name="screen_create_room_public_option_description">"Wiadomości nie są szyfrowane i każdy może je odczytać. Możesz aktywować szyfrowanie później."</string>
|
||||||
|
<string name="screen_create_room_public_option_title">"Pokój publiczny (każdy)"</string>
|
||||||
|
<string name="screen_create_room_room_name_label">"Nazwa pokoju"</string>
|
||||||
|
<string name="screen_create_room_title">"Utwórz pokój"</string>
|
||||||
|
<string name="screen_create_room_topic_label">"Temat (opcjonalnie)"</string>
|
||||||
|
<string name="screen_start_chat_error_starting_chat">"Wystąpił błąd podczas próby rozpoczęcia czatu"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_create_room_action_create_room">"Nova sala"</string>
|
||||||
|
<string name="screen_create_room_add_people_title">"Convidar pessoas"</string>
|
||||||
|
<string name="screen_create_room_error_creating_room">"Ocorreu um erro ao criar a sala"</string>
|
||||||
|
<string name="screen_create_room_private_option_description">"As mensagens nesta sala serão criptografadas. A criptografia não pode ser desativada posteriormente."</string>
|
||||||
|
<string name="screen_create_room_private_option_title">"Sala privativa (somente por convite)"</string>
|
||||||
|
<string name="screen_create_room_public_option_description">"As mensagens não serão criptografadas e qualquer pessoa pode lê-las. Você pode ativar a criptografia posteriormente."</string>
|
||||||
|
<string name="screen_create_room_public_option_title">"Sala pública (qualquer pessoa)"</string>
|
||||||
|
<string name="screen_create_room_room_name_label">"Nome da sala"</string>
|
||||||
|
<string name="screen_create_room_title">"Criar uma sala"</string>
|
||||||
|
<string name="screen_create_room_topic_label">"Tópico (opcional)"</string>
|
||||||
|
<string name="screen_start_chat_error_starting_chat">"Ocorreu um erro ao tentar iniciar um chat"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -45,7 +45,7 @@ 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
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -93,11 +93,9 @@ class FtueFlowNode @AssistedInject constructor(
|
||||||
})
|
})
|
||||||
|
|
||||||
analyticsService.didAskUserConsent()
|
analyticsService.didAskUserConsent()
|
||||||
.drop(1) // We only care about consent passing from not asked to asked state
|
.distinctUntilChanged()
|
||||||
.onEach { didAskUserConsent ->
|
.onEach {
|
||||||
if (didAskUserConsent) {
|
lifecycleScope.launch { moveToNextStep() }
|
||||||
lifecycleScope.launch { moveToNextStep() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.launchIn(lifecycleScope)
|
.launchIn(lifecycleScope)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import io.element.android.features.ftue.api.state.FtueService
|
||||||
import io.element.android.features.ftue.api.state.FtueState
|
import io.element.android.features.ftue.api.state.FtueState
|
||||||
import io.element.android.features.lockscreen.api.LockScreenService
|
import io.element.android.features.lockscreen.api.LockScreenService
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
import io.element.android.libraries.permissions.api.PermissionStateProvider
|
import io.element.android.libraries.permissions.api.PermissionStateProvider
|
||||||
|
|
@ -34,6 +35,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
|
@ -47,7 +49,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||||
@ContributesBinding(SessionScope::class)
|
@ContributesBinding(SessionScope::class)
|
||||||
class DefaultFtueService @Inject constructor(
|
class DefaultFtueService @Inject constructor(
|
||||||
private val sdkVersionProvider: BuildVersionSdkIntProvider,
|
private val sdkVersionProvider: BuildVersionSdkIntProvider,
|
||||||
coroutineScope: CoroutineScope,
|
@SessionCoroutineScope sessionCoroutineScope: CoroutineScope,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
private val permissionStateProvider: PermissionStateProvider,
|
private val permissionStateProvider: PermissionStateProvider,
|
||||||
private val lockScreenService: LockScreenService,
|
private val lockScreenService: LockScreenService,
|
||||||
|
|
@ -66,11 +68,12 @@ class DefaultFtueService @Inject constructor(
|
||||||
init {
|
init {
|
||||||
sessionVerificationService.sessionVerifiedStatus
|
sessionVerificationService.sessionVerifiedStatus
|
||||||
.onEach { updateState() }
|
.onEach { updateState() }
|
||||||
.launchIn(coroutineScope)
|
.launchIn(sessionCoroutineScope)
|
||||||
|
|
||||||
analyticsService.didAskUserConsent()
|
analyticsService.didAskUserConsent()
|
||||||
|
.distinctUntilChanged()
|
||||||
.onEach { updateState() }
|
.onEach { updateState() }
|
||||||
.launchIn(coroutineScope)
|
.launchIn(sessionCoroutineScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getNextStep(currentStep: FtueStep? = null): FtueStep? =
|
suspend fun getNextStep(currentStep: FtueStep? = null): FtueStep? =
|
||||||
|
|
@ -119,10 +122,7 @@ class DefaultFtueService @Inject constructor(
|
||||||
emit(SessionVerifiedStatus.NotVerified)
|
emit(SessionVerifiedStatus.NotVerified)
|
||||||
}
|
}
|
||||||
.first()
|
.first()
|
||||||
// For some obscure reason we need to call this *before* we check the `readyVerifiedSessionStatus`, otherwise there's a deadlock
|
return readyVerifiedSessionStatus == SessionVerifiedStatus.NotVerified && !canSkipVerification()
|
||||||
// It seems like a DataStore bug
|
|
||||||
val skipVerification = canSkipVerification()
|
|
||||||
return readyVerifiedSessionStatus == SessionVerifiedStatus.NotVerified && !skipVerification
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun canSkipVerification(): Boolean {
|
private suspend fun canSkipVerification(): Boolean {
|
||||||
|
|
@ -130,7 +130,6 @@ class DefaultFtueService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun needsAnalyticsOptIn(): Boolean {
|
private suspend fun needsAnalyticsOptIn(): Boolean {
|
||||||
// We need this function to not be suspend, so we need to load the value through runBlocking
|
|
||||||
return analyticsService.didAskUserConsent().first().not()
|
return analyticsService.didAskUserConsent().first().not()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
11
features/ftue/impl/src/main/res/values-pl/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_notification_optin_subtitle">"Możesz zmienić ustawienia później."</string>
|
||||||
|
<string name="screen_notification_optin_title">"Zezwól na powiadomienia i nie przegap żadnej wiadomości"</string>
|
||||||
|
<string name="screen_welcome_bullet_1">"Połączenia, ankiety, wyszukiwanie i inne zostaną dodane później w tym roku."</string>
|
||||||
|
<string name="screen_welcome_bullet_2">"Historia wiadomości dla pokoi szyfrowanych nie jest jeszcze dostępna."</string>
|
||||||
|
<string name="screen_welcome_bullet_3">"Chętnie poznamy Twoją opinię. Daj nam znać, co myślisz na stronie ustawień."</string>
|
||||||
|
<string name="screen_welcome_button">"Naprzód!"</string>
|
||||||
|
<string name="screen_welcome_subtitle">"Oto, co musisz wiedzieć:"</string>
|
||||||
|
<string name="screen_welcome_title">"Witamy w %1$s!"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -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_notification_optin_subtitle">"Você pode alterar suas configurações mais tarde."</string>
|
||||||
|
<string name="screen_notification_optin_title">"Permita notificações e nunca perca uma mensagem"</string>
|
||||||
|
<string name="screen_welcome_bullet_1">"Chamadas, enquetes, pesquisa e muito mais serão adicionadas ainda este ano."</string>
|
||||||
|
<string name="screen_welcome_bullet_2">"O histórico de mensagens para salas criptografadas ainda não está disponível."</string>
|
||||||
|
<string name="screen_welcome_bullet_3">"Adoraríamos ouvir sua opinião. Deixe-nos saber o que você pensa através da página de configurações."</string>
|
||||||
|
<string name="screen_welcome_button">"Vamos lá!"</string>
|
||||||
|
<string name="screen_welcome_subtitle">"Aqui está o que você precisa saber:"</string>
|
||||||
|
<string name="screen_welcome_title">"Bem-vindo ao %1$s!"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -251,7 +251,7 @@ class DefaultFtueServiceTest {
|
||||||
// First version where notification permission is required
|
// First version where notification permission is required
|
||||||
sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU,
|
sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU,
|
||||||
) = DefaultFtueService(
|
) = DefaultFtueService(
|
||||||
coroutineScope = coroutineScope,
|
sessionCoroutineScope = coroutineScope,
|
||||||
sessionVerificationService = sessionVerificationService,
|
sessionVerificationService = sessionVerificationService,
|
||||||
sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkIntVersion),
|
sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkIntVersion),
|
||||||
analyticsService = analyticsService,
|
analyticsService = analyticsService,
|
||||||
|
|
|
||||||
|
|
@ -27,13 +27,13 @@ open class AcceptDeclineInviteStateProvider : PreviewParameterProvider<AcceptDec
|
||||||
anAcceptDeclineInviteState(),
|
anAcceptDeclineInviteState(),
|
||||||
anAcceptDeclineInviteState(
|
anAcceptDeclineInviteState(
|
||||||
invite = Optional.of(
|
invite = Optional.of(
|
||||||
InviteData(RoomId("!room:matrix.org"), isDirect = true, roomName = "Alice"),
|
InviteData(RoomId("!room:matrix.org"), isDm = true, roomName = "Alice"),
|
||||||
),
|
),
|
||||||
declineAction = AsyncAction.Confirming,
|
declineAction = AsyncAction.Confirming,
|
||||||
),
|
),
|
||||||
anAcceptDeclineInviteState(
|
anAcceptDeclineInviteState(
|
||||||
invite = Optional.of(
|
invite = Optional.of(
|
||||||
InviteData(RoomId("!room:matrix.org"), isDirect = false, roomName = "Some room"),
|
InviteData(RoomId("!room:matrix.org"), isDm = false, roomName = "Some room"),
|
||||||
),
|
),
|
||||||
declineAction = AsyncAction.Confirming,
|
declineAction = AsyncAction.Confirming,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -21,5 +21,5 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
data class InviteData(
|
data class InviteData(
|
||||||
val roomId: RoomId,
|
val roomId: RoomId,
|
||||||
val roomName: String,
|
val roomName: String,
|
||||||
val isDirect: Boolean,
|
val isDm: Boolean,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ import io.element.android.libraries.architecture.runUpdatingState
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
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.room.join.JoinRoom
|
import io.element.android.libraries.matrix.api.room.join.JoinRoom
|
||||||
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
|
|
@ -44,7 +44,7 @@ import kotlin.jvm.optionals.getOrNull
|
||||||
class AcceptDeclineInvitePresenter @Inject constructor(
|
class AcceptDeclineInvitePresenter @Inject constructor(
|
||||||
private val client: MatrixClient,
|
private val client: MatrixClient,
|
||||||
private val joinRoom: JoinRoom,
|
private val joinRoom: JoinRoom,
|
||||||
private val notificationDrawerManager: NotificationDrawerManager,
|
private val notificationCleaner: NotificationCleaner,
|
||||||
) : Presenter<AcceptDeclineInviteState> {
|
) : Presenter<AcceptDeclineInviteState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): AcceptDeclineInviteState {
|
override fun present(): AcceptDeclineInviteState {
|
||||||
|
|
@ -112,7 +112,7 @@ class AcceptDeclineInvitePresenter @Inject constructor(
|
||||||
trigger = JoinedRoom.Trigger.Invite,
|
trigger = JoinedRoom.Trigger.Invite,
|
||||||
)
|
)
|
||||||
.onSuccess {
|
.onSuccess {
|
||||||
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId)
|
notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, roomId)
|
||||||
}
|
}
|
||||||
.map { roomId }
|
.map { roomId }
|
||||||
}
|
}
|
||||||
|
|
@ -122,7 +122,7 @@ class AcceptDeclineInvitePresenter @Inject constructor(
|
||||||
suspend {
|
suspend {
|
||||||
client.getRoom(roomId)?.use {
|
client.getRoom(roomId)?.use {
|
||||||
it.leave().getOrThrow()
|
it.leave().getOrThrow()
|
||||||
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId)
|
notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, roomId)
|
||||||
}
|
}
|
||||||
roomId
|
roomId
|
||||||
}.runCatchingUpdatingState(declinedAction)
|
}.runCatchingUpdatingState(declinedAction)
|
||||||
|
|
|
||||||
|
|
@ -79,13 +79,13 @@ private fun DeclineConfirmationDialog(
|
||||||
onDismissClick: () -> Unit,
|
onDismissClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val contentResource = if (invite.isDirect) {
|
val contentResource = if (invite.isDm) {
|
||||||
R.string.screen_invites_decline_direct_chat_message
|
R.string.screen_invites_decline_direct_chat_message
|
||||||
} else {
|
} else {
|
||||||
R.string.screen_invites_decline_chat_message
|
R.string.screen_invites_decline_chat_message
|
||||||
}
|
}
|
||||||
|
|
||||||
val titleResource = if (invite.isDirect) {
|
val titleResource = if (invite.isDm) {
|
||||||
R.string.screen_invites_decline_direct_chat_title
|
R.string.screen_invites_decline_direct_chat_title
|
||||||
} else {
|
} else {
|
||||||
R.string.screen_invites_decline_chat_title
|
R.string.screen_invites_decline_chat_title
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_invites_decline_chat_message">"Czy na pewno chcesz odrzucić zaproszenie do dołączenia do %1$s?"</string>
|
||||||
|
<string name="screen_invites_decline_chat_title">"Odrzuć zaproszenie"</string>
|
||||||
|
<string name="screen_invites_decline_direct_chat_message">"Czy na pewno chcesz odrzucić rozmowę prywatną z %1$s?"</string>
|
||||||
|
<string name="screen_invites_decline_direct_chat_title">"Odrzuć czat"</string>
|
||||||
|
<string name="screen_invites_empty_list">"Brak zaproszeń"</string>
|
||||||
|
<string name="screen_invites_invited_you">"%1$s (%2$s) zaprosił Cię"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_invites_decline_chat_message">"Tem certeza de que deseja recusar o convite para ingressar em %1$s?"</string>
|
||||||
|
<string name="screen_invites_decline_chat_title">"Recusar convite"</string>
|
||||||
|
<string name="screen_invites_decline_direct_chat_message">"Tem certeza de que deseja recusar esse chat privado com %1$s?"</string>
|
||||||
|
<string name="screen_invites_decline_direct_chat_title">"Recusar chat"</string>
|
||||||
|
<string name="screen_invites_empty_list">"Sem convites"</string>
|
||||||
|
<string name="screen_invites_invited_you">"%1$s(%2$s) convidou você"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -30,8 +30,8 @@ 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.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
|
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
|
||||||
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
||||||
import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager
|
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.lambda.assert
|
import io.element.android.tests.testutils.lambda.assert
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
|
@ -92,9 +92,9 @@ class AcceptDeclineInvitePresenterTest {
|
||||||
val client = FakeMatrixClient().apply {
|
val client = FakeMatrixClient().apply {
|
||||||
givenGetRoomResult(
|
givenGetRoomResult(
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
result = FakeMatrixRoom().apply {
|
result = FakeMatrixRoom(
|
||||||
leaveRoomLambda = declineInviteFailure
|
leaveRoomLambda = declineInviteFailure
|
||||||
}
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val presenter = createAcceptDeclineInvitePresenter(client = client)
|
val presenter = createAcceptDeclineInvitePresenter(client = client)
|
||||||
|
|
@ -133,7 +133,7 @@ class AcceptDeclineInvitePresenterTest {
|
||||||
val clearMembershipNotificationForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ ->
|
val clearMembershipNotificationForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ ->
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
}
|
}
|
||||||
val notificationDrawerManager = FakeNotificationDrawerManager(
|
val fakeNotificationCleaner = FakeNotificationCleaner(
|
||||||
clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda
|
clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda
|
||||||
)
|
)
|
||||||
val declineInviteSuccess = lambdaRecorder { ->
|
val declineInviteSuccess = lambdaRecorder { ->
|
||||||
|
|
@ -142,14 +142,14 @@ class AcceptDeclineInvitePresenterTest {
|
||||||
val client = FakeMatrixClient().apply {
|
val client = FakeMatrixClient().apply {
|
||||||
givenGetRoomResult(
|
givenGetRoomResult(
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
result = FakeMatrixRoom().apply {
|
result = FakeMatrixRoom(
|
||||||
leaveRoomLambda = declineInviteSuccess
|
leaveRoomLambda = declineInviteSuccess
|
||||||
}
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val presenter = createAcceptDeclineInvitePresenter(
|
val presenter = createAcceptDeclineInvitePresenter(
|
||||||
client = client,
|
client = client,
|
||||||
notificationDrawerManager = notificationDrawerManager,
|
notificationCleaner = fakeNotificationCleaner,
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val inviteData = anInviteData()
|
val inviteData = anInviteData()
|
||||||
|
|
@ -219,7 +219,7 @@ class AcceptDeclineInvitePresenterTest {
|
||||||
val clearMembershipNotificationForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ ->
|
val clearMembershipNotificationForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ ->
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
}
|
}
|
||||||
val notificationDrawerManager = FakeNotificationDrawerManager(
|
val fakeNotificationCleaner = FakeNotificationCleaner(
|
||||||
clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda
|
clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda
|
||||||
)
|
)
|
||||||
val joinRoomSuccess = lambdaRecorder { _: RoomId, _: List<String>, _: JoinedRoom.Trigger ->
|
val joinRoomSuccess = lambdaRecorder { _: RoomId, _: List<String>, _: JoinedRoom.Trigger ->
|
||||||
|
|
@ -227,7 +227,7 @@ class AcceptDeclineInvitePresenterTest {
|
||||||
}
|
}
|
||||||
val presenter = createAcceptDeclineInvitePresenter(
|
val presenter = createAcceptDeclineInvitePresenter(
|
||||||
joinRoomLambda = joinRoomSuccess,
|
joinRoomLambda = joinRoomSuccess,
|
||||||
notificationDrawerManager = notificationDrawerManager,
|
notificationCleaner = fakeNotificationCleaner,
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val inviteData = anInviteData()
|
val inviteData = anInviteData()
|
||||||
|
|
@ -260,12 +260,12 @@ class AcceptDeclineInvitePresenterTest {
|
||||||
private fun anInviteData(
|
private fun anInviteData(
|
||||||
roomId: RoomId = A_ROOM_ID,
|
roomId: RoomId = A_ROOM_ID,
|
||||||
name: String = A_ROOM_NAME,
|
name: String = A_ROOM_NAME,
|
||||||
isDirect: Boolean = false
|
isDm: Boolean = false
|
||||||
): InviteData {
|
): InviteData {
|
||||||
return InviteData(
|
return InviteData(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
roomName = name,
|
roomName = name,
|
||||||
isDirect = isDirect
|
isDm = isDm
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -274,12 +274,12 @@ class AcceptDeclineInvitePresenterTest {
|
||||||
joinRoomLambda: (RoomId, List<String>, JoinedRoom.Trigger) -> Result<Unit> = { _, _, _ ->
|
joinRoomLambda: (RoomId, List<String>, JoinedRoom.Trigger) -> Result<Unit> = { _, _, _ ->
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
},
|
},
|
||||||
notificationDrawerManager: NotificationDrawerManager = FakeNotificationDrawerManager(),
|
notificationCleaner: NotificationCleaner = FakeNotificationCleaner(),
|
||||||
): AcceptDeclineInvitePresenter {
|
): AcceptDeclineInvitePresenter {
|
||||||
return AcceptDeclineInvitePresenter(
|
return AcceptDeclineInvitePresenter(
|
||||||
client = client,
|
client = client,
|
||||||
joinRoom = FakeJoinRoom(joinRoomLambda),
|
joinRoom = FakeJoinRoom(joinRoomLambda),
|
||||||
notificationDrawerManager = notificationDrawerManager,
|
notificationCleaner = notificationCleaner,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
||||||
import io.element.android.libraries.matrix.api.room.RoomType
|
import io.element.android.libraries.matrix.api.room.RoomType
|
||||||
|
import io.element.android.libraries.matrix.api.room.isDm
|
||||||
import io.element.android.libraries.matrix.api.room.join.JoinRoom
|
import io.element.android.libraries.matrix.api.room.join.JoinRoom
|
||||||
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
|
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
|
||||||
import io.element.android.libraries.matrix.ui.model.toInviteSender
|
import io.element.android.libraries.matrix.ui.model.toInviteSender
|
||||||
|
|
@ -173,7 +174,7 @@ private fun RoomPreview.toContentState(): ContentState {
|
||||||
topic = topic,
|
topic = topic,
|
||||||
alias = canonicalAlias,
|
alias = canonicalAlias,
|
||||||
numberOfMembers = numberOfJoinedMembers,
|
numberOfMembers = numberOfJoinedMembers,
|
||||||
isDirect = false,
|
isDm = false,
|
||||||
roomType = roomType,
|
roomType = roomType,
|
||||||
roomAvatarUrl = avatarUrl,
|
roomAvatarUrl = avatarUrl,
|
||||||
joinAuthorisationStatus = when {
|
joinAuthorisationStatus = when {
|
||||||
|
|
@ -194,7 +195,7 @@ internal fun RoomDescription.toContentState(): ContentState {
|
||||||
topic = topic,
|
topic = topic,
|
||||||
alias = alias,
|
alias = alias,
|
||||||
numberOfMembers = numberOfMembers,
|
numberOfMembers = numberOfMembers,
|
||||||
isDirect = false,
|
isDm = false,
|
||||||
roomType = RoomType.Room,
|
roomType = RoomType.Room,
|
||||||
roomAvatarUrl = avatarUrl,
|
roomAvatarUrl = avatarUrl,
|
||||||
joinAuthorisationStatus = when (joinRule) {
|
joinAuthorisationStatus = when (joinRule) {
|
||||||
|
|
@ -213,7 +214,7 @@ internal fun MatrixRoomInfo.toContentState(): ContentState {
|
||||||
topic = topic,
|
topic = topic,
|
||||||
alias = canonicalAlias,
|
alias = canonicalAlias,
|
||||||
numberOfMembers = activeMembersCount,
|
numberOfMembers = activeMembersCount,
|
||||||
isDirect = isDirect,
|
isDm = isDm,
|
||||||
roomType = if (isSpace) RoomType.Space else RoomType.Room,
|
roomType = if (isSpace) RoomType.Space else RoomType.Room,
|
||||||
roomAvatarUrl = avatarUrl,
|
roomAvatarUrl = avatarUrl,
|
||||||
joinAuthorisationStatus = when {
|
joinAuthorisationStatus = when {
|
||||||
|
|
@ -233,7 +234,7 @@ internal fun ContentState.toInviteData(): InviteData? {
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
// Note: name should not be null at this point, but use Id just in case...
|
// Note: name should not be null at this point, but use Id just in case...
|
||||||
roomName = name ?: roomId.value,
|
roomName = name ?: roomId.value,
|
||||||
isDirect = isDirect
|
isDm = isDm
|
||||||
)
|
)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ sealed interface ContentState {
|
||||||
val topic: String?,
|
val topic: String?,
|
||||||
val alias: RoomAlias?,
|
val alias: RoomAlias?,
|
||||||
val numberOfMembers: Long?,
|
val numberOfMembers: Long?,
|
||||||
val isDirect: Boolean,
|
val isDm: Boolean,
|
||||||
val roomType: RoomType,
|
val roomType: RoomType,
|
||||||
val roomAvatarUrl: String?,
|
val roomAvatarUrl: String?,
|
||||||
val joinAuthorisationStatus: JoinAuthorisationStatus,
|
val joinAuthorisationStatus: JoinAuthorisationStatus,
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||||
import io.element.android.libraries.matrix.api.room.RoomType
|
import io.element.android.libraries.matrix.api.room.RoomType
|
||||||
|
import io.element.android.libraries.matrix.api.room.isDm
|
||||||
import io.element.android.libraries.matrix.ui.model.InviteSender
|
import io.element.android.libraries.matrix.ui.model.InviteSender
|
||||||
|
|
||||||
open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
|
open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
|
||||||
|
|
@ -84,6 +85,12 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
|
||||||
roomType = RoomType.Space,
|
roomType = RoomType.Space,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aLoadedContentState(
|
||||||
|
name = "A DM",
|
||||||
|
isDm = true,
|
||||||
|
)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,7 +113,7 @@ fun aLoadedContentState(
|
||||||
alias: RoomAlias? = RoomAlias("#exa:matrix.org"),
|
alias: RoomAlias? = RoomAlias("#exa:matrix.org"),
|
||||||
topic: String? = "Element X is a secure, private and decentralized messenger.",
|
topic: String? = "Element X is a secure, private and decentralized messenger.",
|
||||||
numberOfMembers: Long? = null,
|
numberOfMembers: Long? = null,
|
||||||
isDirect: Boolean = false,
|
isDm: Boolean = false,
|
||||||
roomType: RoomType = RoomType.Room,
|
roomType: RoomType = RoomType.Room,
|
||||||
roomAvatarUrl: String? = null,
|
roomAvatarUrl: String? = null,
|
||||||
joinAuthorisationStatus: JoinAuthorisationStatus = JoinAuthorisationStatus.Unknown
|
joinAuthorisationStatus: JoinAuthorisationStatus = JoinAuthorisationStatus.Unknown
|
||||||
|
|
@ -116,7 +123,7 @@ fun aLoadedContentState(
|
||||||
alias = alias,
|
alias = alias,
|
||||||
topic = topic,
|
topic = topic,
|
||||||
numberOfMembers = numberOfMembers,
|
numberOfMembers = numberOfMembers,
|
||||||
isDirect = isDirect,
|
isDm = isDm,
|
||||||
roomType = roomType,
|
roomType = roomType,
|
||||||
roomAvatarUrl = roomAvatarUrl,
|
roomAvatarUrl = roomAvatarUrl,
|
||||||
joinAuthorisationStatus = joinAuthorisationStatus
|
joinAuthorisationStatus = joinAuthorisationStatus
|
||||||
|
|
|
||||||
|
|
@ -155,13 +155,13 @@ private fun JoinRoomFooter(
|
||||||
text = stringResource(CommonStrings.action_decline),
|
text = stringResource(CommonStrings.action_decline),
|
||||||
onClick = onDeclineInvite,
|
onClick = onDeclineInvite,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
size = ButtonSize.Large,
|
size = ButtonSize.LargeLowPadding,
|
||||||
)
|
)
|
||||||
Button(
|
Button(
|
||||||
text = stringResource(CommonStrings.action_accept),
|
text = stringResource(CommonStrings.action_accept),
|
||||||
onClick = onAcceptInvite,
|
onClick = onAcceptInvite,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
size = ButtonSize.Large,
|
size = ButtonSize.LargeLowPadding,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ class JoinRoomPresenterTest {
|
||||||
assertThat(contentState.topic).isEqualTo(roomInfo.topic)
|
assertThat(contentState.topic).isEqualTo(roomInfo.topic)
|
||||||
assertThat(contentState.alias).isEqualTo(roomInfo.canonicalAlias)
|
assertThat(contentState.alias).isEqualTo(roomInfo.canonicalAlias)
|
||||||
assertThat(contentState.numberOfMembers).isEqualTo(roomInfo.activeMembersCount)
|
assertThat(contentState.numberOfMembers).isEqualTo(roomInfo.activeMembersCount)
|
||||||
assertThat(contentState.isDirect).isEqualTo(roomInfo.isDirect)
|
assertThat(contentState.isDm).isEqualTo(roomInfo.isDirect)
|
||||||
assertThat(contentState.roomAvatarUrl).isEqualTo(roomInfo.avatarUrl)
|
assertThat(contentState.roomAvatarUrl).isEqualTo(roomInfo.avatarUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -283,7 +283,7 @@ class JoinRoomPresenterTest {
|
||||||
assertThat(contentState.topic).isEqualTo(roomDescription.topic)
|
assertThat(contentState.topic).isEqualTo(roomDescription.topic)
|
||||||
assertThat(contentState.alias).isEqualTo(roomDescription.alias)
|
assertThat(contentState.alias).isEqualTo(roomDescription.alias)
|
||||||
assertThat(contentState.numberOfMembers).isEqualTo(roomDescription.numberOfMembers)
|
assertThat(contentState.numberOfMembers).isEqualTo(roomDescription.numberOfMembers)
|
||||||
assertThat(contentState.isDirect).isFalse()
|
assertThat(contentState.isDm).isFalse()
|
||||||
assertThat(contentState.roomAvatarUrl).isEqualTo(roomDescription.avatarUrl)
|
assertThat(contentState.roomAvatarUrl).isEqualTo(roomDescription.avatarUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -398,7 +398,7 @@ class JoinRoomPresenterTest {
|
||||||
topic = "Room topic",
|
topic = "Room topic",
|
||||||
alias = RoomAlias("#alias:matrix.org"),
|
alias = RoomAlias("#alias:matrix.org"),
|
||||||
numberOfMembers = 2,
|
numberOfMembers = 2,
|
||||||
isDirect = false,
|
isDm = false,
|
||||||
roomType = RoomType.Room,
|
roomType = RoomType.Room,
|
||||||
roomAvatarUrl = "avatarUrl",
|
roomAvatarUrl = "avatarUrl",
|
||||||
joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin
|
joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="leave_conversation_alert_subtitle">"Czy na pewno chcesz opuścić tę konwersację? Konwersacja nie jest publiczna i nie będziesz mógł dołączyć ponownie bez zaproszenia."</string>
|
||||||
|
<string name="leave_room_alert_empty_subtitle">"Jesteś pewien, że chcesz opuścić ten pokój? Jesteś tu jedyną osobą. Jeśli wyjdziesz, nikt nie będzie mógł dołączyć, w tym Ty."</string>
|
||||||
|
<string name="leave_room_alert_private_subtitle">"Czy na pewno chcesz opuścić ten pokój? Ten pokój nie jest publiczny i nie będziesz mógł do niego wrócić bez zaproszenia."</string>
|
||||||
|
<string name="leave_room_alert_subtitle">"Jesteś pewien, że chcesz wyjść z tego pokoju?"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="leave_room_alert_empty_subtitle">"Tem certeza de que deseja sair desta sala? Você é a única pessoa aqui. Se você sair, ninguém poderá ingressar no futuro, inclusive você."</string>
|
||||||
|
<string name="leave_room_alert_private_subtitle">"Tem certeza de que deseja sair desta sala? Esta sala não é pública e você não poderá entrar novamente sem um convite."</string>
|
||||||
|
<string name="leave_room_alert_subtitle">"Tem certeza de que deseja sair da sala?"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -34,6 +34,7 @@ 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.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||||
|
import io.element.android.libraries.matrix.api.room.isDm
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ class DefaultLeaveRoomPresenterTest {
|
||||||
client = FakeMatrixClient().apply {
|
client = FakeMatrixClient().apply {
|
||||||
givenGetRoomResult(
|
givenGetRoomResult(
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
result = FakeMatrixRoom(activeMemberCount = 2, isDirect = true, isOneToOne = true),
|
result = FakeMatrixRoom(activeMemberCount = 2, isDirect = true),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -140,7 +140,9 @@ class DefaultLeaveRoomPresenterTest {
|
||||||
client = FakeMatrixClient().apply {
|
client = FakeMatrixClient().apply {
|
||||||
givenGetRoomResult(
|
givenGetRoomResult(
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
result = FakeMatrixRoom(),
|
result = FakeMatrixRoom(
|
||||||
|
leaveRoomLambda = { Result.success(Unit) }
|
||||||
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
roomMembershipObserver = roomMembershipObserver
|
roomMembershipObserver = roomMembershipObserver
|
||||||
|
|
@ -162,9 +164,9 @@ class DefaultLeaveRoomPresenterTest {
|
||||||
client = FakeMatrixClient().apply {
|
client = FakeMatrixClient().apply {
|
||||||
givenGetRoomResult(
|
givenGetRoomResult(
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
result = FakeMatrixRoom().apply {
|
result = FakeMatrixRoom(
|
||||||
this.leaveRoomLambda = { Result.failure(RuntimeException("Blimey!")) }
|
leaveRoomLambda = { Result.failure(RuntimeException("Blimey!")) }
|
||||||
},
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -186,7 +188,9 @@ class DefaultLeaveRoomPresenterTest {
|
||||||
client = FakeMatrixClient().apply {
|
client = FakeMatrixClient().apply {
|
||||||
givenGetRoomResult(
|
givenGetRoomResult(
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
result = FakeMatrixRoom(),
|
result = FakeMatrixRoom(
|
||||||
|
leaveRoomLambda = { Result.success(Unit) }
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -208,9 +212,9 @@ class DefaultLeaveRoomPresenterTest {
|
||||||
client = FakeMatrixClient().apply {
|
client = FakeMatrixClient().apply {
|
||||||
givenGetRoomResult(
|
givenGetRoomResult(
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
result = FakeMatrixRoom().apply {
|
result = FakeMatrixRoom(
|
||||||
this.leaveRoomLambda = { Result.failure(RuntimeException("Blimey!")) }
|
leaveRoomLambda = { Result.failure(RuntimeException("Blimey!")) }
|
||||||
},
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -29,13 +29,15 @@ import io.element.android.features.location.impl.common.permissions.PermissionsE
|
||||||
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
|
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
|
||||||
import io.element.android.features.location.impl.common.permissions.PermissionsState
|
import io.element.android.features.location.impl.common.permissions.PermissionsState
|
||||||
import io.element.android.features.messages.test.FakeMessageComposerContext
|
import io.element.android.features.messages.test.FakeMessageComposerContext
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||||
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.matrix.test.room.SendLocationInvocation
|
|
||||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import io.element.android.tests.testutils.lambda.value
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
|
@ -46,16 +48,18 @@ class SendLocationPresenterTest {
|
||||||
val warmUpRule = WarmUpRule()
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
private val fakePermissionsPresenter = FakePermissionsPresenter()
|
private val fakePermissionsPresenter = FakePermissionsPresenter()
|
||||||
private val fakeMatrixRoom = FakeMatrixRoom()
|
|
||||||
private val fakeAnalyticsService = FakeAnalyticsService()
|
private val fakeAnalyticsService = FakeAnalyticsService()
|
||||||
private val fakeMessageComposerContext = FakeMessageComposerContext()
|
private val fakeMessageComposerContext = FakeMessageComposerContext()
|
||||||
private val fakeLocationActions = FakeLocationActions()
|
private val fakeLocationActions = FakeLocationActions()
|
||||||
private val fakeBuildMeta = aBuildMeta(applicationName = "app name")
|
private val fakeBuildMeta = aBuildMeta(applicationName = "app name")
|
||||||
private val sendLocationPresenter: SendLocationPresenter = SendLocationPresenter(
|
|
||||||
|
private fun createSendLocationPresenter(
|
||||||
|
matrixRoom: MatrixRoom = FakeMatrixRoom(),
|
||||||
|
): SendLocationPresenter = SendLocationPresenter(
|
||||||
permissionsPresenterFactory = object : PermissionsPresenter.Factory {
|
permissionsPresenterFactory = object : PermissionsPresenter.Factory {
|
||||||
override fun create(permissions: List<String>): PermissionsPresenter = fakePermissionsPresenter
|
override fun create(permissions: List<String>): PermissionsPresenter = fakePermissionsPresenter
|
||||||
},
|
},
|
||||||
room = fakeMatrixRoom,
|
room = matrixRoom,
|
||||||
analyticsService = fakeAnalyticsService,
|
analyticsService = fakeAnalyticsService,
|
||||||
messageComposerContext = fakeMessageComposerContext,
|
messageComposerContext = fakeMessageComposerContext,
|
||||||
locationActions = fakeLocationActions,
|
locationActions = fakeLocationActions,
|
||||||
|
|
@ -64,6 +68,7 @@ class SendLocationPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `initial state with permissions granted`() = runTest {
|
fun `initial state with permissions granted`() = runTest {
|
||||||
|
val sendLocationPresenter = createSendLocationPresenter()
|
||||||
fakePermissionsPresenter.givenState(
|
fakePermissionsPresenter.givenState(
|
||||||
aPermissionsState(
|
aPermissionsState(
|
||||||
permissions = PermissionsState.Permissions.AllGranted,
|
permissions = PermissionsState.Permissions.AllGranted,
|
||||||
|
|
@ -90,6 +95,7 @@ class SendLocationPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `initial state with permissions partially granted`() = runTest {
|
fun `initial state with permissions partially granted`() = runTest {
|
||||||
|
val sendLocationPresenter = createSendLocationPresenter()
|
||||||
fakePermissionsPresenter.givenState(
|
fakePermissionsPresenter.givenState(
|
||||||
aPermissionsState(
|
aPermissionsState(
|
||||||
permissions = PermissionsState.Permissions.SomeGranted,
|
permissions = PermissionsState.Permissions.SomeGranted,
|
||||||
|
|
@ -116,6 +122,7 @@ class SendLocationPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `initial state with permissions denied`() = runTest {
|
fun `initial state with permissions denied`() = runTest {
|
||||||
|
val sendLocationPresenter = createSendLocationPresenter()
|
||||||
fakePermissionsPresenter.givenState(
|
fakePermissionsPresenter.givenState(
|
||||||
aPermissionsState(
|
aPermissionsState(
|
||||||
permissions = PermissionsState.Permissions.NoneGranted,
|
permissions = PermissionsState.Permissions.NoneGranted,
|
||||||
|
|
@ -142,6 +149,7 @@ class SendLocationPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `initial state with permissions denied once`() = runTest {
|
fun `initial state with permissions denied once`() = runTest {
|
||||||
|
val sendLocationPresenter = createSendLocationPresenter()
|
||||||
fakePermissionsPresenter.givenState(
|
fakePermissionsPresenter.givenState(
|
||||||
aPermissionsState(
|
aPermissionsState(
|
||||||
permissions = PermissionsState.Permissions.NoneGranted,
|
permissions = PermissionsState.Permissions.NoneGranted,
|
||||||
|
|
@ -168,6 +176,7 @@ class SendLocationPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `rationale dialog dismiss`() = runTest {
|
fun `rationale dialog dismiss`() = runTest {
|
||||||
|
val sendLocationPresenter = createSendLocationPresenter()
|
||||||
fakePermissionsPresenter.givenState(
|
fakePermissionsPresenter.givenState(
|
||||||
aPermissionsState(
|
aPermissionsState(
|
||||||
permissions = PermissionsState.Permissions.NoneGranted,
|
permissions = PermissionsState.Permissions.NoneGranted,
|
||||||
|
|
@ -199,6 +208,7 @@ class SendLocationPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `rationale dialog continue`() = runTest {
|
fun `rationale dialog continue`() = runTest {
|
||||||
|
val sendLocationPresenter = createSendLocationPresenter()
|
||||||
fakePermissionsPresenter.givenState(
|
fakePermissionsPresenter.givenState(
|
||||||
aPermissionsState(
|
aPermissionsState(
|
||||||
permissions = PermissionsState.Permissions.NoneGranted,
|
permissions = PermissionsState.Permissions.NoneGranted,
|
||||||
|
|
@ -227,6 +237,7 @@ class SendLocationPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `permission denied dialog dismiss`() = runTest {
|
fun `permission denied dialog dismiss`() = runTest {
|
||||||
|
val sendLocationPresenter = createSendLocationPresenter()
|
||||||
fakePermissionsPresenter.givenState(
|
fakePermissionsPresenter.givenState(
|
||||||
aPermissionsState(
|
aPermissionsState(
|
||||||
permissions = PermissionsState.Permissions.NoneGranted,
|
permissions = PermissionsState.Permissions.NoneGranted,
|
||||||
|
|
@ -258,6 +269,13 @@ class SendLocationPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `share sender location`() = runTest {
|
fun `share sender location`() = runTest {
|
||||||
|
val sendLocationResult = lambdaRecorder<String, String, String?, Int?, AssetType?, Result<Unit>> { _, _, _, _, _ ->
|
||||||
|
Result.success(Unit)
|
||||||
|
}
|
||||||
|
val matrixRoom = FakeMatrixRoom(
|
||||||
|
sendLocationResult = sendLocationResult,
|
||||||
|
)
|
||||||
|
val sendLocationPresenter = createSendLocationPresenter(matrixRoom)
|
||||||
fakePermissionsPresenter.givenState(
|
fakePermissionsPresenter.givenState(
|
||||||
aPermissionsState(
|
aPermissionsState(
|
||||||
permissions = PermissionsState.Permissions.AllGranted,
|
permissions = PermissionsState.Permissions.AllGranted,
|
||||||
|
|
@ -289,16 +307,14 @@ class SendLocationPresenterTest {
|
||||||
|
|
||||||
delay(1) // Wait for the coroutine to finish
|
delay(1) // Wait for the coroutine to finish
|
||||||
|
|
||||||
assertThat(fakeMatrixRoom.sentLocations.size).isEqualTo(1)
|
sendLocationResult.assertions().isCalledOnce()
|
||||||
assertThat(fakeMatrixRoom.sentLocations.last()).isEqualTo(
|
.with(
|
||||||
SendLocationInvocation(
|
value("Location was shared at geo:3.0,4.0;u=5.0"),
|
||||||
body = "Location was shared at geo:3.0,4.0;u=5.0",
|
value("geo:3.0,4.0;u=5.0"),
|
||||||
geoUri = "geo:3.0,4.0;u=5.0",
|
value(null),
|
||||||
description = null,
|
value(15),
|
||||||
zoomLevel = 15,
|
value(AssetType.SENDER),
|
||||||
assetType = AssetType.SENDER
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
assertThat(fakeAnalyticsService.capturedEvents.size).isEqualTo(1)
|
assertThat(fakeAnalyticsService.capturedEvents.size).isEqualTo(1)
|
||||||
assertThat(fakeAnalyticsService.capturedEvents.last()).isEqualTo(
|
assertThat(fakeAnalyticsService.capturedEvents.last()).isEqualTo(
|
||||||
|
|
@ -314,6 +330,13 @@ class SendLocationPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `share pin location`() = runTest {
|
fun `share pin location`() = runTest {
|
||||||
|
val sendLocationResult = lambdaRecorder<String, String, String?, Int?, AssetType?, Result<Unit>> { _, _, _, _, _ ->
|
||||||
|
Result.success(Unit)
|
||||||
|
}
|
||||||
|
val matrixRoom = FakeMatrixRoom(
|
||||||
|
sendLocationResult = sendLocationResult,
|
||||||
|
)
|
||||||
|
val sendLocationPresenter = createSendLocationPresenter(matrixRoom)
|
||||||
fakePermissionsPresenter.givenState(
|
fakePermissionsPresenter.givenState(
|
||||||
aPermissionsState(
|
aPermissionsState(
|
||||||
permissions = PermissionsState.Permissions.NoneGranted,
|
permissions = PermissionsState.Permissions.NoneGranted,
|
||||||
|
|
@ -345,16 +368,14 @@ class SendLocationPresenterTest {
|
||||||
|
|
||||||
delay(1) // Wait for the coroutine to finish
|
delay(1) // Wait for the coroutine to finish
|
||||||
|
|
||||||
assertThat(fakeMatrixRoom.sentLocations.size).isEqualTo(1)
|
sendLocationResult.assertions().isCalledOnce()
|
||||||
assertThat(fakeMatrixRoom.sentLocations.last()).isEqualTo(
|
.with(
|
||||||
SendLocationInvocation(
|
value("Location was shared at geo:0.0,1.0"),
|
||||||
body = "Location was shared at geo:0.0,1.0",
|
value("geo:0.0,1.0"),
|
||||||
geoUri = "geo:0.0,1.0",
|
value(null),
|
||||||
description = null,
|
value(15),
|
||||||
zoomLevel = 15,
|
value(AssetType.PIN),
|
||||||
assetType = AssetType.PIN
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
assertThat(fakeAnalyticsService.capturedEvents.size).isEqualTo(1)
|
assertThat(fakeAnalyticsService.capturedEvents.size).isEqualTo(1)
|
||||||
assertThat(fakeAnalyticsService.capturedEvents.last()).isEqualTo(
|
assertThat(fakeAnalyticsService.capturedEvents.last()).isEqualTo(
|
||||||
|
|
@ -370,6 +391,13 @@ class SendLocationPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `composer context passes through analytics`() = runTest {
|
fun `composer context passes through analytics`() = runTest {
|
||||||
|
val sendLocationResult = lambdaRecorder<String, String, String?, Int?, AssetType?, Result<Unit>> { _, _, _, _, _ ->
|
||||||
|
Result.success(Unit)
|
||||||
|
}
|
||||||
|
val matrixRoom = FakeMatrixRoom(
|
||||||
|
sendLocationResult = sendLocationResult,
|
||||||
|
)
|
||||||
|
val sendLocationPresenter = createSendLocationPresenter(matrixRoom)
|
||||||
fakePermissionsPresenter.givenState(
|
fakePermissionsPresenter.givenState(
|
||||||
aPermissionsState(
|
aPermissionsState(
|
||||||
permissions = PermissionsState.Permissions.NoneGranted,
|
permissions = PermissionsState.Permissions.NoneGranted,
|
||||||
|
|
@ -418,6 +446,7 @@ class SendLocationPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `open settings activity`() = runTest {
|
fun `open settings activity`() = runTest {
|
||||||
|
val sendLocationPresenter = createSendLocationPresenter()
|
||||||
fakePermissionsPresenter.givenState(
|
fakePermissionsPresenter.givenState(
|
||||||
aPermissionsState(
|
aPermissionsState(
|
||||||
permissions = PermissionsState.Permissions.NoneGranted,
|
permissions = PermissionsState.Permissions.NoneGranted,
|
||||||
|
|
@ -452,6 +481,7 @@ class SendLocationPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `application name is in state`() = runTest {
|
fun `application name is in state`() = runTest {
|
||||||
|
val sendLocationPresenter = createSendLocationPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
sendLocationPresenter.present()
|
sendLocationPresenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ dependencies {
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.featureflag.api)
|
implementation(projects.libraries.featureflag.api)
|
||||||
implementation(projects.libraries.cryptography.api)
|
implementation(projects.libraries.cryptography.api)
|
||||||
|
implementation(projects.libraries.preferences.api)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.libraries.sessionStorage.api)
|
implementation(projects.libraries.sessionStorage.api)
|
||||||
implementation(projects.services.appnavstate.api)
|
implementation(projects.services.appnavstate.api)
|
||||||
|
|
|
||||||