Merge tag 'v1.4.32' into sc

Change-Id: Ib3724625b1aada2541e030f9fdaf47a032538c85

Conflicts:
	.gitignore
	library/ui-styles/src/main/res/values/styles_toolbar.xml
	vector/build.gradle
	vector/src/main/java/im/vector/app/SpaceStateHandlerImpl.kt
	vector/src/main/java/im/vector/app/VectorApplication.kt
	vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
	vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt
	vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt
	vector/src/main/java/im/vector/app/features/home/UnreadMessagesSharedViewModel.kt
	vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
	vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
	vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilder.kt
	vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt
	vector/src/main/java/im/vector/app/features/login/LoginSplashFragment.kt
	vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt
	vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt
	vector/src/main/java/im/vector/app/features/settings/VectorLocale.kt
	vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
	vector/src/main/res/layout/fragment_timeline.xml
This commit is contained in:
SpiritCroc 2022-08-10 16:02:12 +02:00
commit 5d1722054e
256 changed files with 4469 additions and 1482 deletions

View File

@ -7,10 +7,8 @@ on:
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: env:
CI_GRADLE_ARG_PROPERTIES: > GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options="-Xmx2560m" -Dkotlin.incremental=false
-Porg.gradle.jvmargs=-Xmx4g CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon
-Porg.gradle.parallel=false
--no-daemon
jobs: jobs:
debug: debug:
@ -36,7 +34,7 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-gradle- ${{ runner.os }}-gradle-
- name: Assemble ${{ matrix.target }} debug apk - name: Assemble ${{ matrix.target }} debug apk
run: ./gradlew assemble${{ matrix.target }}Debug $CI_GRADLE_ARG_PROPERTIES --stacktrace run: ./gradlew assemble${{ matrix.target }}Debug $CI_GRADLE_ARG_PROPERTIES
- name: Upload ${{ matrix.target }} debug APKs - name: Upload ${{ matrix.target }} debug APKs
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
@ -61,7 +59,7 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-gradle- ${{ runner.os }}-gradle-
- name: Assemble GPlay unsigned apk - name: Assemble GPlay unsigned apk
run: ./gradlew clean assembleGplayRelease $CI_GRADLE_ARG_PROPERTIES --stacktrace run: ./gradlew clean assembleGplayRelease $CI_GRADLE_ARG_PROPERTIES
- name: Upload Gplay unsigned APKs - name: Upload Gplay unsigned APKs
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:

18
.github/workflows/danger.yml vendored Normal file
View File

@ -0,0 +1,18 @@
name: Danger CI
on: [pull_request]
jobs:
build:
runs-on: ubuntu-latest
name: Danger
steps:
- uses: actions/checkout@v3
- run: |
npm install --save-dev @babel/plugin-transform-flow-strip-types
- name: Danger
uses: danger/danger-js@11.1.1
with:
args: "--dangerfile tools/danger/dangerfile.js"
env:
DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }}

View File

@ -1,5 +1,8 @@
name: "Validate Gradle Wrapper" name: "Validate Gradle Wrapper"
on: [push, pull_request] on:
pull_request: { }
push:
branches: [ main, develop ]
jobs: jobs:
validation: validation:

View File

@ -6,10 +6,8 @@ on:
- cron: "0 4 * * *" - cron: "0 4 * * *"
env: env:
CI_GRADLE_ARG_PROPERTIES: > GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options="-Xmx2560m" -Dkotlin.incremental=false
-Porg.gradle.jvmargs=-Xmx4g CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon
-Porg.gradle.parallel=false
--no-daemon
jobs: jobs:
nightly: nightly:
@ -40,7 +38,7 @@ jobs:
yes n | towncrier build --version nightly yes n | towncrier build --version nightly
- name: Build and upload Gplay Nightly APK - name: Build and upload Gplay Nightly APK
run: | run: |
./gradlew assembleGplayNightly appDistributionUploadGplayNightly $CI_GRADLE_ARG_PROPERTIES --stacktrace ./gradlew assembleGplayNightly appDistributionUploadGplayNightly $CI_GRADLE_ARG_PROPERTIES
env: env:
ELEMENT_ANDROID_NIGHTLY_KEYID: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYID }} ELEMENT_ANDROID_NIGHTLY_KEYID: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYID }}
ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD }} ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD }}

View File

@ -10,10 +10,8 @@ on:
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: env:
CI_GRADLE_ARG_PROPERTIES: > GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options="-Xmx2560m" -Dkotlin.incremental=false
-Porg.gradle.jvmargs=-Xmx4g CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon
-Porg.gradle.parallel=false
--no-daemon
jobs: jobs:

View File

@ -7,10 +7,8 @@ on:
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: env:
CI_GRADLE_ARG_PROPERTIES: > GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options="-Xmx2560m" -Dkotlin.incremental=false
-Porg.gradle.jvmargs=-Xmx4g CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon
-Porg.gradle.parallel=false
--no-daemon
jobs: jobs:
check: check:
@ -29,80 +27,50 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Run knit - name: Run knit
run: | run: |
./gradlew knit ./gradlew knitCheck
# ktlint for all the modules # Check the project: ktlint, detekt, lint
ktlint: lint:
name: Kotlin Linter name: Android Linter
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Allow all jobs on main and develop. Just one per PR. # Allow all jobs on main and develop. Just one per PR.
concurrency: concurrency:
group: ${{ github.ref == 'refs/heads/main' && format('ktlint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('ktlint-develop-{0}', github.sha) || format('ktlint-{0}', github.ref) }} group: ${{ github.ref == 'refs/heads/main' && format('lint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('lint-develop-{0}', github.sha) || format('lint-{0}', github.ref) }}
cancel-in-progress: true cancel-in-progress: true
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Run ktlint - name: Run ktlint
run: | run: |
./gradlew ktlintCheck --continue ./gradlew ktlintCheck --continue
- name: Run detekt
if: always()
run: |
./gradlew detekt $CI_GRADLE_ARG_PROPERTIES
- name: Run lint
# Not always, if ktlint or detekt fail, avoid running the long lint check.
run: |
./gradlew lintGplayRelease $CI_GRADLE_ARG_PROPERTIES
./gradlew lintFdroidRelease $CI_GRADLE_ARG_PROPERTIES
- name: Upload reports - name: Upload reports
if: always() if: always()
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: ktlinting-report name: linting-report
path: | path: |
*/build/reports/ktlint/ktlint*/ktlint*.txt */build/reports/**/*.*
- name: Handle Results - name: Prepare Danger
if: always() if: always()
id: ktlint-results
run: | run: |
results="$(cat */*/build/reports/ktlint/ktlint*/ktlint*.txt */build/reports/ktlint/ktlint*/ktlint*.txt | sed -r "s/\x1B\[([0-9]{1,3}(;[0-9]{1,2})?)?[mGK]//g")" npm install --save-dev @babel/core
if [ -z "$results" ]; then npm install --save-dev @babel/plugin-transform-flow-strip-types
echo "::set-output name=add_comment::false" yarn add danger-plugin-lint-report --dev
else - name: Danger lint
body="👎\`Failed${results}\`" if: always()
body="${body//'%'/'%25'}" uses: danger/danger-js@11.1.1
body="${body//$'\n'/'%0A'}"
body="${body//$'\r'/'%0D'}"
body="$( echo $body | sed 's/\/home\/runner\/work\/element-android\/element-android\//\`<br\/>\`/g')"
body="$( echo $body | sed 's/\/src\/main\/java\// 🔸 /g')"
body="$( echo $body | sed 's/im\/vector\/app\///g')"
body="$( echo $body | sed 's/im\/vector\/lib\/attachmentviewer\///g')"
body="$( echo $body | sed 's/im\/vector\/lib\/multipicker\///g')"
body="$( echo $body | sed 's/im\/vector\/lib\///g')"
body="$( echo $body | sed 's/org\/matrix\/android\/sdk\///g')"
body="$( echo $body | sed 's/\/src\/androidTest\/java\// 🔸 /g')"
echo "::set-output name=add_comment::true"
echo "::set-output name=body::$body"
fi
- name: Find Comment
if: always() && github.event_name == 'pull_request'
uses: peter-evans/find-comment@v2
id: fc
with: with:
issue-number: ${{ github.event.pull_request.number }} args: "--dangerfile tools/danger/dangerfile-lint.js"
comment-author: 'github-actions[bot]' env:
body-includes: Ktlint Results DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }}
- name: Add comment if needed
if: always() && github.event_name == 'pull_request' && steps.ktlint-results.outputs.add_comment == 'true'
uses: peter-evans/create-or-update-comment@v2
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body: |
### Ktlint Results
${{ steps.ktlint-results.outputs.body }}
edit-mode: replace
- name: Delete comment if needed
if: always() && github.event_name == 'pull_request' && steps.fc.outputs.comment-id != '' && steps.ktlint-results.outputs.add_comment == 'false'
uses: actions/github-script@v3
with:
script: |
github.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: ${{ steps.fc.outputs.comment-id }}
})
# Gradle dependency analysis using https://github.com/autonomousapps/dependency-analysis-android-gradle-plugin # Gradle dependency analysis using https://github.com/autonomousapps/dependency-analysis-android-gradle-plugin
dependency-analysis: dependency-analysis:
@ -122,107 +90,3 @@ jobs:
with: with:
name: dependency-analysis name: dependency-analysis
path: build/reports/dependency-check-report.html path: build/reports/dependency-check-report.html
# Lint for main module
android-lint:
name: Android Linter
runs-on: ubuntu-latest
# Allow all jobs on main and develop. Just one per PR.
concurrency:
group: ${{ github.ref == 'refs/heads/main' && format('android-lint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('android-lint-develop-{0}', github.sha) || format('android-lint-{0}', github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v3
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Lint analysis
run: ./gradlew clean :vector:lint --stacktrace $CI_GRADLE_ARG_PROPERTIES
- name: Upload reports
if: always()
uses: actions/upload-artifact@v3
with:
name: lint-report
path: |
vector/build/reports/*.*
# Lint for Gplay and Fdroid release APK
apk-lint:
name: Lint APK (${{ matrix.target }})
runs-on: ubuntu-latest
if: github.ref != 'refs/heads/main'
strategy:
fail-fast: false
matrix:
target: [ Gplay, Fdroid ]
# Allow all jobs on develop. Just one per PR.
concurrency:
group: ${{ github.ref == 'refs/heads/develop' && format('apk-lint-develop-{0}-{1}', matrix.target, github.sha) || format('apk-lint-{0}-{1}', matrix.target, github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v3
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Lint ${{ matrix.target }} release
run: ./gradlew clean lint${{ matrix.target }}Release --stacktrace $CI_GRADLE_ARG_PROPERTIES
- name: Upload ${{ matrix.target }} linting report
if: always()
uses: actions/upload-artifact@v3
with:
name: release-lint-report-${{ matrix.target }}
path: |
vector/build/reports/*.*
detekt:
name: Detekt Analysis
runs-on: ubuntu-latest
# Allow all jobs on main and develop. Just one per PR.
concurrency:
group: ${{ github.ref == 'refs/heads/main' && format('detekt-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('detekt-develop-{0}', github.sha) || format('detekt-{0}', github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v3
- name: Run detekt
run: |
./gradlew detekt $CI_GRADLE_ARG_PROPERTIES
- name: Upload reports
if: always()
uses: actions/upload-artifact@v3
with:
name: detekt-report
path: |
*/build/reports/detekt/detekt.html
# towncrier:
# name: Towncrier check
# runs-on: ubuntu-latest
# if: github.event_name == 'pull_request' && github.head_ref == 'develop'
# steps:
# - uses: actions/checkout@v3
# - name: Set up Python 3.8
# uses: actions/setup-python@v4
# with:
# python-version: 3.8
# - name: Install towncrier
# run: |
# python3 -m pip install towncrier
# - name: Run towncrier
# # Fetch the pull request' base branch so towncrier will be able to
# # compare the current branch with the base branch.
# # Source: https://github.com/actions/checkout/#fetch-all-branches.
# run: |
# git fetch --no-tags origin +refs/heads/${BASE_BRANCH}:refs/remotes/origin/${BASE_BRANCH}
# towncrier check --compare-with origin/${BASE_BRANCH}
# env:
# BASE_BRANCH: ${{ github.base_ref }}

View File

@ -7,10 +7,8 @@ on:
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: env:
CI_GRADLE_ARG_PROPERTIES: > GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options="-Xmx2560m" -Dkotlin.incremental=false
-Porg.gradle.jvmargs=-Xmx4g CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon
-Porg.gradle.parallel=false
--no-daemon
jobs: jobs:
tests: tests:
@ -51,9 +49,9 @@ jobs:
disable-animations: true disable-animations: true
emulator-build: 7425822 emulator-build: 7425822
script: | script: |
./gradlew unitTestsWithCoverage --stacktrace $CI_GRADLE_ARG_PROPERTIES ./gradlew unitTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES
./gradlew instrumentationTestsWithCoverage --stacktrace $CI_GRADLE_ARG_PROPERTIES ./gradlew instrumentationTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES
./gradlew generateCoverageReport --stacktrace $CI_GRADLE_ARG_PROPERTIES ./gradlew generateCoverageReport $CI_GRADLE_ARG_PROPERTIES
# NB: continue-on-error marks steps.tests.conclusion = 'success' but leaves stes.tests.outcome = 'failure' # NB: continue-on-error marks steps.tests.conclusion = 'success' but leaves stes.tests.outcome = 'failure'
- name: Run all the codecoverage tests at once (retry if emulator failed) - name: Run all the codecoverage tests at once (retry if emulator failed)
uses: reactivecircus/android-emulator-runner@v2 uses: reactivecircus/android-emulator-runner@v2
@ -67,13 +65,13 @@ jobs:
disable-animations: true disable-animations: true
emulator-build: 7425822 emulator-build: 7425822
script: | script: |
./gradlew unitTestsWithCoverage --stacktrace $CI_GRADLE_ARG_PROPERTIES ./gradlew unitTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES
./gradlew instrumentationTestsWithCoverage --stacktrace $CI_GRADLE_ARG_PROPERTIES ./gradlew instrumentationTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES
./gradlew generateCoverageReport --stacktrace $CI_GRADLE_ARG_PROPERTIES ./gradlew generateCoverageReport $CI_GRADLE_ARG_PROPERTIES
- run: ./gradlew sonarqube $CI_GRADLE_ARG_PROPERTIES - run: ./gradlew sonarqube $CI_GRADLE_ARG_PROPERTIES
if: always() # we may have failed a previous step and retried, that's OK if: always() # we may have failed a previous step and retried, that's OK
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any GITHUB_TOKEN: ${{ secrets.SONARQUBE_GITHUB_API_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }} ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }}
@ -114,5 +112,5 @@ jobs:
# restore-keys: | # restore-keys: |
# ${{ runner.os }}-gradle- # ${{ runner.os }}-gradle-
# - name: Build Android Tests # - name: Build Android Tests
# run: ./gradlew clean assembleAndroidTest $CI_GRADLE_ARG_PROPERTIES --stacktrace # run: ./gradlew clean assembleAndroidTest $CI_GRADLE_ARG_PROPERTIES

6
.gitignore vendored
View File

@ -19,5 +19,11 @@
/**/build /**/build
# Added by SC
.tmp_unpatched_strings .tmp_unpatched_strings
.tmp_new_patched_strings .tmp_new_patched_strings
# Added by yarn
/package.json
/yarn.lock
/node_modules

View File

@ -1,18 +0,0 @@
# FTR: Configuration on https://travis-ci.org/github/vector-im/element-android/settings
#
# - Build only if .travis.yml is present -> On
# - Limit concurrent jobs -> Off
# - Build pushed branches -> On (build the branch)
# - Build pushed pull request -> On (build the PR after auto-merge)
#
# - Auto cancel branch builds -> On
# - Auto cancel pull request builds -> On
sudo: false
notifications:
email: false
# Just run a simple script here
script:
- ./tools/travis/check_pr.sh

View File

@ -1,3 +1,38 @@
Changes in Element v1.4.32 (2022-08-10)
=======================================
Features ✨
----------
- [Location Share] Render fallback UI when map fails to load ([#6711](https://github.com/vector-im/element-android/issues/6711))
Bugfixes 🐛
----------
- Fix message content sometimes appearing in the log ([#6706](https://github.com/vector-im/element-android/issues/6706))
- Disable 'Enable biometrics' option if there are not biometric authenticators enrolled. ([#6713](https://github.com/vector-im/element-android/issues/6713))
- Fix crash when biometric key is used when coming back to foreground and KeyStore reports that the device is still locked. ([#6768](https://github.com/vector-im/element-android/issues/6768))
- Catch all exceptions on lockscreen system key migrations. ([#6769](https://github.com/vector-im/element-android/issues/6769))
- Fixes crash when entering non ascii characters during account creation ([#6735](https://github.com/vector-im/element-android/issues/6735))
- Fixes onboarding login/account creation errors showing after navigation ([#6737](https://github.com/vector-im/element-android/issues/6737))
- [Location sharing] Invisible text on map symbol ([#6687](https://github.com/vector-im/element-android/issues/6687))
In development 🚧
----------------
- Adds new app layout toolbar ([#6655](https://github.com/vector-im/element-android/issues/6655))
Other changes
-------------
- [Modularization] Provides abstraction to avoid direct usages of BuildConfig ([#6406](https://github.com/vector-im/element-android/issues/6406))
- Refactors SpaceStateHandler (previously AppStateHandler) and adds unit tests for it ([#6598](https://github.com/vector-im/element-android/issues/6598))
- Setup Danger to the project ([#6637](https://github.com/vector-im/element-android/issues/6637))
- [Location Share] Open maximized map on tapping on live sharing notification ([#6642](https://github.com/vector-im/element-android/issues/6642))
- [Location sharing] Align naming of components for live location feature ([#6647](https://github.com/vector-im/element-android/issues/6647))
- [Location share] Update minimum sending period to 5 seconds for a live ([#6653](https://github.com/vector-im/element-android/issues/6653))
- [Location sharing] - Fix the memory leaks ([#6674](https://github.com/vector-im/element-android/issues/6674))
- [Timeline] Memory leak in audio message playback tracker ([#6678](https://github.com/vector-im/element-android/issues/6678))
- [FTUE] Memory leak on FtueAuthSplashCarouselFragment ([#6680](https://github.com/vector-im/element-android/issues/6680))
- Link directly to DCO docs from danger message. ([#6739](https://github.com/vector-im/element-android/issues/6739))
Changes in Element v1.4.31 (2022-08-01) Changes in Element v1.4.31 (2022-08-01)
======================================= =======================================

View File

@ -1,3 +1,4 @@
source "https://rubygems.org" source "https://rubygems.org"
gem "fastlane" gem "fastlane"
gem 'danger'

View File

@ -24,10 +24,29 @@ GEM
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4) babosa (1.0.4)
claide (1.0.3) claide (1.0.3)
claide-plugins (0.9.2)
cork
nap
open4 (~> 1.3)
colored (1.2) colored (1.2)
colored2 (3.1.2) colored2 (3.1.2)
commander (4.6.0) commander (4.6.0)
highline (~> 2.0.0) highline (~> 2.0.0)
cork (0.3.0)
colored2 (~> 3.1)
danger (8.6.1)
claide (~> 1.0)
claide-plugins (>= 0.9.2)
colored2 (~> 3.1)
cork (~> 0.1)
faraday (>= 0.9.0, < 2.0)
faraday-http-cache (~> 2.0)
git (~> 1.7)
kramdown (~> 2.3)
kramdown-parser-gfm (~> 1.0)
no_proxy_fix
octokit (~> 4.7)
terminal-table (>= 1, < 4)
declarative (0.0.20) declarative (0.0.20)
digest-crc (0.6.3) digest-crc (0.6.3)
rake (>= 12.0.0, < 14.0.0) rake (>= 12.0.0, < 14.0.0)
@ -52,6 +71,8 @@ GEM
faraday-em_http (1.0.0) faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0) faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0) faraday-excon (1.1.0)
faraday-http-cache (2.4.0)
faraday (>= 0.8)
faraday-httpclient (1.0.1) faraday-httpclient (1.0.1)
faraday-net_http (1.0.1) faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0) faraday-net_http_persistent (1.2.0)
@ -98,6 +119,8 @@ GEM
xcpretty (~> 0.3.0) xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3) xcpretty-travis-formatter (>= 0.0.3)
gh_inspector (1.1.3) gh_inspector (1.1.3)
git (1.11.0)
rchardet (~> 1.8)
google-apis-androidpublisher_v3 (0.8.0) google-apis-androidpublisher_v3 (0.8.0)
google-apis-core (>= 0.4, < 2.a) google-apis-core (>= 0.4, < 2.a)
google-apis-core (0.4.0) google-apis-core (0.4.0)
@ -143,17 +166,28 @@ GEM
jmespath (1.4.0) jmespath (1.4.0)
json (2.5.1) json (2.5.1)
jwt (2.2.3) jwt (2.2.3)
kramdown (2.4.0)
rexml
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
memoist (0.16.2) memoist (0.16.2)
mini_magick (4.11.0) mini_magick (4.11.0)
mini_mime (1.1.0) mini_mime (1.1.0)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.0.0) multipart-post (2.0.0)
nanaimo (0.3.0) nanaimo (0.3.0)
nap (1.1.0)
naturally (2.2.1) naturally (2.2.1)
no_proxy_fix (0.1.2)
octokit (4.25.1)
faraday (>= 1, < 3)
sawyer (~> 0.9)
open4 (1.3.4)
os (1.1.1) os (1.1.1)
plist (3.6.0) plist (3.6.0)
public_suffix (4.0.6) public_suffix (4.0.6)
rake (13.0.6) rake (13.0.6)
rchardet (1.8.0)
representable (3.1.1) representable (3.1.1)
declarative (< 0.1.0) declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0) trailblazer-option (>= 0.1.1, < 0.2.0)
@ -163,6 +197,9 @@ GEM
rouge (2.0.7) rouge (2.0.7)
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rubyzip (2.3.2) rubyzip (2.3.2)
sawyer (0.9.2)
addressable (>= 2.3.5)
faraday (>= 0.17.3, < 3)
security (0.1.3) security (0.1.3)
signet (0.15.0) signet (0.15.0)
addressable (~> 2.3) addressable (~> 2.3)
@ -200,9 +237,11 @@ GEM
xcpretty (~> 0.2, >= 0.0.7) xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS PLATFORMS
universal-darwin-21
x86_64-darwin-20 x86_64-darwin-20
DEPENDENCIES DEPENDENCIES
danger
fastlane fastlane
BUNDLED WITH BUNDLED WITH

View File

@ -30,7 +30,7 @@ buildscript {
classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5' classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5'
classpath "com.likethesalad.android:stem-plugin:2.1.1" classpath "com.likethesalad.android:stem-plugin:2.1.1"
classpath 'org.owasp:dependency-check-gradle:7.1.1' classpath 'org.owasp:dependency-check-gradle:7.1.1'
classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.7.0" classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.7.10"
classpath "org.jetbrains.kotlinx:kotlinx-knit:0.4.0" classpath "org.jetbrains.kotlinx:kotlinx-knit:0.4.0"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
@ -41,10 +41,10 @@ plugins {
// ktlint Plugin // ktlint Plugin
id "org.jlleitschuh.gradle.ktlint" version "10.3.0" id "org.jlleitschuh.gradle.ktlint" version "10.3.0"
// Detekt // Detekt
id "io.gitlab.arturbosch.detekt" version "1.20.0" id "io.gitlab.arturbosch.detekt" version "1.21.0"
// Dependency Analysis // Dependency Analysis
id 'com.autonomousapps.dependency-analysis' version "1.9.0" id 'com.autonomousapps.dependency-analysis' version "1.11.2"
} }
// https://github.com/jeremylong/DependencyCheck // https://github.com/jeremylong/DependencyCheck
@ -126,6 +126,11 @@ allprojects {
enableExperimentalRules = true enableExperimentalRules = true
// display the corresponding rule // display the corresponding rule
verbose = true verbose = true
reporters {
reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.PLAIN)
// To have XML report for Danger
reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.CHECKSTYLE)
}
disabledRules = [ disabledRules = [
// TODO Re-enable these 4 rules after reformatting project // TODO Re-enable these 4 rules after reformatting project
"indent", "indent",

View File

@ -20,9 +20,9 @@ def retrofit = "2.9.0"
def arrow = "0.8.2" def arrow = "0.8.2"
def markwon = "4.6.2" def markwon = "4.6.2"
def moshi = "1.13.0" def moshi = "1.13.0"
def lifecycle = "2.5.0" def lifecycle = "2.5.1"
def flowBinding = "1.2.0" def flowBinding = "1.2.0"
def flipper = "0.154.0" def flipper = "0.156.0"
def epoxy = "4.6.2" def epoxy = "4.6.2"
def mavericks = "2.7.0" def mavericks = "2.7.0"
def glide = "4.13.2" def glide = "4.13.2"
@ -30,7 +30,7 @@ def bigImageViewer = "1.8.1"
def jjwt = "0.11.5" def jjwt = "0.11.5"
def vanniktechEmoji = "0.15.0" def vanniktechEmoji = "0.15.0"
def fragment = "1.5.0" def fragment = "1.5.1"
// Testing // Testing
def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819 def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819
@ -51,7 +51,7 @@ ext.libs = [
'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines" 'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines"
], ],
androidx : [ androidx : [
'activity' : "androidx.activity:activity:1.5.0", 'activity' : "androidx.activity:activity:1.5.1",
'appCompat' : "androidx.appcompat:appcompat:1.4.2", 'appCompat' : "androidx.appcompat:appcompat:1.4.2",
'biometric' : "androidx.biometric:biometric:1.1.0", 'biometric' : "androidx.biometric:biometric:1.1.0",
'core' : "androidx.core:core-ktx:1.8.0", 'core' : "androidx.core:core-ktx:1.8.0",

View File

@ -75,6 +75,7 @@ ext.groups = [
'com.github.javaparser', 'com.github.javaparser',
'com.github.piasy', 'com.github.piasy',
'com.github.shyiko.klob', 'com.github.shyiko.klob',
'com.github.rubensousa',
'com.google', 'com.google',
'com.google.android', 'com.google.android',
'com.google.api.grpc', 'com.google.api.grpc',
@ -126,6 +127,7 @@ ext.groups = [
'info.picocli', 'info.picocli',
'io.arrow-kt', 'io.arrow-kt',
'io.element.android', 'io.element.android',
'io.github.davidburstrom.contester',
'io.github.detekt.sarif4k', 'io.github.detekt.sarif4k',
'io.github.microutils', 'io.github.microutils',
'io.github.reactivecircus.flowbinding', 'io.github.reactivecircus.flowbinding',

102
docs/danger.md Normal file
View File

@ -0,0 +1,102 @@
## Danger
<!--- TOC -->
* [What does danger checks](#what-does-danger-checks)
* [PR check](#pr-check)
* [Quality check](#quality-check)
* [Setup](#setup)
* [Run danger locally](#run-danger-locally)
* [Danger user](#danger-user)
* [Useful links](#useful-links)
<!--- END -->
## What does danger checks
### PR check
See the [dangerfile](../tools/danger/dangerfile.js). If you add rules in the dangerfile, please update the list below!
Here are the checks that Danger does so far:
- PR description is not empty
- Big PR got a warning to recommend to split
- PR contains a file for towncrier and extension is checked
- PR contains a Sign-Off, with exception for Element employee contributors
- PR with change on layout should include screenshot in the description
- PR which adds png file warn about the usage of vector drawables
- non draft PR should have a reviewer
### Quality check
After all the checks that generate checkstyle XML report, such as Ktlint, lint, or Detekt, Danger is run with this [dangerfile](../tools/danger/dangerfile-lint.js), in order to post comments to the PR with the detected error and warnings.
To run locally, you will have to install the plugin `danger-plugin-lint-report` using:
```shell
yarn add danger-plugin-lint-report --dev
```
## Setup
This operation should not be necessary, since Danger is already setup for the project.
To setup danger to the project, run:
```shell
bundle exec danger init
```
## Run danger locally
When modifying the [dangerfile](../tools/danger/dangerfile.js), you can check it by running Danger locally.
To run danger locally, install it and run:
```shell
bundle exec danger pr <PR_URL> --dangerfile=./tools/danger/dangerfile.js
```
For instance:
```shell
bundle exec danger pr https://github.com/vector-im/element-android/pull/6637 --dangerfile=./tools/danger/dangerfile.js
```
We may need to create a GitHub token to have less API rate limiting, and then set the env var:
```shell
export DANGER_GITHUB_API_TOKEN='YOUR_TOKEN'
```
Swift and Kotlin (just in case)
```shell
bundle exec danger-swift pr <PR_URL> --dangerfile=./tools/danger/dangerfile.js
bundle exec danger-kotlin pr <PR_URL> --dangerfile=./tools/danger/dangerfile.js
```
## Danger user
To let Danger check all the PRs, including PRs form forks, a GitHub account have been created:
- login: ElementBot
- password: Stored on Passbolt
- GitHub token: A token with limited access has been created and added to the repository https://github.com/vector-im/element-android as secret DANGER_GITHUB_API_TOKEN. This token is not saved anywhere else. In case of problem, just delete it and create a new one, then update the secret.
## Useful links
- https://danger.systems/
- https://danger.systems/js/
- https://danger.systems/js/guides/getting_started.html
- https://danger.systems/js/reference.html
- https://github.com/danger/awesome-danger
Some danger files to get inspired from
- https://github.com/artsy/emission/blob/master/dangerfile.ts
- https://github.com/facebook/react-native/blob/master/bots/dangerfile.js
- https://github.com/apollographql/apollo-client/blob/master/config/dangerfile.ts
- https://github.com/styleguidist/react-styleguidist/blob/master/dangerfile.js
- https://github.com/storybooks/storybook/blob/master/dangerfile.js
- https://github.com/ReactiveX/rxjs/blob/master/dangerfile.js

View File

@ -48,7 +48,7 @@ mv towncrier.toml towncrier.toml.bak
sed 's/CHANGES\.md/CHANGES_NIGHTLY\.md/' towncrier.toml.bak > towncrier.toml sed 's/CHANGES\.md/CHANGES_NIGHTLY\.md/' towncrier.toml.bak > towncrier.toml
rm towncrier.toml.bak rm towncrier.toml.bak
yes n | towncrier --version nightly yes n | towncrier --version nightly
./gradlew assembleGplayNightly appDistributionUploadGplayNightly $CI_GRADLE_ARG_PROPERTIES --stacktrace ./gradlew assembleGplayNightly appDistributionUploadGplayNightly $CI_GRADLE_ARG_PROPERTIES
``` ```
Then you can reset the change on the codebase. Then you can reset the change on the codebase.

View File

@ -0,0 +1,2 @@
Main changes in this version: Various bug fixes and stability improvements.
Full changelog: https://github.com/vector-im/element-android/releases

View File

@ -0,0 +1,4 @@
<vector android:height="22dp" android:viewportHeight="22"
android:viewportWidth="22" android:width="22dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#737D8C" android:fillType="evenOdd" android:pathData="M16.999,14.899C18.07,13.407 18.7,11.577 18.7,9.6C18.7,4.574 14.626,0.5 9.6,0.5C4.574,0.5 0.5,4.574 0.5,9.6C0.5,14.626 4.574,18.7 9.6,18.7C11.577,18.7 13.406,18.07 14.899,16.999C14.941,17.055 14.988,17.109 15.039,17.161L18.939,21.061C19.525,21.646 20.475,21.646 21.06,21.061C21.646,20.475 21.646,19.525 21.06,18.939L17.16,15.039C17.109,14.988 17.055,14.941 16.999,14.899ZM15.7,9.6C15.7,12.969 12.969,15.7 9.6,15.7C6.231,15.7 3.5,12.969 3.5,9.6C3.5,6.231 6.231,3.5 9.6,3.5C12.969,3.5 15.7,6.231 15.7,9.6Z"/>
</vector>

View File

@ -75,4 +75,7 @@
<dimen name="location_sharing_compass_button_margin_horizontal">8dp</dimen> <dimen name="location_sharing_compass_button_margin_horizontal">8dp</dimen>
<dimen name="location_sharing_live_duration_choice_margin_horizontal">12dp</dimen> <dimen name="location_sharing_live_duration_choice_margin_horizontal">12dp</dimen>
<dimen name="location_sharing_live_duration_choice_margin_vertical">22dp</dimen> <dimen name="location_sharing_live_duration_choice_margin_vertical">22dp</dimen>
<!-- Material 3 -->
<dimen name="collapsing_toolbar_layout_medium_size">112dp</dimen>
</resources> </resources>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<declare-styleable name="LocationLiveEndedBannerView"> <declare-styleable name="LiveLocationEndedBannerView">
<attr name="locLiveEndedBkgWithAlpha" format="boolean" /> <attr name="locLiveEndedBkgWithAlpha" format="boolean" />
<attr name="locLiveEndedIconMarginStart" format="dimension" /> <attr name="locLiveEndedIconMarginStart" format="dimension" />
</declare-styleable> </declare-styleable>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MapLoadingErrorView">
<attr name="mapErrorDescription" format="string" />
</declare-styleable>
</resources>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Widget.Vector.Button.Text.OnPrimary.LocationLive"> <style name="Widget.Vector.Button.Text.OnPrimary.LiveLocation">
<item name="android:foreground">?selectableItemBackground</item> <item name="android:foreground">?selectableItemBackground</item>
<item name="android:background">@android:color/transparent</item> <item name="android:background">@android:color/transparent</item>
<item name="android:textSize">12sp</item> <item name="android:textSize">12sp</item>
@ -12,7 +12,7 @@
<item name="android:insetLeft">8dp</item> <item name="android:insetLeft">8dp</item>
</style> </style>
<style name="Widget.Vector.Button.Text.LocationLive"> <style name="Widget.Vector.Button.Text.LiveLocation">
<item name="android:foreground">?selectableItemBackground</item> <item name="android:foreground">?selectableItemBackground</item>
<item name="android:background">@android:color/transparent</item> <item name="android:background">@android:color/transparent</item>
<item name="android:textAppearance">@style/TextAppearance.Vector.Body.Medium</item> <item name="android:textAppearance">@style/TextAppearance.Vector.Body.Medium</item>

View File

@ -39,4 +39,14 @@
<item name="android:textSize">13sp</item> <item name="android:textSize">13sp</item>
</style> </style>
<!-- Material 3 -->
<style name="Widget.Vector.Material3.Toolbar" parent="Widget.Material3.Toolbar" />
<style name="Widget.Vector.Material3.CollapsingToolbar.Medium" parent="Widget.Material3.CollapsingToolbar.Medium">
<item name="expandedTitleTextAppearance">@style/TextAppearance.Vector.Title.Medium</item>
<item name="expandedTitleMarginBottom">20dp</item>
<item name="collapsedTitleTextAppearance">@style/TextAppearance.Vector.Headline.Bold</item>
</style>
</resources> </resources>

View File

@ -32,6 +32,15 @@
<item name="android:textColor">?vctr_content_primary</item> <item name="android:textColor">?vctr_content_primary</item>
</style> </style>
<style name="TextAppearance.Vector.Headline.Bold" parent="TextAppearance.MaterialComponents.Headline1">
<item name="fontFamily">sans-serif</item>
<item name="android:fontFamily">sans-serif</item>
<item name="android:textStyle">bold</item>
<item name="android:textSize">@dimen/text_size_headline</item>
<item name="android:letterSpacing">0</item>
<item name="android:textColor">?vctr_content_primary</item>
</style>
<style name="TextAppearance.Vector.Subtitle" parent="TextAppearance.MaterialComponents.Subtitle1"> <style name="TextAppearance.Vector.Subtitle" parent="TextAppearance.MaterialComponents.Subtitle1">
<item name="fontFamily">sans-serif</item> <item name="fontFamily">sans-serif</item>
<item name="android:fontFamily">sans-serif</item> <item name="android:fontFamily">sans-serif</item>

View File

@ -189,6 +189,9 @@
<!-- Location sharing --> <!-- Location sharing -->
<item name="vctr_live_location">@color/vctr_live_location_dark</item> <item name="vctr_live_location">@color/vctr_live_location_dark</item>
<!-- Material 3 -->
<item name="collapsingToolbarLayoutMediumSize">@dimen/collapsing_toolbar_layout_medium_size</item>
</style> </style>
<style name="Theme.Vector.Dark" parent="Base.Theme.Vector.Dark" /> <style name="Theme.Vector.Dark" parent="Base.Theme.Vector.Dark" />

View File

@ -190,8 +190,12 @@
<!-- Location sharing --> <!-- Location sharing -->
<item name="vctr_live_location">@color/vctr_live_location_light</item> <item name="vctr_live_location">@color/vctr_live_location_light</item>
<!-- Material 3 -->
<item name="collapsingToolbarLayoutMediumSize">@dimen/collapsing_toolbar_layout_medium_size</item>
</style> </style>
<style name="Theme.Vector.Light" parent="Base.Theme.Vector.Light" /> <style name="Theme.Vector.Light" parent="Base.Theme.Vector.Light" />
</resources> </resources>

View File

@ -60,7 +60,7 @@ android {
// that the app's state is completely cleared between tests. // that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true' testInstrumentationRunnerArguments clearPackageData: 'true'
buildConfigField "String", "SDK_VERSION", "\"1.4.31\"" buildConfigField "String", "SDK_VERSION", "\"1.4.32\""
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""
@ -199,7 +199,7 @@ dependencies {
implementation libs.apache.commonsImaging implementation libs.apache.commonsImaging
// Phone number https://github.com/google/libphonenumber // Phone number https://github.com/google/libphonenumber
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.52' implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.53'
testImplementation libs.tests.junit testImplementation libs.tests.junit
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281 // Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281

View File

@ -62,7 +62,10 @@ fun Throwable.isUsernameInUse() = this is Failure.ServerError &&
error.code == MatrixError.M_USER_IN_USE error.code == MatrixError.M_USER_IN_USE
fun Throwable.isInvalidUsername() = this is Failure.ServerError && fun Throwable.isInvalidUsername() = this is Failure.ServerError &&
error.code == MatrixError.M_INVALID_USERNAME (error.code == MatrixError.M_INVALID_USERNAME || usernameContainsNonAsciiCharacters())
private fun Failure.ServerError.usernameContainsNonAsciiCharacters() = error.code == MatrixError.M_UNKNOWN &&
error.message == "Query parameter \'username\' must be ascii"
fun Throwable.isInvalidPassword() = this is Failure.ServerError && fun Throwable.isInvalidPassword() = this is Failure.ServerError &&
error.code == MatrixError.M_FORBIDDEN && error.code == MatrixError.M_FORBIDDEN &&

View File

@ -76,7 +76,7 @@ object StringOrderUtils {
} }
fun stringToBase(x: String, alphabet: CharArray): BigInteger { fun stringToBase(x: String, alphabet: CharArray): BigInteger {
if (x.isEmpty()) throw IllegalArgumentException() require(x.isNotEmpty())
val len = alphabet.size.toBigInteger() val len = alphabet.size.toBigInteger()
var result = BigInteger("0") var result = BigInteger("0")
x.reversed().forEachIndexed { index, c -> x.reversed().forEachIndexed { index, c ->

View File

@ -535,7 +535,7 @@ internal class MXMegolmEncryption(
@Throws @Throws
override suspend fun shareHistoryKeysWithDevice(inboundSessionWrapper: InboundGroupSessionHolder, deviceInfo: CryptoDeviceInfo) { override suspend fun shareHistoryKeysWithDevice(inboundSessionWrapper: InboundGroupSessionHolder, deviceInfo: CryptoDeviceInfo) {
if (!inboundSessionWrapper.wrapper.sessionData.sharedHistory) throw IllegalArgumentException("This key can't be shared") require(inboundSessionWrapper.wrapper.sessionData.sharedHistory) { "This key can't be shared" }
Timber.tag(loggerTag.value).i("process shareHistoryKeys for ${inboundSessionWrapper.wrapper.safeSessionId} to ${deviceInfo.shortDebugString()}") Timber.tag(loggerTag.value).i("process shareHistoryKeys for ${inboundSessionWrapper.wrapper.safeSessionId} to ${deviceInfo.shortDebugString()}")
val userId = deviceInfo.userId val userId = deviceInfo.userId
val deviceId = deviceInfo.deviceId val deviceId = deviceInfo.deviceId

View File

@ -119,7 +119,7 @@ internal class EventSenderProcessorThread @Inject constructor(
override fun cancel(eventId: String, roomId: String) { override fun cancel(eventId: String, roomId: String) {
(currentTask as? SendEventQueuedTask) (currentTask as? SendEventQueuedTask)
?.takeIf { it -> it.event.eventId == eventId && it.event.roomId == roomId } ?.takeIf { it.event.eventId == eventId && it.event.roomId == roomId }
?.cancel() ?.cancel()
} }

View File

@ -0,0 +1,29 @@
import { schedule } from 'danger'
/**
* Ref and documentation: https://github.com/damian-burke/danger-plugin-lint-report
* This file will check all the error in XML Checkstyle format.
* It covers, lint, ktlint, and detekt errors
*/
const reporter = require("danger-plugin-lint-report")
schedule(reporter.scan({
/**
* File mask used to find XML checkstyle reports.
*/
fileMask: "**/reports/**/**.xml",
/**
* If set to true, the severity will be used to switch between the different message formats (message, warn, fail).
*/
reportSeverity: true,
/**
* If set to true, only issues will be reported that are contained in the current changeset (line comparison).
* If set to false, all issues that are in modified files will be reported.
*/
requireLineModification: false,
/**
* Optional: Sets a prefix foreach violation message.
* This can be useful if there are multiple reports being parsed to make them distinguishable.
*/
// outputPrefix?: ""
}))

107
tools/danger/dangerfile.js Normal file
View File

@ -0,0 +1,107 @@
const {danger, warn} = require('danger')
/**
* Note: if you update the checks in this file, please also update the file ./docs/danger.md
*/
// Useful to see what we got in danger object
// warn(JSON.stringify(danger))
const pr = danger.github.pr
const github = danger.github
// User who has created the PR.
const user = pr.user.login
const modified = danger.git.modified_files
const created = danger.git.created_files
const editedFiles = [...modified, ...created]
// Check that the PR has a description
if (pr.body.length == 0) {
warn("Please provide a description for this PR.")
}
// Warn when there is a big PR
if (editedFiles.length > 50) {
message("This pull request seems relatively large. Please consider splitting it into multiple smaller ones.")
}
// Request a changelog for each PR
const changelogAllowList = [
"dependabot[bot]",
]
const requiresChangelog = !changelogAllowList.includes(user)
if (requiresChangelog) {
const changelogFiles = editedFiles.filter(file => file.startsWith("changelog.d/"))
if (changelogFiles.length == 0) {
warn("Please add a changelog. See instructions [here](https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md#changelog)")
} else {
const validTowncrierExtensions = [
"bugfix",
"doc",
"feature",
"misc",
"sdk",
"wip",
]
if (!changelogFiles.every(file => validTowncrierExtensions.includes(file.split(".").pop()))) {
fail("Invalid extension for changelog. See instructions [here](https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md#changelog)")
}
}
}
// Check for a sign-off
const signOff = "Signed-off-by:"
// Please add new names following the alphabetical order.
const allowList = [
"aringenbach",
"BillCarsonFr",
"bmarty",
"Claire1817",
"dependabot[bot]",
"ericdecanini",
"fedrunov",
"Florian14",
"ganfra",
"jmartinesp",
"langleyd",
"MadLittleMods",
"manuroe",
"mnaturel",
"onurays",
"ouchadam",
"stefanceriu",
"yostyle",
]
const requiresSignOff = !allowList.includes(user)
if (requiresSignOff) {
const hasPRBodySignOff = pr.body.includes(signOff)
const hasCommitSignOff = danger.git.commits.every(commit => commit.message.includes(signOff))
if (!hasPRBodySignOff && !hasCommitSignOff) {
fail("Please add a sign-off to either the PR description or to the commits themselves. See instructions [here](https://matrix-org.github.io/synapse/latest/development/contributing_guide.html#sign-off).")
}
}
// Check for screenshots on view changes
const hasChangedViews = editedFiles.filter(file => file.includes("/layout")).length > 0
if (hasChangedViews) {
if (!pr.body.includes("user-images")) {
warn("You seem to have made changes to views. Please consider adding screenshots.")
}
}
// Check for pngs on resources
const hasPngs = editedFiles.filter(file => file.toLowerCase().endsWith(".png")).length > 0
if (hasPngs) {
warn("You seem to have made changes to some images. Please consider using an vector drawable.")
}
// Check for reviewers
if (github.requested_reviewers.users.length == 0 && !pr.draft) {
warn("Please add a reviewer to your PR.")
}

View File

@ -23,6 +23,8 @@ style:
active: false active: false
ProtectedMemberInFinalClass: ProtectedMemberInFinalClass:
active: false active: false
UseCheckOrError:
active: false
empty-blocks: empty-blocks:
EmptyFunctionBlock: EmptyFunctionBlock:
@ -43,6 +45,8 @@ exceptions:
active: false active: false
TooGenericExceptionThrown: TooGenericExceptionThrown:
active: false active: false
InstanceOfCheckForException:
active: false
complexity: complexity:
TooManyFunctions: TooManyFunctions:

View File

@ -1,45 +0,0 @@
#!/usr/bin/env bash
#
# Copyright 2018 New Vector Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
branch=${TRAVIS_BRANCH}
# echo ${TRAVIS_BRANCH}
# If not on develop, exit, else we cannot get the list of modified files
# It is ok to check only when on develop branch
if [[ "${branch}" -eq 'develop' ]]; then
echo "Check that a file has been added to /changelog.d"
else
echo "Not on develop branch"
exit 0
fi
# git status
listOfModifiedFiles=`git diff --name-only HEAD ${branch}`
# echo "List of modified files by this PR:"
# echo ${listOfModifiedFiles}
if [[ ${listOfModifiedFiles} = *"changelog.d"* ]]; then
echo "A file has been added to /changelog.d!"
else
echo "❌ Please add a file describing your changes in /changelog.d. See https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md#changelog"
exit 1
fi

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.config
/**
* The types of analytics Element currently supports.
*/
sealed interface Analytics {
/**
* Disables the analytics integrations.
*/
object Disabled : Analytics
/**
* Analytics integration via PostHog.
*/
data class PostHog(
/**
* The PostHog instance url.
*/
val postHogHost: String,
/**
* The PostHog instance API key.
*/
val postHogApiKey: String,
/**
* A URL to more information about the analytics collection.
*/
val policyLink: String,
) : Analytics
}

View File

@ -36,4 +36,57 @@ object Config {
* - Changing the value from `true` to `false` will force the app to return to the background sync / Firebase Push. * - Changing the value from `true` to `false` will force the app to return to the background sync / Firebase Push.
*/ */
const val ALLOW_EXTERNAL_UNIFIED_PUSH_DISTRIBUTORS = true const val ALLOW_EXTERNAL_UNIFIED_PUSH_DISTRIBUTORS = true
const val ENABLE_LOCATION_SHARING = true
const val LOCATION_MAP_TILER_KEY = "fU3vlMsMn4Jb6dnEIFsx"
/**
* The maximum length of voice messages in milliseconds.
*/
const val VOICE_MESSAGE_LIMIT_MS = 120_000L
/**
* The strategy for sharing device keys.
*/
val KEY_SHARING_STRATEGY = KeySharingStrategy.WhenTyping
/**
* The onboarding flow.
*/
val ONBOARDING_VARIANT = OnboardingVariant.LEGACY
/**
* If set, MSC3086 asserted identity messages sent on VoIP calls will cause the call to appear in the room corresponding to the asserted identity.
* This *must* only be set in trusted environments.
*/
const val HANDLE_CALL_ASSERTED_IDENTITY_EVENTS = false
const val LOW_PRIVACY_LOG_ENABLE = false
const val ENABLE_STRICT_MODE_LOGS = false
/**
* The analytics configuration to use for the Debug build type.
* Can be disabled by providing Analytics.Disabled
*/
val DEBUG_ANALYTICS_CONFIG = Analytics.PostHog(
postHogHost = "https://posthog.element.dev",
postHogApiKey = "phc_VtA1L35nw3aeAtHIx1ayrGdzGkss7k1xINeXcoIQzXN",
policyLink = "https://element.io/cookie-policy",
)
/**
* The analytics configuration to use for the Release build type.
* Can be disabled by providing Analytics.Disabled
*/
val RELEASE_ANALYTICS_CONFIG = Analytics.PostHog(
postHogHost = "https://posthog.hss.element.io",
postHogApiKey = "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO",
policyLink = "https://element.io/cookie-policy",
)
/**
* The analytics configuration to use for the Nightly build type.
* Can be disabled by providing Analytics.Disabled
*/
val NIGHTLY_ANALYTICS_CONFIG = RELEASE_ANALYTICS_CONFIG
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2021 New Vector Ltd * Copyright (c) 2022 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,17 +16,20 @@
package im.vector.app.config package im.vector.app.config
import im.vector.app.BuildConfig enum class KeySharingStrategy {
import im.vector.app.features.analytics.AnalyticsConfig /**
* Keys will be sent for the first time when the first message is sent.
* This is handled by the Matrix SDK so there's no need to do it in Vector.
*/
WhenSendingEvent,
private val allowedPackageList = listOf( /**
"im.vector.app", * Keys will be sent for the first time when the timeline displayed.
"im.vector.app.nightly", */
) WhenEnteringRoom,
val analyticsConfig: AnalyticsConfig = object : AnalyticsConfig { /**
override val isEnabled = BuildConfig.APPLICATION_ID in allowedPackageList * Keys will be sent for the first time when a typing started.
override val postHogHost = "https://posthog.hss.element.io" */
override val postHogApiKey = "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO" WhenTyping
override val policyLink = "https://element.io/cookie-policy"
} }

View File

@ -0,0 +1,23 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.config
enum class OnboardingVariant {
LEGACY,
LOGIN_2,
FTUE_AUTH
}

View File

@ -27,6 +27,7 @@ knit {
exclude '**/.gradle/**' exclude '**/.gradle/**'
exclude '**/towncrier/template.md' exclude '**/towncrier/template.md'
exclude '**/CHANGES.md' exclude '**/CHANGES.md'
exclude '/node_modules'
} }
} }
@ -36,7 +37,7 @@ ext.versionMinor = 4
// Note: even values are reserved for regular release, odd values for hotfix release. // Note: even values are reserved for regular release, odd values for hotfix release.
// When creating a hotfix, you should decrease the value, since the current value // When creating a hotfix, you should decrease the value, since the current value
// is the value for the next regular release. // is the value for the next regular release.
ext.versionPatch = 31 ext.versionPatch = 32
ext.scVersion = 56 ext.scVersion = 56
@ -157,19 +158,6 @@ android {
buildConfigField "String", "GIT_BRANCH_NAME", "\"${gitBranchName()}\"" buildConfigField "String", "GIT_BRANCH_NAME", "\"${gitBranchName()}\""
buildConfigField "String", "BUILD_NUMBER", "\"${buildNumber}\"" buildConfigField "String", "BUILD_NUMBER", "\"${buildNumber}\""
buildConfigField "im.vector.app.features.VectorFeatures.OnboardingVariant", "ONBOARDING_VARIANT", "im.vector.app.features.VectorFeatures.OnboardingVariant.LEGACY"
buildConfigField "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy", "outboundSessionKeySharingStrategy", "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy.WhenTyping"
buildConfigField "Long", "VOICE_MESSAGE_DURATION_LIMIT_MS", "120_000L"
// If set, MSC3086 asserted identity messages sent on VoIP calls will cause the call to appear in the room corresponding to the asserted identity.
// This *must* only be set in trusted environments.
buildConfigField "Boolean", "handleCallAssertedIdentityEvents", "false"
buildConfigField "Boolean", "enableLocationSharing", "true"
buildConfigField "String", "mapTilerKey", "\"fU3vlMsMn4Jb6dnEIFsx\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// Keep abiFilter for the universalApk // Keep abiFilter for the universalApk
@ -253,10 +241,6 @@ android {
resValue "string", "app_name", "SchildiChat dbg" resValue "string", "app_name", "SchildiChat dbg"
resValue "color", "launcher_background", "#0DBD8B" resValue "color", "launcher_background", "#0DBD8B"
buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false"
// Set to true if you want to enable strict mode in debug
buildConfigField "boolean", "ENABLE_STRICT_MODE_LOGS", "false"
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
if (project.hasProperty("coverage")) { if (project.hasProperty("coverage")) {
@ -268,10 +252,6 @@ android {
resValue "string", "app_name", "SchildiChat" resValue "string", "app_name", "SchildiChat"
resValue "color", "launcher_background", "#0DBD8B" resValue "color", "launcher_background", "#0DBD8B"
buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false"
buildConfigField "boolean", "ENABLE_STRICT_MODE_LOGS", "false"
// When updating this block, please also update the same block in the `nightly` buildType below
postprocessing { postprocessing {
removeUnusedCode true removeUnusedCode true
removeUnusedResources true removeUnusedResources true
@ -333,8 +313,6 @@ android {
versionName "1.4.31.sc56" versionName "1.4.31.sc56"
resValue "bool", "isGplay", "true" resValue "bool", "isGplay", "true"
buildConfigField "boolean", "ALLOW_FCM_USE", "true"
buildConfigField "boolean", "ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB", "true"
buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"G\"" buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"G\""
buildConfigField "String", "FLAVOR_DESCRIPTION", "\"GooglePlay\"" buildConfigField "String", "FLAVOR_DESCRIPTION", "\"GooglePlay\""
} }
@ -345,8 +323,6 @@ android {
versionName "1.4.31.sc56" versionName "1.4.31.sc56"
resValue "bool", "isGplay", "false" resValue "bool", "isGplay", "false"
buildConfigField "boolean", "ALLOW_FCM_USE", "false"
buildConfigField "boolean", "ALLOW_EXTERNAL_UNIFIEDPUSH_DISTRIB", "true"
buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"F\"" buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"F\""
buildConfigField "String", "FLAVOR_DESCRIPTION", "\"FDroid\"" buildConfigField "String", "FLAVOR_DESCRIPTION", "\"FDroid\""
} }
@ -450,7 +426,7 @@ dependencies {
implementation 'com.facebook.stetho:stetho:1.6.0' implementation 'com.facebook.stetho:stetho:1.6.0'
// Phone number https://github.com/google/libphonenumber // Phone number https://github.com/google/libphonenumber
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.52' implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.53'
// FlowBinding // FlowBinding
implementation libs.github.flowBinding implementation libs.github.flowBinding
@ -462,6 +438,9 @@ dependencies {
implementation libs.airbnb.epoxyPaging implementation libs.airbnb.epoxyPaging
implementation libs.airbnb.mavericks implementation libs.airbnb.mavericks
// Snap Helper https://github.com/rubensousa/GravitySnapHelper
implementation 'com.github.rubensousa:gravitysnaphelper:2.2.2'
// Nightly // Nightly
// API-only library // API-only library
gplayImplementation libs.google.appdistributionApi gplayImplementation libs.google.appdistributionApi
@ -634,4 +613,5 @@ dependencies {
androidTestImplementation libs.mockk.mockkAndroid androidTestImplementation libs.mockk.mockkAndroid
androidTestUtil libs.androidx.orchestrator androidTestUtil libs.androidx.orchestrator
debugImplementation libs.androidx.fragmentTesting debugImplementation libs.androidx.fragmentTesting
androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.10"
} }

View File

@ -102,6 +102,7 @@
<!-- Wording --> <!-- Wording -->
<issue id="Typos" severity="error" /> <issue id="Typos" severity="error" />
<issue id="TypographyDashes" severity="error" /> <issue id="TypographyDashes" severity="error" />
<issue id="PluralsCandidate" severity="error" />
<!-- DI --> <!-- DI -->
<issue id="JvmStaticProvidesInObjectDetector" severity="error" /> <issue id="JvmStaticProvidesInObjectDetector" severity="error" />

View File

@ -27,6 +27,7 @@ import im.vector.app.features.MainActivity
import im.vector.app.ui.robot.ElementRobot import im.vector.app.ui.robot.ElementRobot
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.runner.RunWith import org.junit.runner.RunWith
import java.util.UUID import java.util.UUID
@ -35,7 +36,9 @@ import java.util.UUID
class CantVerifyTest : VerificationTestBase() { class CantVerifyTest : VerificationTestBase() {
@get:Rule @get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java) val testRule = RuleChain
.outerRule(ActivityScenarioRule(MainActivity::class.java))
.around(ClearCurrentSessionRule())
private val elementRobot = ElementRobot() private val elementRobot = ElementRobot()
var userName: String = "loginTest_${UUID.randomUUID()}" var userName: String = "loginTest_${UUID.randomUUID()}"

View File

@ -0,0 +1,63 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.test.platform.app.InstrumentationRegistry
import im.vector.app.features.analytics.store.AnalyticsStore
import kotlinx.coroutines.runBlocking
import org.junit.rules.TestWatcher
import org.junit.runner.Description
import org.junit.runners.model.Statement
import kotlin.reflect.KClass
/**
* A TestRule to reset and clear the current Session.
* If a Session is active it will be signed out and cleared from the ActiveSessionHolder.
* The VectorPreferences and AnalyticsDatastore are also cleared in an attempt to recreate a fresh base.
*/
class ClearCurrentSessionRule : TestWatcher() {
override fun apply(base: Statement, description: Description): Statement {
val context = InstrumentationRegistry.getInstrumentation().targetContext
runBlocking {
reflectAnalyticDatastore(context).edit { it.clear() }
runCatching {
val holder = (context.applicationContext as VectorApplication).activeSessionHolder
holder.getSafeActiveSession()?.signOutService()?.signOut(true)
(context.applicationContext as VectorApplication).vectorPreferences.clearPreferences()
holder.clearActiveSession()
}
}
return super.apply(base, description)
}
}
private fun KClass<*>.asTopLevel() = Class.forName("${qualifiedName}Kt")
/**
* Fetches the top level, private [Context.dataStore] extension property from [im.vector.app.features.analytics.store.AnalyticsStore]
* via reflection to avoid exposing property to all callers.
*/
@Suppress("UNCHECKED_CAST")
private fun reflectAnalyticDatastore(context: Context): DataStore<Preferences> {
val klass = AnalyticsStore::class.asTopLevel()
val method = klass.getMethod("access\$getDataStore", Context::class.java)
return method.invoke(klass, context) as DataStore<Preferences>
}

View File

@ -31,7 +31,6 @@ import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import im.vector.app.TestBuildVersionSdkIntProvider import im.vector.app.TestBuildVersionSdkIntProvider
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguratorProvider
import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode
import im.vector.app.features.pin.lockscreen.crypto.LockScreenCryptoConstants import im.vector.app.features.pin.lockscreen.crypto.LockScreenCryptoConstants
import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeyRepository import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeyRepository
@ -40,6 +39,7 @@ import im.vector.app.features.pin.lockscreen.ui.fallbackprompt.FallbackBiometric
import im.vector.app.features.pin.lockscreen.utils.DevicePromptCheck import im.vector.app.features.pin.lockscreen.utils.DevicePromptCheck
import io.mockk.clearAllMocks import io.mockk.clearAllMocks
import io.mockk.every import io.mockk.every
import io.mockk.justRun
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkObject import io.mockk.mockkObject
import io.mockk.mockkStatic import io.mockk.mockkStatic
@ -54,8 +54,10 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.amshove.kluent.coInvoking
import org.amshove.kluent.shouldBeFalse import org.amshove.kluent.shouldBeFalse
import org.amshove.kluent.shouldBeTrue import org.amshove.kluent.shouldBeTrue
import org.amshove.kluent.shouldThrow
import org.junit.Before import org.junit.Before
import org.junit.Ignore import org.junit.Ignore
import org.junit.Test import org.junit.Test
@ -239,36 +241,35 @@ class BiometricHelperTests {
@Test @Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R) // Due to some issues with mockk and CryptoObject initialization @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R) // Due to some issues with mockk and CryptoObject initialization
fun authenticateCreatesSystemKeyIfNeededOnSuccessOnAndroidM() = runTest { fun enableAuthenticationDeletesSystemKeyOnFailure() = runTest {
buildVersionSdkIntProvider.value = Build.VERSION_CODES.M buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
every { lockScreenKeyRepository.isSystemKeyValid() } returns true
val mockAuthChannel = Channel<Boolean>(capacity = 1) val mockAuthChannel = Channel<Boolean>(capacity = 1)
val biometricUtils = spyk(createBiometricHelper(createDefaultConfiguration(isBiometricsEnabled = true))) { val biometricUtils = spyk(createBiometricHelper(createDefaultConfiguration(isBiometricsEnabled = true))) {
every { createAuthChannel() } returns mockAuthChannel every { createAuthChannel() } returns mockAuthChannel
every { authenticateWithPromptInternal(any(), any(), any()) } returns mockk() every { authenticateWithPromptInternal(any(), any(), any()) } returns mockk()
} }
justRun { lockScreenKeyRepository.deleteSystemKey() }
val latch = CountDownLatch(1) val latch = CountDownLatch(1)
val intent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, LockScreenTestActivity::class.java) val intent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, LockScreenTestActivity::class.java)
ActivityScenario.launch<LockScreenTestActivity>(intent).onActivity { activity -> ActivityScenario.launch<LockScreenTestActivity>(intent).onActivity { activity ->
activity.lifecycleScope.launch { activity.lifecycleScope.launch {
val exception = IllegalStateException("Some error")
launch { launch {
mockAuthChannel.send(true) mockAuthChannel.close(exception)
mockAuthChannel.close()
} }
biometricUtils.authenticate(activity).collect() coInvoking { biometricUtils.enableAuthentication(activity).collect() } shouldThrow exception
latch.countDown() latch.countDown()
} }
} }
latch.await(1, TimeUnit.SECONDS) latch.await(1, TimeUnit.SECONDS)
verify { lockScreenKeyRepository.ensureSystemKey() } verify { lockScreenKeyRepository.deleteSystemKey() }
} }
private fun createBiometricHelper(configuration: LockScreenConfiguration): BiometricHelper { private fun createBiometricHelper(configuration: LockScreenConfiguration): BiometricHelper {
val context = InstrumentationRegistry.getInstrumentation().targetContext val context = InstrumentationRegistry.getInstrumentation().targetContext
val configProvider = LockScreenConfiguratorProvider(configuration) return BiometricHelper(configuration, context, lockScreenKeyRepository, biometricManager, buildVersionSdkIntProvider)
return BiometricHelper(context, lockScreenKeyRepository, configProvider, biometricManager, buildVersionSdkIntProvider)
} }
private fun createDefaultConfiguration( private fun createDefaultConfiguration(

View File

@ -18,6 +18,7 @@ package im.vector.app.features.pin.lockscreen.crypto
import android.os.Build import android.os.Build
import android.security.keystore.KeyPermanentlyInvalidatedException import android.security.keystore.KeyPermanentlyInvalidatedException
import android.security.keystore.UserNotAuthenticatedException
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import im.vector.app.TestBuildVersionSdkIntProvider import im.vector.app.TestBuildVersionSdkIntProvider
import io.mockk.every import io.mockk.every
@ -69,10 +70,12 @@ class KeyStoreCryptoTests {
runCatching { keyStoreCrypto.ensureKey() } runCatching { keyStoreCrypto.ensureKey() }
keyStoreCrypto.hasValidKey() shouldBe true keyStoreCrypto.hasValidKey() shouldBe true
val exception = KeyPermanentlyInvalidatedException() val keyInvalidatedException = KeyPermanentlyInvalidatedException()
every { secretStoringUtils.getEncryptCipher(any()) } throws exception every { secretStoringUtils.getEncryptCipher(any()) } throws keyInvalidatedException
keyStoreCrypto.hasValidKey() shouldBe false
runCatching { keyStoreCrypto.ensureKey() } val userNotAuthenticatedException = UserNotAuthenticatedException()
every { secretStoringUtils.getEncryptCipher(any()) } throws userNotAuthenticatedException
keyStoreCrypto.hasValidKey() shouldBe false keyStoreCrypto.hasValidKey() shouldBe false
} }

View File

@ -17,8 +17,6 @@
package im.vector.app.features.pin.lockscreen.crypto package im.vector.app.features.pin.lockscreen.crypto
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import im.vector.app.features.pin.lockscreen.crypto.migrations.LegacyPinCodeMigrator
import im.vector.app.features.settings.VectorPreferences
import io.mockk.clearAllMocks import io.mockk.clearAllMocks
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
@ -44,8 +42,6 @@ class LockScreenKeyRepositoryTests {
} }
private lateinit var lockScreenKeyRepository: LockScreenKeyRepository private lateinit var lockScreenKeyRepository: LockScreenKeyRepository
private val legacyPinCodeMigrator: LegacyPinCodeMigrator = mockk(relaxed = true)
private val vectorPreferences: VectorPreferences = mockk(relaxed = true)
private val keyStore: KeyStore by lazy { private val keyStore: KeyStore by lazy {
KeyStore.getInstance(LockScreenCryptoConstants.ANDROID_KEY_STORE).also { it.load(null) } KeyStore.getInstance(LockScreenCryptoConstants.ANDROID_KEY_STORE).also { it.load(null) }

View File

@ -70,6 +70,11 @@ class DebugFeaturesStateFactory @Inject constructor(
key = DebugFeatureKeys.allowExternalUnifiedPushDistributors, key = DebugFeatureKeys.allowExternalUnifiedPushDistributors,
factory = VectorFeatures::allowExternalUnifiedPushDistributors factory = VectorFeatures::allowExternalUnifiedPushDistributors
), ),
createBooleanFeature(
label = "Enable Live Location Sharing",
key = DebugFeatureKeys.liveLocationSharing,
factory = VectorFeatures::isLocationSharingEnabled
),
createBooleanFeature( createBooleanFeature(
label = "Force usage of OpusEncoder library", label = "Force usage of OpusEncoder library",
key = DebugFeatureKeys.forceUsageOfOpusEncoder, key = DebugFeatureKeys.forceUsageOfOpusEncoder,

View File

@ -24,6 +24,7 @@ import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import im.vector.app.config.OnboardingVariant
import im.vector.app.features.DefaultVectorFeatures import im.vector.app.features.DefaultVectorFeatures
import im.vector.app.features.VectorFeatures import im.vector.app.features.VectorFeatures
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@ -39,8 +40,8 @@ class DebugVectorFeatures(
private val dataStore = context.dataStore private val dataStore = context.dataStore
override fun onboardingVariant(): VectorFeatures.OnboardingVariant { override fun onboardingVariant(): OnboardingVariant {
return readPreferences().getEnum<VectorFeatures.OnboardingVariant>() ?: vectorFeatures.onboardingVariant() return readPreferences().getEnum<OnboardingVariant>() ?: vectorFeatures.onboardingVariant()
} }
override fun isOnboardingAlreadyHaveAccountSplashEnabled(): Boolean = read(DebugFeatureKeys.onboardingAlreadyHaveAnAccount) override fun isOnboardingAlreadyHaveAccountSplashEnabled(): Boolean = read(DebugFeatureKeys.onboardingAlreadyHaveAnAccount)
@ -66,6 +67,9 @@ class DebugVectorFeatures(
override fun isScreenSharingEnabled(): Boolean = read(DebugFeatureKeys.screenSharing) override fun isScreenSharingEnabled(): Boolean = read(DebugFeatureKeys.screenSharing)
?: vectorFeatures.isScreenSharingEnabled() ?: vectorFeatures.isScreenSharingEnabled()
override fun isLocationSharingEnabled(): Boolean = read(DebugFeatureKeys.liveLocationSharing)
?: vectorFeatures.isLocationSharingEnabled()
override fun forceUsageOfOpusEncoder(): Boolean = read(DebugFeatureKeys.forceUsageOfOpusEncoder) override fun forceUsageOfOpusEncoder(): Boolean = read(DebugFeatureKeys.forceUsageOfOpusEncoder)
?: vectorFeatures.forceUsageOfOpusEncoder() ?: vectorFeatures.forceUsageOfOpusEncoder()

View File

@ -351,7 +351,7 @@
<activity android:name=".features.spaces.leave.SpaceLeaveAdvancedActivity" /> <activity android:name=".features.spaces.leave.SpaceLeaveAdvancedActivity" />
<activity android:name=".features.poll.create.CreatePollActivity" /> <activity android:name=".features.poll.create.CreatePollActivity" />
<activity android:name=".features.location.LocationSharingActivity" /> <activity android:name=".features.location.LocationSharingActivity" />
<activity android:name=".features.location.live.map.LocationLiveMapViewActivity" /> <activity android:name=".features.location.live.map.LiveLocationMapViewActivity" />
<activity android:name=".features.settings.font.FontScaleSettingActivity"/> <activity android:name=".features.settings.font.FontScaleSettingActivity"/>
<!-- Services --> <!-- Services -->
@ -381,7 +381,7 @@
</service> </service>
<service <service
android:name=".features.location.LocationSharingAndroidService" android:name=".features.location.live.tracking.LocationSharingAndroidService"
android:exported="false" android:exported="false"
android:foregroundServiceType="location" /> android:foregroundServiceType="location" />

View File

@ -0,0 +1,76 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app
import androidx.lifecycle.DefaultLifecycleObserver
import arrow.core.Option
import kotlinx.coroutines.flow.Flow
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.model.RoomSummary
/**
* Gets info about the current space the user has navigated to, any space backstack they may have
* and handles switching to different spaces.
*/
interface SpaceStateHandler : DefaultLifecycleObserver {
/**
* Gets the current space the current user has navigated to.
*
* @return null if the user is not in
*/
fun getCurrentSpace(): RoomSummary?
/**
* Sets the new space the current user is navigating to.
*
* @param spaceId the id of the space being navigated to
* @param session the current active session
* @param persistNow if true, the current space will immediately be persisted in shared prefs
* @param isForwardNavigation whether this navigation is a forward action to properly handle backstack
*/
fun setCurrentSpace(
spaceId: String?,
session: Session? = null,
persistNow: Boolean = false,
isForwardNavigation: Boolean = true,
from: SelectSpaceFrom = SelectSpaceFrom.SELECT,
)
/**
* Gets the current backstack of spaces (via their id).
*
* null may be an entry in the ArrayDeque to indicate the root space (All Chats)
*/
fun getSpaceBackstack(): ArrayDeque<String?>
/**
* Gets a flow of the selected space for clients to react immediately to space changes.
*/
fun getSelectedSpaceFlow(): Flow<Option<RoomSummary?>>
// SC: no viewpager swipes included
fun getSelectedSpaceFlowIgnoreSwipe(): Flow<Option<Pair<RoomSummary?, SelectSpaceFrom>>>
/**
* Gets the id of the active space, or null if there is none.
*/
fun getSafeActiveSpaceId(): String?
// Some SC additions
fun persistSelectedSpace();
}

View File

@ -16,7 +16,6 @@
package im.vector.app package im.vector.app
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import arrow.core.Option import arrow.core.Option
import arrow.core.getOrElse import arrow.core.getOrElse
@ -61,16 +60,15 @@ enum class SelectSpaceFrom {
/** /**
* This class handles the global app state. * This class handles the global app state.
* It requires to be added to ProcessLifecycleOwner.get().lifecycle * It is required that this class is added as an observer to ProcessLifecycleOwner.get().lifecycle in [VectorApplication]
*/ */
// TODO Keep this class for now, will maybe be used fro Space
@Singleton @Singleton
class AppStateHandler @Inject constructor( class SpaceStateHandlerImpl @Inject constructor(
private val sessionDataSource: ActiveSessionDataSource, private val sessionDataSource: ActiveSessionDataSource,
private val uiStateRepository: UiStateRepository, private val uiStateRepository: UiStateRepository,
private val activeSessionHolder: ActiveSessionHolder, private val activeSessionHolder: ActiveSessionHolder,
private val analyticsTracker: AnalyticsTracker private val analyticsTracker: AnalyticsTracker
) : DefaultLifecycleObserver { ) : SpaceStateHandler {
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
@ -78,26 +76,34 @@ class AppStateHandler @Inject constructor(
// SC: Call it different then the upstream one so we don't forget adding `first` to upstream's logic. // SC: Call it different then the upstream one so we don't forget adding `first` to upstream's logic.
private val selectedSpaceDataSourceSc = BehaviorDataSource<Option<Pair<RoomSummary?, SelectSpaceFrom>>>(Option.empty()) private val selectedSpaceDataSourceSc = BehaviorDataSource<Option<Pair<RoomSummary?, SelectSpaceFrom>>>(Option.empty())
//private val selectedSpaceFlow = selectedSpaceDataSource.stream()
//val selectedSpaceFlow = selectedSpaceDataSource.stream() private val selectedSpaceFlow = selectedSpaceDataSourceSc.stream().map { it.map { it.first } }
private val selectedSpaceFlowIgnoreSwipe = selectedSpaceDataSourceSc.stream()
val selectedSpaceFlow = selectedSpaceDataSourceSc.stream().map { it.map { it.first } }
val selectedSpaceFlowIgnoreSwipe = selectedSpaceDataSourceSc.stream()
.filter { it.getOrElse{ null }?.second != SelectSpaceFrom.SWIPE } .filter { it.getOrElse{ null }?.second != SelectSpaceFrom.SWIPE }
private val spaceBackstack = ArrayDeque<String?>() private val spaceBackstack = ArrayDeque<String?>()
fun getCurrentSpace(): RoomSummary? { override fun getCurrentSpace(): RoomSummary? {
return selectedSpaceDataSourceSc.currentValue?.orNull()?.first?.let { spaceSummary -> return selectedSpaceDataSourceSc.currentValue?.orNull()?.first?.let { spaceSummary ->
activeSessionHolder.getSafeActiveSession()?.roomService()?.getRoomSummary(spaceSummary.roomId) activeSessionHolder.getSafeActiveSession()?.roomService()?.getRoomSummary(spaceSummary.roomId)
} }
} }
fun setCurrentSpace(spaceId: String?, session: Session? = null, persistNow: Boolean = false, isForwardNavigation: Boolean = true, from: SelectSpaceFrom = SelectSpaceFrom.SELECT) { override fun setCurrentSpace(
spaceId: String?,
session: Session?,
persistNow: Boolean,
isForwardNavigation: Boolean,
from: SelectSpaceFrom,
) {
val activeSession = session ?: activeSessionHolder.getSafeActiveSession() ?: return
val currentSpace = selectedSpaceDataSourceSc.currentValue?.orNull()?.first val currentSpace = selectedSpaceDataSourceSc.currentValue?.orNull()?.first
val uSession = session ?: activeSessionHolder.getSafeActiveSession() ?: return val spaceSummary = spaceId?.let { activeSession.getRoomSummary(spaceId) }
if (currentSpace != null && spaceId == currentSpace.roomId) return val sameSpaceSelected = currentSpace != null && spaceId == currentSpace.roomId
val spaceSum = spaceId?.let { uSession.getRoomSummary(spaceId) }
if (sameSpaceSelected) {
return
}
if (DbgUtil.isDbgEnabled(DbgUtil.DBG_VIEW_PAGER) && from == SelectSpaceFrom.SELECT) { if (DbgUtil.isDbgEnabled(DbgUtil.DBG_VIEW_PAGER) && from == SelectSpaceFrom.SELECT) {
// We want a stack trace // We want a stack trace
@ -109,20 +115,20 @@ class AppStateHandler @Inject constructor(
} }
if (persistNow) { if (persistNow) {
uiStateRepository.storeSelectedSpace(spaceSum?.roomId, uSession.sessionId) uiStateRepository.storeSelectedSpace(spaceSummary?.roomId, activeSession.sessionId)
} }
if (spaceSum == null) { if (spaceSummary == null) {
//selectedSpaceDataSourceSc.post(Option.empty()) //selectedSpaceDataSourceSc.post(Option.empty())
selectedSpaceDataSourceSc.post(Option.just(Pair(null, from))) selectedSpaceDataSourceSc.post(Option.just(Pair(null, from)))
} else { } else {
selectedSpaceDataSourceSc.post(Option.just(Pair(spaceSum, from))) selectedSpaceDataSourceSc.post(Option.just(Pair(spaceSummary, from)))
} }
if (spaceId != null) { if (spaceId != null) {
uSession.coroutineScope.launch(Dispatchers.IO) { activeSession.coroutineScope.launch(Dispatchers.IO) {
tryOrNull { tryOrNull {
uSession.getRoom(spaceId)?.membershipService()?.loadRoomMembersIfNeeded() activeSession.getRoom(spaceId)?.membershipService()?.loadRoomMembersIfNeeded()
} }
} }
} }
@ -153,9 +159,13 @@ class AppStateHandler @Inject constructor(
} }
*/ */
fun getSpaceBackstack() = spaceBackstack override fun getSpaceBackstack() = spaceBackstack
fun safeActiveSpaceId(): String? { override fun getSelectedSpaceFlow() = selectedSpaceFlow
override fun getSelectedSpaceFlowIgnoreSwipe() = selectedSpaceFlowIgnoreSwipe
override fun getSafeActiveSpaceId(): String? {
return selectedSpaceDataSourceSc.currentValue?.orNull()?.first?.roomId return selectedSpaceDataSourceSc.currentValue?.orNull()?.first?.roomId
} }
@ -169,7 +179,7 @@ class AppStateHandler @Inject constructor(
uiStateRepository.storeSelectedSpace(selectedSpaceDataSourceSc.currentValue?.orNull()?.first?.roomId, session.sessionId) uiStateRepository.storeSelectedSpace(selectedSpaceDataSourceSc.currentValue?.orNull()?.first?.roomId, session.sessionId)
} }
fun persistSelectedSpace() { override fun persistSelectedSpace() {
val currentValue = selectedSpaceDataSourceSc.currentValue?.orNull() ?: return val currentValue = selectedSpaceDataSourceSc.currentValue?.orNull() ?: return
val currentMethod = currentValue.first val currentMethod = currentValue.first
val uSession = activeSessionHolder.getSafeActiveSession() ?: return val uSession = activeSessionHolder.getSafeActiveSession() ?: return

View File

@ -25,24 +25,30 @@ import android.content.res.Configuration
import android.os.Handler import android.os.Handler
import android.os.HandlerThread import android.os.HandlerThread
import android.os.StrictMode import android.os.StrictMode
import android.view.Gravity
import androidx.core.provider.FontRequest import androidx.core.provider.FontRequest
import androidx.core.provider.FontsContractCompat import androidx.core.provider.FontsContractCompat
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.multidex.MultiDex import androidx.multidex.MultiDex
import androidx.recyclerview.widget.SnapHelper
import com.airbnb.epoxy.Carousel
import com.airbnb.epoxy.EpoxyAsyncUtil import com.airbnb.epoxy.EpoxyAsyncUtil
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.Mavericks
import com.facebook.stetho.Stetho import com.facebook.stetho.Stetho
import com.gabrielittner.threetenbp.LazyThreeTen import com.gabrielittner.threetenbp.LazyThreeTen
import com.github.rubensousa.gravitysnaphelper.GravitySnapHelper
import com.mapbox.mapboxsdk.Mapbox import com.mapbox.mapboxsdk.Mapbox
import com.vanniktech.emoji.EmojiManager import com.vanniktech.emoji.EmojiManager
import com.vanniktech.emoji.google.GoogleEmojiProvider import com.vanniktech.emoji.google.GoogleEmojiProvider
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import de.spiritcroc.matrixsdk.StaticScSdkHelper import de.spiritcroc.matrixsdk.StaticScSdkHelper
import de.spiritcroc.matrixsdk.util.DbgUtil import de.spiritcroc.matrixsdk.util.DbgUtil
import im.vector.app.config.Config
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.resources.BuildMeta
import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.analytics.VectorAnalytics
import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.configuration.VectorConfiguration import im.vector.app.features.configuration.VectorConfiguration
@ -90,7 +96,7 @@ class VectorApplication :
@Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var vectorPreferences: VectorPreferences
@Inject lateinit var versionProvider: VersionProvider @Inject lateinit var versionProvider: VersionProvider
@Inject lateinit var notificationUtils: NotificationUtils @Inject lateinit var notificationUtils: NotificationUtils
@Inject lateinit var appStateHandler: AppStateHandler @Inject lateinit var spaceStateHandler: SpaceStateHandler
@Inject lateinit var popupAlertManager: PopupAlertManager @Inject lateinit var popupAlertManager: PopupAlertManager
@Inject lateinit var pinLocker: PinLocker @Inject lateinit var pinLocker: PinLocker
@Inject lateinit var callManager: WebRtcCallManager @Inject lateinit var callManager: WebRtcCallManager
@ -101,6 +107,7 @@ class VectorApplication :
@Inject lateinit var flipperProxy: FlipperProxy @Inject lateinit var flipperProxy: FlipperProxy
@Inject lateinit var matrix: Matrix @Inject lateinit var matrix: Matrix
@Inject lateinit var fcmHelper: FcmHelper @Inject lateinit var fcmHelper: FcmHelper
@Inject lateinit var buildMeta: BuildMeta
// font thread handler // font thread handler
private var fontThreadHandler: Handler? = null private var fontThreadHandler: Handler? = null
@ -133,19 +140,20 @@ class VectorApplication :
.filterIsInstance(JitsiMeetDefaultLogHandler::class.java) .filterIsInstance(JitsiMeetDefaultLogHandler::class.java)
.forEach { Timber.uproot(it) } .forEach { Timber.uproot(it) }
if (BuildConfig.DEBUG) { if (buildMeta.isDebug) {
Timber.plant(Timber.DebugTree()) Timber.plant(Timber.DebugTree())
} }
Timber.plant(vectorFileLogger) Timber.plant(vectorFileLogger)
if (BuildConfig.DEBUG) { if (buildMeta.isDebug) {
Stetho.initializeWithDefaults(this) Stetho.initializeWithDefaults(this)
} }
logInfo() logInfo()
LazyThreeTen.init(this) LazyThreeTen.init(this)
Mavericks.initialize(debugMode = false) Mavericks.initialize(debugMode = false)
EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler() configureEpoxy()
registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks(popupAlertManager)) registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks(popupAlertManager))
val fontRequest = FontRequest( val fontRequest = FontRequest(
"com.google.android.gms.fonts", "com.google.android.gms.fonts",
@ -154,7 +162,7 @@ class VectorApplication :
R.array.com_google_android_gms_fonts_certs R.array.com_google_android_gms_fonts_certs
) )
FontsContractCompat.requestFont(this, fontRequest, emojiCompatFontProvider, getFontThreadHandler()) FontsContractCompat.requestFont(this, fontRequest, emojiCompatFontProvider, getFontThreadHandler())
VectorLocale.init(this) VectorLocale.init(this, buildMeta)
ThemeUtils.init(this) ThemeUtils.init(this)
vectorConfiguration.applyToApplicationContext() vectorConfiguration.applyToApplicationContext()
@ -183,7 +191,7 @@ class VectorApplication :
fcmHelper.onEnterBackground(activeSessionHolder) fcmHelper.onEnterBackground(activeSessionHolder)
} }
}) })
ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler) ProcessLifecycleOwner.get().lifecycle.addObserver(spaceStateHandler)
ProcessLifecycleOwner.get().lifecycle.addObserver(pinLocker) ProcessLifecycleOwner.get().lifecycle.addObserver(pinLocker)
ProcessLifecycleOwner.get().lifecycle.addObserver(callManager) ProcessLifecycleOwner.get().lifecycle.addObserver(callManager)
// This should be done as early as possible // This should be done as early as possible
@ -201,8 +209,18 @@ class VectorApplication :
Mapbox.getInstance(this) Mapbox.getInstance(this)
} }
private fun configureEpoxy() {
EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
Carousel.setDefaultGlobalSnapHelperFactory(object : Carousel.SnapHelperFactory() {
override fun buildSnapHelper(context: Context?): SnapHelper {
return GravitySnapHelper(Gravity.START)
}
})
}
private fun enableStrictModeIfNeeded() { private fun enableStrictModeIfNeeded() {
if (BuildConfig.ENABLE_STRICT_MODE_LOGS) { if (Config.ENABLE_STRICT_MODE_LOGS) {
StrictMode.setThreadPolicy( StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder() StrictMode.ThreadPolicy.Builder()
.detectAll() .detectAll()

View File

@ -0,0 +1,81 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.di
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import im.vector.app.BuildConfig
import im.vector.app.config.Analytics
import im.vector.app.config.Config
import im.vector.app.config.KeySharingStrategy
import im.vector.app.features.analytics.AnalyticsConfig
import im.vector.app.features.call.webrtc.VoipConfig
import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageConfig
import im.vector.app.features.location.LocationSharingConfig
import im.vector.app.features.raw.wellknown.CryptoConfig
@InstallIn(SingletonComponent::class)
@Module
object ConfigurationModule {
@Provides
fun providesAnalyticsConfig(): AnalyticsConfig {
// Schildi: always disable
val config: Analytics = if (true) Analytics.Disabled else when (BuildConfig.BUILD_TYPE) {
"debug" -> Config.DEBUG_ANALYTICS_CONFIG
"nightly" -> Config.NIGHTLY_ANALYTICS_CONFIG
"release" -> Config.RELEASE_ANALYTICS_CONFIG
else -> throw IllegalStateException("Unhandled build type: ${BuildConfig.BUILD_TYPE}")
}
return when (config) {
Analytics.Disabled -> AnalyticsConfig(isEnabled = false, "", "", "")
is Analytics.PostHog -> AnalyticsConfig(
isEnabled = true,
postHogHost = config.postHogHost,
postHogApiKey = config.postHogApiKey,
policyLink = config.policyLink
)
}
}
@Provides
fun providesVoiceMessageConfig() = VoiceMessageConfig(
lengthLimitMs = Config.VOICE_MESSAGE_LIMIT_MS
)
@Provides
fun providesCryptoConfig() = CryptoConfig(
fallbackKeySharingStrategy = when (Config.KEY_SHARING_STRATEGY) {
KeySharingStrategy.WhenSendingEvent -> OutboundSessionKeySharingStrategy.WhenSendingEvent
KeySharingStrategy.WhenEnteringRoom -> OutboundSessionKeySharingStrategy.WhenSendingEvent
KeySharingStrategy.WhenTyping -> OutboundSessionKeySharingStrategy.WhenSendingEvent
}
)
@Provides
fun providesLocationSharingConfig() = LocationSharingConfig(
mapTilerKey = Config.LOCATION_MAP_TILER_KEY,
)
@Provides
fun providesVoipConfig() = VoipConfig(
handleCallAssertedIdentityEvents = Config.HANDLE_CALL_ASSERTED_IDENTITY_EVENTS
)
}

View File

@ -57,14 +57,15 @@ import im.vector.app.features.discovery.change.SetIdentityServerFragment
import im.vector.app.features.home.HomeDetailFragment import im.vector.app.features.home.HomeDetailFragment
import im.vector.app.features.home.HomeDrawerFragment import im.vector.app.features.home.HomeDrawerFragment
import im.vector.app.features.home.LoadingFragment import im.vector.app.features.home.LoadingFragment
import im.vector.app.features.home.NewHomeDetailFragment
import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsFragment import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsFragment
import im.vector.app.features.home.room.detail.TimelineFragment import im.vector.app.features.home.room.detail.TimelineFragment
import im.vector.app.features.home.room.detail.search.SearchFragment import im.vector.app.features.home.room.detail.search.SearchFragment
import im.vector.app.features.home.room.list.RoomListFragment import im.vector.app.features.home.room.list.RoomListFragment
import im.vector.app.features.home.room.list.home.HomeRoomListFragment import im.vector.app.features.home.room.list.home.HomeRoomListFragment
import im.vector.app.features.home.room.threads.list.views.ThreadListFragment import im.vector.app.features.home.room.threads.list.views.ThreadListFragment
import im.vector.app.features.location.LocationPreviewFragment
import im.vector.app.features.location.LocationSharingFragment import im.vector.app.features.location.LocationSharingFragment
import im.vector.app.features.location.preview.LocationPreviewFragment
import im.vector.app.features.login.LoginCaptchaFragment import im.vector.app.features.login.LoginCaptchaFragment
import im.vector.app.features.login.LoginFragment import im.vector.app.features.login.LoginFragment
import im.vector.app.features.login.LoginGenericTextInputFormFragment import im.vector.app.features.login.LoginGenericTextInputFormFragment
@ -260,6 +261,11 @@ interface FragmentModule {
@FragmentKey(HomeDetailFragment::class) @FragmentKey(HomeDetailFragment::class)
fun bindHomeDetailFragment(fragment: HomeDetailFragment): Fragment fun bindHomeDetailFragment(fragment: HomeDetailFragment): Fragment
@Binds
@IntoMap
@FragmentKey(NewHomeDetailFragment::class)
fun bindNewHomeDetailFragment(fragment: NewHomeDetailFragment): Fragment
@Binds @Binds
@IntoMap @IntoMap
@FragmentKey(EmojiSearchResultFragment::class) @FragmentKey(EmojiSearchResultFragment::class)

View File

@ -55,7 +55,8 @@ import im.vector.app.features.home.room.list.home.HomeRoomListViewModel
import im.vector.app.features.homeserver.HomeServerCapabilitiesViewModel import im.vector.app.features.homeserver.HomeServerCapabilitiesViewModel
import im.vector.app.features.invite.InviteUsersToRoomViewModel import im.vector.app.features.invite.InviteUsersToRoomViewModel
import im.vector.app.features.location.LocationSharingViewModel import im.vector.app.features.location.LocationSharingViewModel
import im.vector.app.features.location.live.map.LocationLiveMapViewModel import im.vector.app.features.location.live.map.LiveLocationMapViewModel
import im.vector.app.features.location.preview.LocationPreviewViewModel
import im.vector.app.features.login.LoginViewModel import im.vector.app.features.login.LoginViewModel
import im.vector.app.features.login2.LoginViewModel2 import im.vector.app.features.login2.LoginViewModel2
import im.vector.app.features.login2.created.AccountCreatedViewModel import im.vector.app.features.login2.created.AccountCreatedViewModel
@ -605,6 +606,11 @@ interface MavericksViewModelModule {
@MavericksViewModelKey(LocationSharingViewModel::class) @MavericksViewModelKey(LocationSharingViewModel::class)
fun createLocationSharingViewModelFactory(factory: LocationSharingViewModel.Factory): MavericksAssistedViewModelFactory<*, *> fun createLocationSharingViewModelFactory(factory: LocationSharingViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(LocationPreviewViewModel::class)
fun createLocationPreviewViewModelFactory(factory: LocationPreviewViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds @Binds
@IntoMap @IntoMap
@MavericksViewModelKey(VectorAttachmentViewerViewModel::class) @MavericksViewModelKey(VectorAttachmentViewerViewModel::class)
@ -612,8 +618,8 @@ interface MavericksViewModelModule {
@Binds @Binds
@IntoMap @IntoMap
@MavericksViewModelKey(LocationLiveMapViewModel::class) @MavericksViewModelKey(LiveLocationMapViewModel::class)
fun locationLiveMapViewModelFactory(factory: LocationLiveMapViewModel.Factory): MavericksAssistedViewModelFactory<*, *> fun liveLocationMapViewModelFactory(factory: LiveLocationMapViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds @Binds
@IntoMap @IntoMap

View File

@ -31,7 +31,9 @@ import dagger.hilt.components.SingletonComponent
import im.vector.app.BuildConfig import im.vector.app.BuildConfig
import im.vector.app.EmojiCompatWrapper import im.vector.app.EmojiCompatWrapper
import im.vector.app.EmojiSpanify import im.vector.app.EmojiSpanify
import im.vector.app.config.analyticsConfig import im.vector.app.SpaceStateHandler
import im.vector.app.SpaceStateHandlerImpl
import im.vector.app.config.Config
import im.vector.app.core.dispatchers.CoroutineDispatchers import im.vector.app.core.dispatchers.CoroutineDispatchers
import im.vector.app.core.error.DefaultErrorFormatter import im.vector.app.core.error.DefaultErrorFormatter
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
@ -40,7 +42,6 @@ import im.vector.app.core.time.Clock
import im.vector.app.core.time.DefaultClock import im.vector.app.core.time.DefaultClock
import im.vector.app.core.utils.AndroidSystemSettingsProvider import im.vector.app.core.utils.AndroidSystemSettingsProvider
import im.vector.app.core.utils.SystemSettingsProvider import im.vector.app.core.utils.SystemSettingsProvider
import im.vector.app.features.analytics.AnalyticsConfig
import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.analytics.VectorAnalytics
import im.vector.app.features.analytics.impl.DefaultVectorAnalytics import im.vector.app.features.analytics.impl.DefaultVectorAnalytics
@ -108,6 +109,9 @@ abstract class VectorBindModule {
@Binds @Binds
abstract fun bindSystemSettingsProvide(provider: AndroidSystemSettingsProvider): SystemSettingsProvider abstract fun bindSystemSettingsProvide(provider: AndroidSystemSettingsProvider): SystemSettingsProvider
@Binds
abstract fun bindSpaceStateHandler(spaceStateHandlerImpl: SpaceStateHandlerImpl): SpaceStateHandler
} }
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
@ -200,17 +204,23 @@ object VectorStaticModule {
return GlobalScope return GlobalScope
} }
@Provides
fun providesAnalyticsConfig(): AnalyticsConfig {
return analyticsConfig
}
@Provides @Provides
fun providesPhoneNumberUtil(): PhoneNumberUtil = PhoneNumberUtil.getInstance() fun providesPhoneNumberUtil(): PhoneNumberUtil = PhoneNumberUtil.getInstance()
@Provides @Provides
@Singleton @Singleton
fun providesBuildMeta() = BuildMeta() fun providesBuildMeta() = BuildMeta(
isDebug = BuildConfig.DEBUG,
applicationId = BuildConfig.APPLICATION_ID,
lowPrivacyLoggingEnabled = Config.LOW_PRIVACY_LOG_ENABLE,
versionName = BuildConfig.VERSION_NAME,
gitRevision = BuildConfig.GIT_REVISION,
gitRevisionDate = BuildConfig.GIT_REVISION_DATE,
gitBranchName = BuildConfig.GIT_BRANCH_NAME,
buildNumber = BuildConfig.BUILD_NUMBER,
flavorDescription = BuildConfig.FLAVOR_DESCRIPTION,
flavorShortDescription = BuildConfig.SHORT_FLAVOR_DESCRIPTION,
)
@Provides @Provides
@Singleton @Singleton

View File

@ -37,7 +37,7 @@ import androidx.datastore.preferences.core.Preferences
import dagger.hilt.EntryPoints import dagger.hilt.EntryPoints
import im.vector.app.core.datastore.dataStoreProvider import im.vector.app.core.datastore.dataStoreProvider
import im.vector.app.core.di.SingletonEntryPoint import im.vector.app.core.di.SingletonEntryPoint
import im.vector.app.core.resources.BuildMeta import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
import java.io.OutputStream import java.io.OutputStream
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -93,9 +93,9 @@ fun Context.safeOpenOutputStream(uri: Uri): OutputStream? {
*/ */
@Suppress("deprecation") @Suppress("deprecation")
@SuppressLint("NewApi") // false positive @SuppressLint("NewApi") // false positive
fun Context.inferNoConnectivity(buildMeta: BuildMeta): Boolean { fun Context.inferNoConnectivity(sdkIntProvider: BuildVersionSdkIntProvider): Boolean {
val connectivityManager = getSystemService<ConnectivityManager>()!! val connectivityManager = getSystemService<ConnectivityManager>()!!
return if (buildMeta.sdkInt > Build.VERSION_CODES.M) { return if (sdkIntProvider.get() > Build.VERSION_CODES.M) {
val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
when { when {
networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true -> false networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true -> false

View File

@ -28,7 +28,7 @@ import im.vector.app.R
import im.vector.app.core.platform.SimpleTextWatcher import im.vector.app.core.platform.SimpleTextWatcher
fun EditText.setupAsSearch( fun EditText.setupAsSearch(
@DrawableRes searchIconRes: Int = R.drawable.ic_search, @DrawableRes searchIconRes: Int = R.drawable.ic_home_search,
@DrawableRes clearIconRes: Int = R.drawable.ic_x_gray @DrawableRes clearIconRes: Int = R.drawable.ic_x_gray
) { ) {
addTextChangedListener(object : SimpleTextWatcher() { addTextChangedListener(object : SimpleTextWatcher() {

View File

@ -78,10 +78,14 @@ fun TextInputLayout.setOnImeDoneListener(action: () -> Unit) {
} }
} }
fun TextInputLayout.setOnFocusLostListener(action: () -> Unit) { /**
* Set a listener for when the input has lost focus, such as moving to the another input field.
* The listener is only called when the view is in a resumed state to avoid triggers when exiting a screen.
*/
fun TextInputLayout.setOnFocusLostListener(lifecycleOwner: LifecycleOwner, action: () -> Unit) {
editText().setOnFocusChangeListener { _, hasFocus -> editText().setOnFocusChangeListener { _, hasFocus ->
when (hasFocus) { when (hasFocus) {
false -> action() false -> lifecycleOwner.lifecycleScope.launchWhenResumed { action() }
else -> { else -> {
// do nothing // do nothing
} }

View File

@ -50,10 +50,10 @@ import androidx.viewbinding.ViewBinding
import com.airbnb.mvrx.MavericksView import com.airbnb.mvrx.MavericksView
import com.bumptech.glide.util.Util import com.bumptech.glide.util.Util
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.EntryPointAccessors import dagger.hilt.android.EntryPointAccessors
import im.vector.app.BuildConfig
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.ActivityEntryPoint import im.vector.app.core.di.ActivityEntryPoint
@ -67,11 +67,13 @@ import im.vector.app.core.extensions.restart
import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.extensions.singletonEntryPoint
import im.vector.app.core.extensions.toMvRxBundle import im.vector.app.core.extensions.toMvRxBundle
import im.vector.app.core.resources.BuildMeta
import im.vector.app.core.utils.AndroidSystemSettingsProvider import im.vector.app.core.utils.AndroidSystemSettingsProvider
import im.vector.app.core.utils.ToolbarConfig import im.vector.app.core.utils.ToolbarConfig
import im.vector.app.core.utils.toast import im.vector.app.core.utils.toast
import im.vector.app.features.MainActivity import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs import im.vector.app.features.MainActivityArgs
import im.vector.app.features.VectorFeatures
import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.configuration.VectorConfiguration import im.vector.app.features.configuration.VectorConfiguration
@ -155,11 +157,12 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
protected lateinit var bugReporter: BugReporter protected lateinit var bugReporter: BugReporter
private lateinit var pinLocker: PinLocker private lateinit var pinLocker: PinLocker
@Inject @Inject lateinit var rageShake: RageShake
lateinit var rageShake: RageShake @Inject lateinit var buildMeta: BuildMeta
@Inject lateinit var fontScalePreferences: FontScalePreferences
@Inject @Inject
lateinit var fontScalePreferences: FontScalePreferences lateinit var vectorFeatures: VectorFeatures
lateinit var navigator: Navigator lateinit var navigator: Navigator
private set private set
@ -253,6 +256,14 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
initUiAndData() initUiAndData()
if (vectorFeatures.isNewAppLayoutEnabled()) {
tryOrNull { // Add to XML theme when feature flag is removed
val toolbarBackground = MaterialColors.getColor(views.root, R.attr.vctr_toolbar_background)
window.statusBarColor = toolbarBackground
window.navigationBarColor = toolbarBackground
}
}
val titleRes = getTitleRes() val titleRes = getTitleRes()
if (titleRes != -1) { if (titleRes != -1) {
supportActionBar?.let { supportActionBar?.let {
@ -409,7 +420,7 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
} }
DebugReceiver DebugReceiver
.getIntentFilter(this) .getIntentFilter(this)
.takeIf { BuildConfig.DEBUG } .takeIf { buildMeta.isDebug }
?.let { ?.let {
debugReceiver = DebugReceiver() debugReceiver = DebugReceiver()
registerReceiver(debugReceiver, it) registerReceiver(debugReceiver, it)

View File

@ -25,14 +25,14 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.BuildConfig
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.network.WifiDetector import im.vector.app.core.network.WifiDetector
import im.vector.app.core.pushers.model.PushData import im.vector.app.core.pushers.model.PushData
import im.vector.app.core.resources.BuildMeta
import im.vector.app.core.services.GuardServiceStarter import im.vector.app.core.services.GuardServiceStarter
import im.vector.app.features.notifications.NotifiableEventResolver import im.vector.app.features.notifications.NotifiableEventResolver
import im.vector.app.features.notifications.NotificationActionIds
import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.notifications.NotificationDrawerManager
import im.vector.app.features.notifications.NotificationUtils
import im.vector.app.features.settings.BackgroundSyncMode import im.vector.app.features.settings.BackgroundSyncMode
import im.vector.app.features.settings.VectorDataStore import im.vector.app.features.settings.VectorDataStore
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
@ -68,6 +68,8 @@ class VectorMessagingReceiver : MessagingReceiver() {
@Inject lateinit var unifiedPushHelper: UnifiedPushHelper @Inject lateinit var unifiedPushHelper: UnifiedPushHelper
@Inject lateinit var unifiedPushStore: UnifiedPushStore @Inject lateinit var unifiedPushStore: UnifiedPushStore
@Inject lateinit var pushParser: PushParser @Inject lateinit var pushParser: PushParser
@Inject lateinit var actionIds: NotificationActionIds
@Inject lateinit var buildMeta: BuildMeta
private val coroutineScope = CoroutineScope(SupervisorJob()) private val coroutineScope = CoroutineScope(SupervisorJob())
@ -87,7 +89,7 @@ class VectorMessagingReceiver : MessagingReceiver() {
Timber.tag(loggerTag.value).d("## onMessage() received") Timber.tag(loggerTag.value).d("## onMessage() received")
val sMessage = String(message) val sMessage = String(message)
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { if (buildMeta.lowPrivacyLoggingEnabled) {
Timber.tag(loggerTag.value).d("## onMessage() $sMessage") Timber.tag(loggerTag.value).d("## onMessage() $sMessage")
} }
@ -100,7 +102,7 @@ class VectorMessagingReceiver : MessagingReceiver() {
// Diagnostic Push // Diagnostic Push
if (pushData.eventId == PushersManager.TEST_EVENT_ID) { if (pushData.eventId == PushersManager.TEST_EVENT_ID) {
val intent = Intent(NotificationUtils.PUSH_ACTION) val intent = Intent(actionIds.push)
LocalBroadcastManager.getInstance(context).sendBroadcast(intent) LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
return return
} }
@ -171,7 +173,7 @@ class VectorMessagingReceiver : MessagingReceiver() {
*/ */
private suspend fun onMessageReceivedInternal(pushData: PushData) { private suspend fun onMessageReceivedInternal(pushData: PushData) {
try { try {
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { if (buildMeta.lowPrivacyLoggingEnabled) {
Timber.tag(loggerTag.value).d("## onMessageReceivedInternal() : $pushData") Timber.tag(loggerTag.value).d("## onMessageReceivedInternal() : $pushData")
} else { } else {
Timber.tag(loggerTag.value).d("## onMessageReceivedInternal()") Timber.tag(loggerTag.value).d("## onMessageReceivedInternal()")

View File

@ -16,8 +16,15 @@
package im.vector.app.core.resources package im.vector.app.core.resources
import android.os.Build
data class BuildMeta( data class BuildMeta(
val sdkInt: Int = Build.VERSION.SDK_INT val isDebug: Boolean,
val applicationId: String,
val lowPrivacyLoggingEnabled: Boolean,
val versionName: String,
val gitRevision: String,
val gitRevisionDate: String,
val gitBranchName: String,
val buildNumber: String,
val flavorDescription: String,
val flavorShortDescription: String,
) )

View File

@ -39,7 +39,6 @@ import androidx.browser.customtabs.CustomTabsSession
import androidx.core.app.ShareCompat import androidx.core.app.ShareCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import im.vector.app.BuildConfig
import im.vector.app.R import im.vector.app.R
import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.notifications.NotificationUtils
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
@ -182,7 +181,7 @@ fun openUri(activity: Activity, uri: String) {
*/ */
fun openMedia(activity: Activity, savedMediaPath: String, mimeType: String) { fun openMedia(activity: Activity, savedMediaPath: String, mimeType: String) {
val file = File(savedMediaPath) val file = File(savedMediaPath)
val uri = FileProvider.getUriForFile(activity, BuildConfig.APPLICATION_ID + ".fileProvider", file) val uri = FileProvider.getUriForFile(activity, activity.packageName + ".fileProvider", file)
val intent = Intent(Intent.ACTION_VIEW).apply { val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(uri, mimeType) setDataAndType(uri, mimeType)
@ -214,7 +213,7 @@ fun openLocation(activity: Activity, latitude: Double, longitude: Double) {
fun shareMedia(context: Context, file: File, mediaMimeType: String?) { fun shareMedia(context: Context, file: File, mediaMimeType: String?) {
val mediaUri = try { val mediaUri = try {
FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileProvider", file) FileProvider.getUriForFile(context, context.packageName + ".fileProvider", file)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "onMediaAction Selected File cannot be shared") Timber.e(e, "onMediaAction Selected File cannot be shared")
return return
@ -376,7 +375,7 @@ private fun addToGallery(savedFile: File, mediaMimeType: String?, context: Conte
/** /**
* Open the play store to the provided application Id, default to this app. * Open the play store to the provided application Id, default to this app.
*/ */
fun openPlayStore(activity: Activity, appId: String = BuildConfig.APPLICATION_ID) { fun openPlayStore(activity: Activity, appId: String) {
try { try {
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$appId"))) activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$appId")))
} catch (activityNotFoundException: ActivityNotFoundException) { } catch (activityNotFoundException: ActivityNotFoundException) {

View File

@ -159,7 +159,7 @@ fun startInstallFromSourceIntent(context: Context, activityResultLauncher: Activ
} }
fun startSharePlainTextIntent( fun startSharePlainTextIntent(
fragment: Fragment, context: Context,
activityResultLauncher: ActivityResultLauncher<Intent>?, activityResultLauncher: ActivityResultLauncher<Intent>?,
chooserTitle: String?, chooserTitle: String?,
text: String, text: String,
@ -182,10 +182,10 @@ fun startSharePlainTextIntent(
if (activityResultLauncher != null) { if (activityResultLauncher != null) {
activityResultLauncher.launch(intent) activityResultLauncher.launch(intent)
} else { } else {
fragment.startActivity(intent) context.startActivity(intent)
} }
} catch (activityNotFoundException: ActivityNotFoundException) { } catch (activityNotFoundException: ActivityNotFoundException) {
fragment.activity?.toast(R.string.error_no_external_application_found) context.toast(R.string.error_no_external_application_found)
} }
} }

View File

@ -16,8 +16,8 @@
package im.vector.app.features package im.vector.app.features
import im.vector.app.BuildConfig
import im.vector.app.config.Config import im.vector.app.config.Config
import im.vector.app.config.OnboardingVariant
interface VectorFeatures { interface VectorFeatures {
@ -30,19 +30,14 @@ interface VectorFeatures {
fun isOnboardingCombinedLoginEnabled(): Boolean fun isOnboardingCombinedLoginEnabled(): Boolean
fun allowExternalUnifiedPushDistributors(): Boolean fun allowExternalUnifiedPushDistributors(): Boolean
fun isScreenSharingEnabled(): Boolean fun isScreenSharingEnabled(): Boolean
fun isLocationSharingEnabled(): Boolean
fun forceUsageOfOpusEncoder(): Boolean fun forceUsageOfOpusEncoder(): Boolean
fun shouldStartDmOnFirstMessage(): Boolean fun shouldStartDmOnFirstMessage(): Boolean
fun isNewAppLayoutEnabled(): Boolean fun isNewAppLayoutEnabled(): Boolean
enum class OnboardingVariant {
LEGACY,
LOGIN_2,
FTUE_AUTH
}
} }
class DefaultVectorFeatures : VectorFeatures { class DefaultVectorFeatures : VectorFeatures {
override fun onboardingVariant(): VectorFeatures.OnboardingVariant = BuildConfig.ONBOARDING_VARIANT override fun onboardingVariant() = Config.ONBOARDING_VARIANT
override fun isOnboardingAlreadyHaveAccountSplashEnabled() = true override fun isOnboardingAlreadyHaveAccountSplashEnabled() = true
override fun isOnboardingSplashCarouselEnabled() = true override fun isOnboardingSplashCarouselEnabled() = true
override fun isOnboardingUseCaseEnabled() = true override fun isOnboardingUseCaseEnabled() = true
@ -51,6 +46,7 @@ class DefaultVectorFeatures : VectorFeatures {
override fun isOnboardingCombinedLoginEnabled() = true override fun isOnboardingCombinedLoginEnabled() = true
override fun allowExternalUnifiedPushDistributors(): Boolean = Config.ALLOW_EXTERNAL_UNIFIED_PUSH_DISTRIBUTORS override fun allowExternalUnifiedPushDistributors(): Boolean = Config.ALLOW_EXTERNAL_UNIFIED_PUSH_DISTRIBUTORS
override fun isScreenSharingEnabled(): Boolean = true override fun isScreenSharingEnabled(): Boolean = true
override fun isLocationSharingEnabled() = Config.ENABLE_LOCATION_SHARING
override fun forceUsageOfOpusEncoder(): Boolean = false override fun forceUsageOfOpusEncoder(): Boolean = false
override fun shouldStartDmOnFirstMessage(): Boolean = false override fun shouldStartDmOnFirstMessage(): Boolean = false
override fun isNewAppLayoutEnabled(): Boolean = false override fun isNewAppLayoutEnabled(): Boolean = false

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2021 New Vector Ltd * Copyright (c) 2022 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,9 +16,9 @@
package im.vector.app.features.analytics package im.vector.app.features.analytics
interface AnalyticsConfig { data class AnalyticsConfig(
val isEnabled: Boolean val isEnabled: Boolean,
val postHogHost: String val postHogHost: String,
val postHogApiKey: String val postHogApiKey: String,
val policyLink: String val policyLink: String,
} )

View File

@ -18,11 +18,15 @@ package im.vector.app.features.analytics.impl
import android.content.Context import android.content.Context
import com.posthog.android.PostHog import com.posthog.android.PostHog
import im.vector.app.BuildConfig import im.vector.app.core.resources.BuildMeta
import im.vector.app.config.analyticsConfig import im.vector.app.features.analytics.AnalyticsConfig
import javax.inject.Inject import javax.inject.Inject
class PostHogFactory @Inject constructor(private val context: Context) { class PostHogFactory @Inject constructor(
private val context: Context,
private val analyticsConfig: AnalyticsConfig,
private val buildMeta: BuildMeta,
) {
fun createPosthog(): PostHog { fun createPosthog(): PostHog {
return PostHog.Builder(context, analyticsConfig.postHogApiKey, analyticsConfig.postHogHost) return PostHog.Builder(context, analyticsConfig.postHogApiKey, analyticsConfig.postHogHost)
@ -43,7 +47,7 @@ class PostHogFactory @Inject constructor(private val context: Context) {
} }
private fun getLogLevel(): PostHog.LogLevel { private fun getLogLevel(): PostHog.LogLevel {
return if (BuildConfig.DEBUG) { return if (buildMeta.isDebug) {
PostHog.LogLevel.DEBUG PostHog.LogLevel.DEBUG
} else { } else {
PostHog.LogLevel.INFO PostHog.LogLevel.INFO

View File

@ -29,6 +29,9 @@ import kotlinx.coroutines.flow.map
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import javax.inject.Inject import javax.inject.Inject
/**
* Also accessed via reflection by the instrumentation tests @see [im.vector.app.ClearCurrentSessionRule].
*/
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "vector_analytics") private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "vector_analytics")
/** /**

View File

@ -22,17 +22,17 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.activityViewModel
import im.vector.app.R import im.vector.app.R
import im.vector.app.config.analyticsConfig
import im.vector.app.core.extensions.setTextWithColoredPart import im.vector.app.core.extensions.setTextWithColoredPart
import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.OnBackPressed
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.openUrlInChromeCustomTab import im.vector.app.core.utils.openUrlInChromeCustomTab
import im.vector.app.databinding.FragmentAnalyticsOptinBinding import im.vector.app.databinding.FragmentAnalyticsOptinBinding
import im.vector.app.features.analytics.AnalyticsConfig
import javax.inject.Inject import javax.inject.Inject
class AnalyticsOptInFragment @Inject constructor() : class AnalyticsOptInFragment @Inject constructor(
VectorBaseFragment<FragmentAnalyticsOptinBinding>(), private val analyticsConfig: AnalyticsConfig,
OnBackPressed { ) : VectorBaseFragment<FragmentAnalyticsOptinBinding>(), OnBackPressed {
// Share the view model with the Activity so that the Activity // Share the view model with the Activity so that the Activity
// can decide what to do when the data has been saved // can decide what to do when the data has been saved

View File

@ -26,9 +26,9 @@ import androidx.activity.result.ActivityResultLauncher
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.dialogs.PhotoOrVideoDialog import im.vector.app.core.dialogs.PhotoOrVideoDialog
import im.vector.app.core.platform.Restorable import im.vector.app.core.platform.Restorable
import im.vector.app.core.resources.BuildMeta
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import im.vector.lib.multipicker.MultiPicker import im.vector.lib.multipicker.MultiPicker
import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import timber.log.Timber import timber.log.Timber
@ -38,15 +38,14 @@ private const val PENDING_TYPE_KEY = "PENDING_TYPE_KEY"
/** /**
* This class helps to handle attachments by providing simple methods. * This class helps to handle attachments by providing simple methods.
*/ */
class AttachmentsHelper(val context: Context, val callback: Callback) : Restorable { class AttachmentsHelper(
val context: Context,
val callback: Callback,
private val buildMeta: BuildMeta,
) : Restorable {
interface Callback { interface Callback {
fun onContactAttachmentReady(contactAttachment: ContactAttachment) { fun onContactAttachmentReady(contactAttachment: ContactAttachment)
if (BuildConfig.LOG_PRIVATE_DATA) {
Timber.v("On contact attachment ready: $contactAttachment")
}
}
fun onContentAttachmentsReady(attachments: List<ContentAttachmentData>) fun onContentAttachmentsReady(attachments: List<ContentAttachmentData>)
} }
@ -155,6 +154,9 @@ class AttachmentsHelper(val context: Context, val callback: Callback) : Restorab
.firstOrNull() .firstOrNull()
?.toContactAttachment() ?.toContactAttachment()
?.let { ?.let {
if (buildMeta.lowPrivacyLoggingEnabled) {
Timber.v("On contact attachment ready: $it")
}
callback.onContactAttachmentReady(it) callback.onContactAttachmentReady(it)
} }
} }

View File

@ -0,0 +1,21 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.call.webrtc
data class VoipConfig(
val handleCallAssertedIdentityEvents: Boolean
)

View File

@ -20,7 +20,6 @@ import android.content.Context
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import im.vector.app.ActiveSessionDataSource import im.vector.app.ActiveSessionDataSource
import im.vector.app.BuildConfig
import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.core.pushers.UnifiedPushHelper
import im.vector.app.core.services.CallAndroidService import im.vector.app.core.services.CallAndroidService
import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.AnalyticsTracker
@ -74,6 +73,7 @@ class WebRtcCallManager @Inject constructor(
private val activeSessionDataSource: ActiveSessionDataSource, private val activeSessionDataSource: ActiveSessionDataSource,
private val analyticsTracker: AnalyticsTracker, private val analyticsTracker: AnalyticsTracker,
private val unifiedPushHelper: UnifiedPushHelper, private val unifiedPushHelper: UnifiedPushHelper,
private val voipConfig: VoipConfig,
) : CallListener, ) : CallListener,
DefaultLifecycleObserver { DefaultLifecycleObserver {
@ -444,7 +444,7 @@ class WebRtcCallManager @Inject constructor(
} }
override fun onCallAssertedIdentityReceived(callAssertedIdentityContent: CallAssertedIdentityContent) { override fun onCallAssertedIdentityReceived(callAssertedIdentityContent: CallAssertedIdentityContent) {
if (!BuildConfig.handleCallAssertedIdentityEvents) { if (!voipConfig.handleCallAssertedIdentityEvents) {
return return
} }
val call = callsByCallId[callAssertedIdentityContent.callId] val call = callsByCallId[callAssertedIdentityContent.callId]

View File

@ -142,7 +142,7 @@ class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment<Fr
dialog.findViewById<View>(R.id.keys_backup_setup_share)?.debouncedClicks { dialog.findViewById<View>(R.id.keys_backup_setup_share)?.debouncedClicks {
startSharePlainTextIntent( startSharePlainTextIntent(
fragment = this, context = requireContext(),
activityResultLauncher = null, activityResultLauncher = null,
chooserTitle = context?.getString(R.string.keys_backup_setup_step3_share_intent_chooser_title), chooserTitle = context?.getString(R.string.keys_backup_setup_step3_share_intent_chooser_title),
text = recoveryKey, text = recoveryKey,

View File

@ -104,7 +104,7 @@ class BootstrapSaveRecoveryKeyFragment @Inject constructor(
?: return@withState ?: return@withState
startSharePlainTextIntent( startSharePlainTextIntent(
this, requireContext(),
copyStartForActivityResult, copyStartForActivityResult,
context?.getString(R.string.keys_backup_setup_step3_share_intent_chooser_title), context?.getString(R.string.keys_backup_setup_step3_share_intent_chooser_title),
recoveryKey, recoveryKey,

View File

@ -36,8 +36,8 @@ import com.airbnb.mvrx.viewModel
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import de.spiritcroc.menu.ArrayOptionsMenuHelper import de.spiritcroc.menu.ArrayOptionsMenuHelper
import im.vector.app.AppStateHandler
import im.vector.app.R import im.vector.app.R
import im.vector.app.SpaceStateHandler
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.registerStartForActivityResult
@ -49,6 +49,7 @@ import im.vector.app.core.platform.VectorMenuProvider
import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.PushersManager
import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.core.pushers.UnifiedPushHelper
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.utils.startSharePlainTextIntent
import im.vector.app.core.utils.toast import im.vector.app.core.utils.toast
import im.vector.app.databinding.ActivityHomeBinding import im.vector.app.databinding.ActivityHomeBinding
import im.vector.app.features.MainActivity import im.vector.app.features.MainActivity
@ -137,7 +138,7 @@ class HomeActivity :
@Inject lateinit var permalinkHandler: PermalinkHandler @Inject lateinit var permalinkHandler: PermalinkHandler
@Inject lateinit var avatarRenderer: AvatarRenderer @Inject lateinit var avatarRenderer: AvatarRenderer
@Inject lateinit var initSyncStepFormatter: InitSyncStepFormatter @Inject lateinit var initSyncStepFormatter: InitSyncStepFormatter
@Inject lateinit var appStateHandler: AppStateHandler @Inject lateinit var spaceStateHandler: SpaceStateHandler
@Inject lateinit var unifiedPushHelper: UnifiedPushHelper @Inject lateinit var unifiedPushHelper: UnifiedPushHelper
@Inject lateinit var fcmHelper: FcmHelper @Inject lateinit var fcmHelper: FcmHelper
@Inject lateinit var nightlyProxy: NightlyProxy @Inject lateinit var nightlyProxy: NightlyProxy
@ -210,11 +211,16 @@ class HomeActivity :
) )
} }
} }
sharedActionViewModel = viewModelProvider.get(HomeSharedActionViewModel::class.java) sharedActionViewModel = viewModelProvider[HomeSharedActionViewModel::class.java]
views.drawerLayout.addDrawerListener(drawerListener) views.drawerLayout.addDrawerListener(drawerListener)
if (isFirstCreation()) { if (isFirstCreation()) {
replaceFragment(views.homeDetailFragmentContainer, HomeDetailFragment::class.java) if (vectorFeatures.isNewAppLayoutEnabled()) {
replaceFragment(views.homeDrawerFragmentContainer, HomeDrawerFragment::class.java) views.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
replaceFragment(views.homeDetailFragmentContainer, NewHomeDetailFragment::class.java)
} else {
replaceFragment(views.homeDetailFragmentContainer, HomeDetailFragment::class.java)
replaceFragment(views.homeDrawerFragmentContainer, HomeDrawerFragment::class.java)
}
} }
sharedActionViewModel sharedActionViewModel
@ -568,7 +574,7 @@ class HomeActivity :
nightlyProxy.onHomeResumed() nightlyProxy.onHomeResumed()
} }
override fun getMenuRes() = R.menu.home override fun getMenuRes() = if (vectorFeatures.isNewAppLayoutEnabled()) R.menu.menu_new_home else R.menu.menu_home
override fun handlePrepareMenu(menu: Menu) { override fun handlePrepareMenu(menu: Menu) {
menu.findItem(R.id.menu_home_init_sync_legacy)?.isVisible = vectorPreferences.developerMode() menu.findItem(R.id.menu_home_init_sync_legacy)?.isVisible = vectorPreferences.developerMode()
@ -640,6 +646,10 @@ class HomeActivity :
navigator.openSettings(this) navigator.openSettings(this)
true true
} }
R.id.menu_home_invite_friends -> {
launchInviteFriends()
true
}
else -> false else -> false
} }
@ -649,6 +659,21 @@ class HomeActivity :
) )
} }
private fun launchInviteFriends() {
activeSessionHolder.getSafeActiveSession()?.permalinkService()?.createPermalink(sharedActionViewModel.session.myUserId)?.let { permalink ->
analyticsTracker.screen(MobileScreen(screenName = MobileScreen.ScreenName.InviteFriends))
val text = getString(R.string.invite_friends_text, permalink)
startSharePlainTextIntent(
context = this,
activityResultLauncher = null,
chooserTitle = getString(R.string.invite_friends),
text = text,
extraTitle = getString(R.string.invite_friends_rich_title)
)
}
}
override fun onBackPressed() { override fun onBackPressed() {
if (views.drawerLayout.isDrawerOpen(GravityCompat.START)) { if (views.drawerLayout.isDrawerOpen(GravityCompat.START)) {
views.drawerLayout.closeDrawer(GravityCompat.START) views.drawerLayout.closeDrawer(GravityCompat.START)

View File

@ -22,11 +22,11 @@ import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.config.analyticsConfig
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.analytics.AnalyticsConfig
import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.extensions.toAnalyticsType import im.vector.app.features.analytics.extensions.toAnalyticsType
import im.vector.app.features.analytics.plan.Signup import im.vector.app.features.analytics.plan.Signup
@ -80,7 +80,8 @@ class HomeActivityViewModel @AssistedInject constructor(
private val analyticsStore: AnalyticsStore, private val analyticsStore: AnalyticsStore,
private val lightweightSettingsStorage: LightweightSettingsStorage, private val lightweightSettingsStorage: LightweightSettingsStorage,
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
private val analyticsTracker: AnalyticsTracker private val analyticsTracker: AnalyticsTracker,
private val analyticsConfig: AnalyticsConfig,
) : VectorViewModel<HomeActivityViewState, HomeActivityViewActions, HomeActivityViewEvents>(initialState) { ) : VectorViewModel<HomeActivityViewState, HomeActivityViewActions, HomeActivityViewEvents>(initialState) {
@AssistedFactory @AssistedFactory

View File

@ -35,9 +35,9 @@ import com.google.android.material.badge.BadgeDrawable
import de.spiritcroc.matrixsdk.util.DbgUtil import de.spiritcroc.matrixsdk.util.DbgUtil
import de.spiritcroc.matrixsdk.util.Dimber import de.spiritcroc.matrixsdk.util.Dimber
import de.spiritcroc.viewpager.reduceDragSensitivity import de.spiritcroc.viewpager.reduceDragSensitivity
import im.vector.app.AppStateHandler
import im.vector.app.R import im.vector.app.R
import im.vector.app.SelectSpaceFrom import im.vector.app.SelectSpaceFrom
import im.vector.app.SpaceStateHandler
import im.vector.app.core.extensions.commitTransaction import im.vector.app.core.extensions.commitTransaction
import im.vector.app.core.extensions.restart import im.vector.app.core.extensions.restart
import im.vector.app.core.extensions.toMvRxBundle import im.vector.app.core.extensions.toMvRxBundle
@ -51,7 +51,6 @@ import im.vector.app.core.ui.views.CurrentCallsView
import im.vector.app.core.ui.views.CurrentCallsViewPresenter import im.vector.app.core.ui.views.CurrentCallsViewPresenter
import im.vector.app.core.ui.views.KeysBackupBanner import im.vector.app.core.ui.views.KeysBackupBanner
import im.vector.app.databinding.FragmentHomeDetailBinding import im.vector.app.databinding.FragmentHomeDetailBinding
import im.vector.app.features.VectorFeatures
import im.vector.app.features.call.SharedKnownCallsViewModel import im.vector.app.features.call.SharedKnownCallsViewModel
import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.VectorCallActivity
import im.vector.app.features.call.dialpad.DialPadFragment import im.vector.app.features.call.dialpad.DialPadFragment
@ -60,7 +59,6 @@ import im.vector.app.features.home.room.list.RoomListFragment
import im.vector.app.features.home.room.list.RoomListParams import im.vector.app.features.home.room.list.RoomListParams
import im.vector.app.features.home.room.list.RoomListSectionBuilder.Companion.SPACE_ID_FOLLOW_APP import im.vector.app.features.home.room.list.RoomListSectionBuilder.Companion.SPACE_ID_FOLLOW_APP
import im.vector.app.features.home.room.list.UnreadCounterBadgeView import im.vector.app.features.home.room.list.UnreadCounterBadgeView
import im.vector.app.features.home.room.list.home.HomeRoomListFragment
import im.vector.app.features.popup.PopupAlertManager import im.vector.app.features.popup.PopupAlertManager
import im.vector.app.features.popup.VerificationVectorAlert import im.vector.app.features.popup.VerificationVectorAlert
import im.vector.app.features.settings.VectorLocale import im.vector.app.features.settings.VectorLocale
@ -81,8 +79,7 @@ class HomeDetailFragment @Inject constructor(
private val alertManager: PopupAlertManager, private val alertManager: PopupAlertManager,
private val callManager: WebRtcCallManager, private val callManager: WebRtcCallManager,
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
private val appStateHandler: AppStateHandler, private val spaceStateHandler: SpaceStateHandler,
private val vectorFeatures: VectorFeatures,
) : VectorBaseFragment<FragmentHomeDetailBinding>(), ) : VectorBaseFragment<FragmentHomeDetailBinding>(),
KeysBackupBanner.Delegate, KeysBackupBanner.Delegate,
CurrentCallsView.Callback, CurrentCallsView.Callback,
@ -157,7 +154,7 @@ class HomeDetailFragment @Inject constructor(
// Reduce sensitivity of viewpager to avoid scrolling horizontally by accident too easily // Reduce sensitivity of viewpager to avoid scrolling horizontally by accident too easily
views.roomListContainerPager.reduceDragSensitivity(4) views.roomListContainerPager.reduceDragSensitivity(4)
// space pager: update appStateHandler's current page to update rest of the UI accordingly // space pager: update spaceStateHandler's current page to update rest of the UI accordingly
views.roomListContainerPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { views.roomListContainerPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) {
viewPagerDimber.i{"Home pager: selected page $position $initialPageSelected"} viewPagerDimber.i{"Home pager: selected page $position $initialPageSelected"}
@ -251,7 +248,7 @@ class HomeDetailFragment @Inject constructor(
private fun selectSpaceFromSwipe(position: Int) { private fun selectSpaceFromSwipe(position: Int) {
val selectedId = getSpaceIdForPageIndex(position) val selectedId = getSpaceIdForPageIndex(position)
appStateHandler.setCurrentSpace(selectedId, from = SelectSpaceFrom.SWIPE) spaceStateHandler.setCurrentSpace(selectedId, from = SelectSpaceFrom.SWIPE)
if (pagerPagingEnabled) { if (pagerPagingEnabled) {
onSpaceChange( onSpaceChange(
selectedId?.let { viewModel.getRoom(it)?.roomSummary() } selectedId?.let { viewModel.getRoom(it)?.roomSummary() }
@ -260,13 +257,13 @@ class HomeDetailFragment @Inject constructor(
} }
private fun navigateBack() { private fun navigateBack() {
val previousSpaceId = appStateHandler.getSpaceBackstack().removeLastOrNull() val previousSpaceId = spaceStateHandler.getSpaceBackstack().removeLastOrNull()
val parentSpaceId = appStateHandler.getCurrentSpace()?.flattenParentIds?.lastOrNull() val parentSpaceId = spaceStateHandler.getCurrentSpace()?.flattenParentIds?.lastOrNull()
setCurrentSpace(previousSpaceId ?: parentSpaceId) setCurrentSpace(previousSpaceId ?: parentSpaceId)
} }
private fun setCurrentSpace(spaceId: String?) { private fun setCurrentSpace(spaceId: String?) {
appStateHandler.setCurrentSpace(spaceId, isForwardNavigation = false) spaceStateHandler.setCurrentSpace(spaceId, isForwardNavigation = false)
sharedActionViewModel.post(HomeActivitySharedAction.OnCloseSpace) sharedActionViewModel.post(HomeActivitySharedAction.OnCloseSpace)
} }
@ -298,13 +295,13 @@ class HomeDetailFragment @Inject constructor(
private fun refreshSpaceState() { private fun refreshSpaceState() {
/* Upstream impl without care for viewPager /* Upstream impl without care for viewPager
appStateHandler.getCurrentSpace()?.let { spaceStateHandler.getCurrentSpace()?.let {
onSpaceChange(it) onSpaceChange(it)
} }
*/ */
// Current space/group is not live so at least refresh toolbar on resume // Current space/group is not live so at least refresh toolbar on resume
appStateHandler.getCurrentSpace()?.let { currentSpace -> spaceStateHandler.getCurrentSpace()?.let { currentSpace ->
// While paging is enabled, make title follow the view pager directly // While paging is enabled, make title follow the view pager directly
if (pagerPagingEnabled) { if (pagerPagingEnabled) {
return@let return@let
@ -317,7 +314,7 @@ class HomeDetailFragment @Inject constructor(
super.onPause() super.onPause()
// Persist swiped // Persist swiped
appStateHandler.persistSelectedSpace() spaceStateHandler.persistSelectedSpace()
} }
private fun checkNotificationTabStatus(enableDialPad: Boolean? = null) { private fun checkNotificationTabStatus(enableDialPad: Boolean? = null) {
@ -513,12 +510,8 @@ class HomeDetailFragment @Inject constructor(
if (fragmentToShow == null) { if (fragmentToShow == null) {
when (tab) { when (tab) {
is HomeTab.RoomList -> { is HomeTab.RoomList -> {
if (vectorFeatures.isNewAppLayoutEnabled()) { val params = RoomListParams(tab.displayMode)
add(R.id.roomListContainer, HomeRoomListFragment::class.java, null, fragmentTag) add(R.id.roomListContainer, RoomListFragment::class.java, params.toMvRxBundle(), fragmentTag)
} else {
val params = RoomListParams(tab.displayMode)
add(R.id.roomListContainer, RoomListFragment::class.java, params.toMvRxBundle(), fragmentTag)
}
} }
is HomeTab.DialPad -> { is HomeTab.DialPad -> {
add(R.id.roomListContainer, createDialPadFragment(), fragmentTag) add(R.id.roomListContainer, createDialPadFragment(), fragmentTag)
@ -601,7 +594,7 @@ class HomeDetailFragment @Inject constructor(
} }
} }
// In case the last space change was caused by swiping, we don't want to lose it // In case the last space change was caused by swiping, we don't want to lose it
appStateHandler.persistSelectedSpace() spaceStateHandler.persistSelectedSpace()
pagerSpaces = safeSpaces pagerSpaces = safeSpaces
pagerTab = tab pagerTab = tab
pagerPagingEnabled = pagingEnabled pagerPagingEnabled = pagingEnabled
@ -796,7 +789,7 @@ class HomeDetailFragment @Inject constructor(
return this return this
} }
override fun onBackPressed(toolbarButton: Boolean) = if (vectorPreferences.spaceBackNavigation() && appStateHandler.getCurrentSpace() != null) { override fun onBackPressed(toolbarButton: Boolean) = if (vectorPreferences.spaceBackNavigation() && spaceStateHandler.getCurrentSpace() != null) {
navigateBack() navigateBack()
true true
} else { } else {

View File

@ -22,7 +22,7 @@ import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.AppStateHandler import im.vector.app.SpaceStateHandler
import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.extensions.singletonEntryPoint
@ -76,7 +76,7 @@ class HomeDetailViewModel @AssistedInject constructor(
private val vectorDataStore: VectorDataStore, private val vectorDataStore: VectorDataStore,
private val callManager: WebRtcCallManager, private val callManager: WebRtcCallManager,
private val directRoomHelper: DirectRoomHelper, private val directRoomHelper: DirectRoomHelper,
private val appStateHandler: AppStateHandler, private val spaceStateHandler: SpaceStateHandler,
private val autoAcceptInvites: AutoAcceptInvites, private val autoAcceptInvites: AutoAcceptInvites,
private val vectorOverrides: VectorOverrides private val vectorOverrides: VectorOverrides
) : VectorViewModel<HomeDetailViewState, HomeDetailAction, HomeDetailViewEvents>(initialState), ) : VectorViewModel<HomeDetailViewState, HomeDetailAction, HomeDetailViewEvents>(initialState),
@ -215,13 +215,13 @@ class HomeDetailViewModel @AssistedInject constructor(
} }
private fun observeSelectedSpace() { private fun observeSelectedSpace() {
appStateHandler.selectedSpaceFlow spaceStateHandler.getSelectedSpaceFlow()
.setOnEach { .setOnEach {
copy( copy(
selectedSpace = it.orNull() selectedSpace = it.orNull()
) )
} }
appStateHandler.selectedSpaceFlowIgnoreSwipe spaceStateHandler.getSelectedSpaceFlowIgnoreSwipe()
.setOnEach { .setOnEach {
copy( copy(
selectedSpaceIgnoreSwipe = it.orNull() selectedSpaceIgnoreSwipe = it.orNull()
@ -230,7 +230,7 @@ class HomeDetailViewModel @AssistedInject constructor(
} }
private fun observeRoomSummaries() { private fun observeRoomSummaries() {
appStateHandler.selectedSpaceFlow.distinctUntilChanged().flatMapLatest { spaceStateHandler.getSelectedSpaceFlow().distinctUntilChanged().flatMapLatest {
// we use it as a trigger to all changes in room, but do not really load // we use it as a trigger to all changes in room, but do not really load
// the actual models // the actual models
session.roomService().getPagedRoomSummariesLive( session.roomService().getPagedRoomSummariesLive(
@ -242,7 +242,7 @@ class HomeDetailViewModel @AssistedInject constructor(
} }
.throttleFirst(300) .throttleFirst(300)
.onEach { .onEach {
val activeSpaceRoomId = appStateHandler.getCurrentSpace()?.roomId val activeSpaceRoomId = spaceStateHandler.getCurrentSpace()?.roomId
var dmInvites = 0 var dmInvites = 0
var roomsInvite = 0 var roomsInvite = 0
if (autoAcceptInvites.showInvites()) { if (autoAcceptInvites.showInvites()) {

View File

@ -23,11 +23,11 @@ import android.view.ViewGroup
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import im.vector.app.BuildConfig
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.observeK import im.vector.app.core.extensions.observeK
import im.vector.app.core.extensions.replaceChildFragment import im.vector.app.core.extensions.replaceChildFragment
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.BuildMeta
import im.vector.app.core.utils.startSharePlainTextIntent import im.vector.app.core.utils.startSharePlainTextIntent
import im.vector.app.databinding.FragmentHomeDrawerBinding import im.vector.app.databinding.FragmentHomeDrawerBinding
import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.analytics.plan.MobileScreen
@ -44,7 +44,8 @@ import javax.inject.Inject
class HomeDrawerFragment @Inject constructor( class HomeDrawerFragment @Inject constructor(
private val session: Session, private val session: Session,
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
private val avatarRenderer: AvatarRenderer private val avatarRenderer: AvatarRenderer,
private val buildMeta: BuildMeta,
) : VectorBaseFragment<FragmentHomeDrawerBinding>() { ) : VectorBaseFragment<FragmentHomeDrawerBinding>() {
private lateinit var sharedActionViewModel: HomeSharedActionViewModel private lateinit var sharedActionViewModel: HomeSharedActionViewModel
@ -103,7 +104,7 @@ class HomeDrawerFragment @Inject constructor(
val text = getString(R.string.invite_friends_text, permalink) val text = getString(R.string.invite_friends_text, permalink)
startSharePlainTextIntent( startSharePlainTextIntent(
fragment = this, context = requireContext(),
activityResultLauncher = null, activityResultLauncher = null,
chooserTitle = getString(R.string.invite_friends), chooserTitle = getString(R.string.invite_friends),
text = text, text = text,
@ -113,7 +114,7 @@ class HomeDrawerFragment @Inject constructor(
} }
// Debug menu // Debug menu
views.homeDrawerHeaderDebugView.isVisible = BuildConfig.DEBUG && vectorPreferences.developerMode() views.homeDrawerHeaderDebugView.isVisible = buildMeta.isDebug && vectorPreferences.developerMode()
views.homeDrawerHeaderDebugView.debouncedClicks { views.homeDrawerHeaderDebugView.debouncedClicks {
sharedActionViewModel.post(HomeActivitySharedAction.CloseDrawer) sharedActionViewModel.post(HomeActivitySharedAction.CloseDrawer)
navigator.openDebug(requireActivity()) navigator.openDebug(requireActivity())

View File

@ -0,0 +1,459 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.google.android.material.badge.BadgeDrawable
import im.vector.app.R
import im.vector.app.SpaceStateHandler
import im.vector.app.core.extensions.commitTransaction
import im.vector.app.core.platform.OnBackPressed
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.platform.VectorMenuProvider
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.ui.views.CurrentCallsView
import im.vector.app.core.ui.views.CurrentCallsViewPresenter
import im.vector.app.core.ui.views.KeysBackupBanner
import im.vector.app.databinding.FragmentNewHomeDetailBinding
import im.vector.app.features.call.SharedKnownCallsViewModel
import im.vector.app.features.call.VectorCallActivity
import im.vector.app.features.call.dialpad.DialPadFragment
import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.home.room.list.home.HomeRoomListFragment
import im.vector.app.features.popup.PopupAlertManager
import im.vector.app.features.popup.VerificationVectorAlert
import im.vector.app.features.settings.VectorLocale
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS
import im.vector.app.features.themes.ThemeUtils
import im.vector.app.features.workers.signout.BannerState
import im.vector.app.features.workers.signout.ServerBackupStatusViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class NewHomeDetailFragment @Inject constructor(
private val avatarRenderer: AvatarRenderer,
private val colorProvider: ColorProvider,
private val alertManager: PopupAlertManager,
private val callManager: WebRtcCallManager,
private val vectorPreferences: VectorPreferences,
private val appStateHandler: SpaceStateHandler,
private val session: Session,
) : VectorBaseFragment<FragmentNewHomeDetailBinding>(),
KeysBackupBanner.Delegate,
CurrentCallsView.Callback,
OnBackPressed,
VectorMenuProvider {
private val viewModel: HomeDetailViewModel by fragmentViewModel()
private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel()
private val unreadMessagesSharedViewModel: UnreadMessagesSharedViewModel by activityViewModel()
private val serverBackupStatusViewModel: ServerBackupStatusViewModel by activityViewModel()
private lateinit var sharedActionViewModel: HomeSharedActionViewModel
private lateinit var sharedCallActionViewModel: SharedKnownCallsViewModel
private var hasUnreadRooms = false
set(value) {
if (value != field) {
field = value
invalidateOptionsMenu()
}
}
override fun getMenuRes() = R.menu.room_list
override fun handleMenuItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.menu_home_mark_all_as_read -> {
viewModel.handle(HomeDetailAction.MarkAllRoomsRead)
true
}
else -> false
}
}
override fun handlePrepareMenu(menu: Menu) {
withState(viewModel) { state ->
val isRoomList = state.currentTab is HomeTab.RoomList
menu.findItem(R.id.menu_home_mark_all_as_read).isVisible = isRoomList && hasUnreadRooms
}
}
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentNewHomeDetailBinding {
return FragmentNewHomeDetailBinding.inflate(inflater, container, false)
}
private val currentCallsViewPresenter = CurrentCallsViewPresenter()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sharedActionViewModel = activityViewModelProvider.get(HomeSharedActionViewModel::class.java)
sharedCallActionViewModel = activityViewModelProvider.get(SharedKnownCallsViewModel::class.java)
setupBottomNavigationView()
setupToolbar()
setupKeysBackupBanner()
setupActiveCallView()
withState(viewModel) {
// Update the navigation view if needed (for when we restore the tabs)
views.bottomNavigationView.selectedItemId = it.currentTab.toMenuId()
}
viewModel.onEach(HomeDetailViewState::selectedSpace) { selectedSpace ->
onSpaceChange(selectedSpace)
}
viewModel.onEach(HomeDetailViewState::currentTab) { currentTab ->
updateUIForTab(currentTab)
}
viewModel.onEach(HomeDetailViewState::showDialPadTab) { showDialPadTab ->
updateTabVisibilitySafely(R.id.bottom_action_dial_pad, showDialPadTab)
}
viewModel.observeViewEvents { viewEvent ->
when (viewEvent) {
HomeDetailViewEvents.CallStarted -> handleCallStarted()
is HomeDetailViewEvents.FailToCall -> showFailure(viewEvent.failure)
HomeDetailViewEvents.Loading -> showLoadingDialog()
}
}
unknownDeviceDetectorSharedViewModel.onEach { state ->
state.unknownSessions.invoke()?.let { unknownDevices ->
if (unknownDevices.firstOrNull()?.currentSessionTrust == true) {
val uid = "review_login"
alertManager.cancelAlert(uid)
val olderUnverified = unknownDevices.filter { !it.isNew }
val newest = unknownDevices.firstOrNull { it.isNew }?.deviceInfo
if (newest != null) {
promptForNewUnknownDevices(uid, state, newest)
} else if (olderUnverified.isNotEmpty()) {
// In this case we prompt to go to settings to review logins
promptToReviewChanges(uid, state, olderUnverified.map { it.deviceInfo })
}
}
}
}
sharedCallActionViewModel
.liveKnownCalls
.observe(viewLifecycleOwner) {
currentCallsViewPresenter.updateCall(callManager.getCurrentCall(), callManager.getCalls())
invalidateOptionsMenu()
}
}
private fun navigateBack() {
val previousSpaceId = appStateHandler.getSpaceBackstack().removeLastOrNull()
val parentSpaceId = appStateHandler.getCurrentSpace()?.flattenParentIds?.lastOrNull()
setCurrentSpace(previousSpaceId ?: parentSpaceId)
}
private fun setCurrentSpace(spaceId: String?) {
appStateHandler.setCurrentSpace(spaceId, isForwardNavigation = false)
sharedActionViewModel.post(HomeActivitySharedAction.OnCloseSpace)
}
private fun handleCallStarted() {
dismissLoadingDialog()
val fragmentTag = HomeTab.DialPad.toFragmentTag()
(childFragmentManager.findFragmentByTag(fragmentTag) as? DialPadFragment)?.clear()
}
override fun onDestroyView() {
currentCallsViewPresenter.unBind()
super.onDestroyView()
}
override fun onResume() {
super.onResume()
updateTabVisibilitySafely(R.id.bottom_action_notification, vectorPreferences.labAddNotificationTab())
callManager.checkForProtocolsSupportIfNeeded()
refreshSpaceState()
}
private fun refreshSpaceState() {
appStateHandler.getCurrentSpace()?.let {
onSpaceChange(it)
}
}
private fun promptForNewUnknownDevices(uid: String, state: UnknownDevicesState, newest: DeviceInfo) {
val user = state.myMatrixItem
alertManager.postVectorAlert(
VerificationVectorAlert(
uid = uid,
title = getString(R.string.new_session),
description = getString(R.string.verify_this_session, newest.displayName ?: newest.deviceId ?: ""),
iconId = R.drawable.ic_shield_warning
).apply {
viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer)
colorInt = colorProvider.getColorFromAttribute(R.attr.colorPrimary)
contentAction = Runnable {
(weakCurrentActivity?.get() as? VectorBaseActivity<*>)
?.navigator
?.requestSessionVerification(requireContext(), newest.deviceId ?: "")
unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) }.orEmpty())
)
}
dismissedAction = Runnable {
unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) }.orEmpty())
)
}
}
)
}
private fun promptToReviewChanges(uid: String, state: UnknownDevicesState, oldUnverified: List<DeviceInfo>) {
val user = state.myMatrixItem
alertManager.postVectorAlert(
VerificationVectorAlert(
uid = uid,
title = getString(R.string.review_logins),
description = getString(R.string.verify_other_sessions),
iconId = R.drawable.ic_shield_warning
).apply {
viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer)
colorInt = colorProvider.getColorFromAttribute(R.attr.colorPrimary)
contentAction = Runnable {
(weakCurrentActivity?.get() as? VectorBaseActivity<*>)?.let { activity ->
// mark as ignored to avoid showing it again
unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(oldUnverified.mapNotNull { it.deviceId })
)
activity.navigator.openSettings(activity, EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS)
}
}
dismissedAction = Runnable {
unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(oldUnverified.mapNotNull { it.deviceId })
)
}
}
)
}
private fun onSpaceChange(spaceSummary: RoomSummary?) {
// Reimplement in next PR
println(spaceSummary)
}
private fun setupKeysBackupBanner() {
serverBackupStatusViewModel
.onEach {
when (val banState = it.bannerState.invoke()) {
is BannerState.Setup -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(banState.numberOfKeys), false)
BannerState.BackingUp -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.BackingUp, false)
null,
BannerState.Hidden -> views.homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false)
}
}
views.homeKeysBackupBanner.delegate = this
}
private fun setupActiveCallView() {
currentCallsViewPresenter.bind(views.currentCallsView, this)
}
private fun setupToolbar() {
setupToolbar(views.toolbar)
lifecycleScope.launch(Dispatchers.IO) {
session.userService().getUser(session.myUserId)?.let { user ->
avatarRenderer.render(user.toMatrixItem(), views.avatar)
}
}
views.avatar.debouncedClicks {
navigator.openSettings(requireContext())
}
}
private fun setupBottomNavigationView() {
views.bottomNavigationView.menu.findItem(R.id.bottom_action_notification).isVisible = vectorPreferences.labAddNotificationTab()
views.bottomNavigationView.setOnItemSelectedListener {
val tab = when (it.itemId) {
R.id.bottom_action_people -> HomeTab.RoomList(RoomListDisplayMode.PEOPLE)
R.id.bottom_action_rooms -> HomeTab.RoomList(RoomListDisplayMode.ROOMS)
R.id.bottom_action_notification -> HomeTab.RoomList(RoomListDisplayMode.NOTIFICATIONS)
else -> HomeTab.DialPad
}
viewModel.handle(HomeDetailAction.SwitchTab(tab))
true
}
}
private fun updateUIForTab(tab: HomeTab) {
views.bottomNavigationView.menu.findItem(tab.toMenuId()).isChecked = true
updateSelectedFragment(tab)
invalidateOptionsMenu()
}
private fun HomeTab.toFragmentTag() = "FRAGMENT_TAG_$this"
private fun updateSelectedFragment(tab: HomeTab) {
val fragmentTag = tab.toFragmentTag()
val fragmentToShow = childFragmentManager.findFragmentByTag(fragmentTag)
childFragmentManager.commitTransaction {
childFragmentManager.fragments
.filter { it != fragmentToShow }
.forEach {
detach(it)
}
if (fragmentToShow == null) {
when (tab) {
is HomeTab.RoomList -> {
add(R.id.roomListContainer, HomeRoomListFragment::class.java, null, fragmentTag)
}
is HomeTab.DialPad -> {
add(R.id.roomListContainer, createDialPadFragment(), fragmentTag)
}
}
} else {
if (tab is HomeTab.DialPad) {
(fragmentToShow as? DialPadFragment)?.applyCallback()
}
attach(fragmentToShow)
}
}
}
private fun createDialPadFragment(): Fragment {
val fragment = childFragmentManager.fragmentFactory.instantiate(vectorBaseActivity.classLoader, DialPadFragment::class.java.name)
return (fragment as DialPadFragment).apply {
arguments = Bundle().apply {
putBoolean(DialPadFragment.EXTRA_ENABLE_DELETE, true)
putBoolean(DialPadFragment.EXTRA_ENABLE_OK, true)
putString(DialPadFragment.EXTRA_REGION_CODE, VectorLocale.applicationLocale.country)
}
applyCallback()
}
}
private fun updateTabVisibilitySafely(tabId: Int, isVisible: Boolean) {
val wasVisible = views.bottomNavigationView.menu.findItem(tabId).isVisible
views.bottomNavigationView.menu.findItem(tabId).isVisible = isVisible
if (wasVisible && !isVisible) {
// As we hide it check if it's not the current item!
withState(viewModel) {
if (it.currentTab.toMenuId() == tabId) {
viewModel.handle(HomeDetailAction.SwitchTab(HomeTab.RoomList(RoomListDisplayMode.PEOPLE)))
}
}
}
}
/* ==========================================================================================
* KeysBackupBanner Listener
* ========================================================================================== */
override fun setupKeysBackup() {
navigator.openKeysBackupSetup(requireActivity(), false)
}
override fun recoverKeysBackup() {
navigator.openKeysBackupManager(requireActivity())
}
override fun invalidate() = withState(viewModel) {
views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_people).render(it.notificationCountPeople, it.notificationHighlightPeople)
views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_rooms).render(it.notificationCountRooms, it.notificationHighlightRooms)
views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_notification).render(it.notificationCountCatchup, it.notificationHighlightCatchup)
views.syncStateView.render(
it.syncState,
it.incrementalSyncRequestState,
it.pushCounter,
vectorPreferences.developerShowDebugInfo()
)
hasUnreadRooms = it.hasUnreadMessages
}
private fun BadgeDrawable.render(count: Int, highlight: Boolean) {
isVisible = count > 0
number = count
maxCharacterCount = 3
badgeTextColor = ThemeUtils.getColor(requireContext(), R.attr.colorOnPrimary)
backgroundColor = if (highlight) {
ThemeUtils.getColor(requireContext(), R.attr.colorError)
} else {
ThemeUtils.getColor(requireContext(), R.attr.vctr_unread_background)
}
}
private fun HomeTab.toMenuId() = when (this) {
is HomeTab.DialPad -> R.id.bottom_action_dial_pad
is HomeTab.RoomList -> when (displayMode) {
RoomListDisplayMode.PEOPLE -> R.id.bottom_action_people
RoomListDisplayMode.ROOMS -> R.id.bottom_action_rooms
else -> R.id.bottom_action_notification
}
}
override fun onTapToReturnToCall() {
callManager.getCurrentCall()?.let { call ->
VectorCallActivity.newIntent(
context = requireContext(),
callId = call.callId,
signalingRoomId = call.signalingRoomId,
otherUserId = call.mxCall.opponentUserId,
isIncomingCall = !call.mxCall.isOutgoing,
isVideoCall = call.mxCall.isVideoCall,
mode = null
).let {
startActivity(it)
}
}
}
private fun DialPadFragment.applyCallback(): DialPadFragment {
callback = object : DialPadFragment.Callback {
override fun onOkClicked(formatted: String?, raw: String?) {
if (raw.isNullOrEmpty()) return
viewModel.handle(HomeDetailAction.StartCallWithPhoneNumber(raw))
}
}
return this
}
override fun onBackPressed(toolbarButton: Boolean) = if (appStateHandler.getCurrentSpace() != null) {
navigateBack()
true
} else {
false
}
}

View File

@ -24,8 +24,8 @@ import androidx.annotation.WorkerThread
import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import im.vector.app.BuildConfig
import im.vector.app.core.glide.GlideApp import im.vector.app.core.glide.GlideApp
import im.vector.app.core.resources.BuildMeta
import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.MainActivity import im.vector.app.features.MainActivity
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
@ -35,13 +35,15 @@ import javax.inject.Inject
private val useAdaptiveIcon = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O private val useAdaptiveIcon = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
private const val adaptiveIconSizeDp = 108 private const val adaptiveIconSizeDp = 108
private const val adaptiveIconOuterSidesDp = 18 private const val adaptiveIconOuterSidesDp = 18
private const val directShareCategory = BuildConfig.APPLICATION_ID + ".SHORTCUT_SHARE"
class ShortcutCreator @Inject constructor( class ShortcutCreator @Inject constructor(
private val context: Context, private val context: Context,
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val dimensionConverter: DimensionConverter private val dimensionConverter: DimensionConverter,
buildMeta: BuildMeta,
) { ) {
private val directShareCategory = buildMeta.applicationId + ".SHORTCUT_SHARE"
private val adaptiveIconSize = dimensionConverter.dpToPx(adaptiveIconSizeDp) private val adaptiveIconSize = dimensionConverter.dpToPx(adaptiveIconSizeDp)
private val adaptiveIconOuterSides = dimensionConverter.dpToPx(adaptiveIconOuterSidesDp) private val adaptiveIconOuterSides = dimensionConverter.dpToPx(adaptiveIconOuterSidesDp)
private val iconSize by lazy { private val iconSize by lazy {

View File

@ -22,7 +22,7 @@ import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.AppStateHandler import im.vector.app.SpaceStateHandler
import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyAction
@ -58,7 +58,7 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(
@Assisted initialState: UnreadMessagesState, @Assisted initialState: UnreadMessagesState,
session: Session, session: Session,
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
//appStateHandler: AppStateHandler, //spaceStateHandler: SpaceStateHandler,
private val autoAcceptInvites: AutoAcceptInvites private val autoAcceptInvites: AutoAcceptInvites
) : ) :
VectorViewModel<UnreadMessagesState, EmptyAction, EmptyViewEvents>(initialState) { VectorViewModel<UnreadMessagesState, EmptyAction, EmptyViewEvents>(initialState) {
@ -118,8 +118,8 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(
.execute { .execute {
/* /*
combine( combine(
appStateHandler.selectedSpaceFlow.distinctUntilChanged(), spaceStateHandler.getSelectedSpaceFlow().distinctUntilChanged(),
appStateHandler.selectedSpaceFlow.flatMapLatest { spaceStateHandler.getSelectedSpaceFlow().flatMapLatest {
roomService.getPagedRoomSummariesLive( roomService.getPagedRoomSummariesLive(
roomSummaryQueryParams { roomSummaryQueryParams {
this.memberships = Membership.activeMemberships() this.memberships = Membership.activeMemberships()
@ -201,10 +201,10 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(
CountInfo( CountInfo(
homeCount = counts, homeCount = counts,
otherCount = RoomAggregateNotificationCount( otherCount = RoomAggregateNotificationCount(
notificationCount = rootCounts.fold(0, { acc, rs -> acc + rs.notificationCount }) + notificationCount = rootCounts.fold(0) { acc, rs -> acc + rs.notificationCount } +
(counts.notificationCount.takeIf { selectedSpace != null } ?: 0) + (counts.notificationCount.takeIf { selectedSpace != null } ?: 0) +
spaceInviteCount, spaceInviteCount,
highlightCount = rootCounts.fold(0, { acc, rs -> acc + rs.highlightCount }) + highlightCount = rootCounts.fold(0) { acc, rs -> acc + rs.highlightCount } +
(counts.highlightCount.takeIf { selectedSpace != null } ?: 0) + (counts.highlightCount.takeIf { selectedSpace != null } ?: 0) +
spaceInviteCount, spaceInviteCount,

View File

@ -78,7 +78,6 @@ import de.spiritcroc.recyclerview.StickyHeaderItemDecoration
import de.spiritcroc.recyclerview.widget.BetterLinearLayoutManager import de.spiritcroc.recyclerview.widget.BetterLinearLayoutManager
import de.spiritcroc.recyclerview.widget.LinearLayoutManager import de.spiritcroc.recyclerview.widget.LinearLayoutManager
import de.spiritcroc.util.ThumbnailGenerationVideoDownloadDecider import de.spiritcroc.util.ThumbnailGenerationVideoDownloadDecider
import im.vector.app.BuildConfig
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.animations.play import im.vector.app.core.animations.play
import im.vector.app.core.dialogs.ConfirmationDialogBuilder import im.vector.app.core.dialogs.ConfirmationDialogBuilder
@ -104,6 +103,7 @@ import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.platform.VectorMenuProvider
import im.vector.app.core.platform.lifecycleAwareLazy import im.vector.app.core.platform.lifecycleAwareLazy
import im.vector.app.core.platform.showOptimizedSnackbar import im.vector.app.core.platform.showOptimizedSnackbar
import im.vector.app.core.resources.BuildMeta
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.core.time.Clock import im.vector.app.core.time.Clock
@ -137,6 +137,7 @@ import im.vector.app.core.utils.startInstallFromSourceIntent
import im.vector.app.core.utils.toast import im.vector.app.core.utils.toast
import im.vector.app.databinding.DialogReportContentBinding import im.vector.app.databinding.DialogReportContentBinding
import im.vector.app.databinding.FragmentTimelineBinding import im.vector.app.databinding.FragmentTimelineBinding
import im.vector.app.features.VectorFeatures
import im.vector.app.features.analytics.extensions.toAnalyticsInteraction import im.vector.app.features.analytics.extensions.toAnalyticsInteraction
import im.vector.app.features.analytics.plan.Interaction import im.vector.app.features.analytics.plan.Interaction
import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.analytics.plan.MobileScreen
@ -299,7 +300,9 @@ class TimelineFragment @Inject constructor(
private val typingHelper: TypingHelper, private val typingHelper: TypingHelper,
private val audioMessagePlaybackTracker: AudioMessagePlaybackTracker, private val audioMessagePlaybackTracker: AudioMessagePlaybackTracker,
private val shareIntentHandler: ShareIntentHandler, private val shareIntentHandler: ShareIntentHandler,
private val clock: Clock private val clock: Clock,
private val vectorFeatures: VectorFeatures,
private val buildMeta: BuildMeta,
) : ) :
VectorBaseFragment<FragmentTimelineBinding>(), VectorBaseFragment<FragmentTimelineBinding>(),
TimelineEventController.Callback, TimelineEventController.Callback,
@ -401,7 +404,7 @@ class TimelineFragment @Inject constructor(
sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java) sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java)
sharedActivityActionViewModel = activityViewModelProvider.get(RoomDetailSharedActionViewModel::class.java) sharedActivityActionViewModel = activityViewModelProvider.get(RoomDetailSharedActionViewModel::class.java)
knownCallsViewModel = activityViewModelProvider.get(SharedKnownCallsViewModel::class.java) knownCallsViewModel = activityViewModelProvider.get(SharedKnownCallsViewModel::class.java)
attachmentsHelper = AttachmentsHelper(requireContext(), this).register() attachmentsHelper = AttachmentsHelper(requireContext(), this, buildMeta).register()
callActionsHandler = StartCallActionsHandler( callActionsHandler = StartCallActionsHandler(
roomId = timelineArgs.roomId, roomId = timelineArgs.roomId,
fragment = this, fragment = this,
@ -699,8 +702,8 @@ class TimelineFragment @Inject constructor(
) )
} }
private fun navigateToLocationLiveMap() { private fun navigateToLiveLocationMap() {
navigator.openLocationLiveMap( navigator.openLiveLocationMap(
context = requireContext(), context = requireContext(),
roomId = timelineArgs.roomId roomId = timelineArgs.roomId
) )
@ -900,11 +903,11 @@ class TimelineFragment @Inject constructor(
} }
private fun setupLiveLocationIndicator() { private fun setupLiveLocationIndicator() {
views.locationLiveStatusIndicator.stopButton.debouncedClicks { views.liveLocationStatusIndicator.stopButton.debouncedClicks {
timelineViewModel.handle(RoomDetailAction.StopLiveLocationSharing) timelineViewModel.handle(RoomDetailAction.StopLiveLocationSharing)
} }
views.locationLiveStatusIndicator.debouncedClicks { views.liveLocationStatusIndicator.debouncedClicks {
navigateToLocationLiveMap() navigateToLiveLocationMap()
} }
} }
@ -1005,7 +1008,7 @@ class TimelineFragment @Inject constructor(
} }
override fun onDestroyView() { override fun onDestroyView() {
audioMessagePlaybackTracker.makeAllPlaybacksIdle() messageComposerViewModel.endAllVoiceActions()
lazyLoadedViews.unBind() lazyLoadedViews.unBind()
timelineEventController.callback = null timelineEventController.callback = null
timelineEventController.removeModelBuildListener(modelBuildListener) timelineEventController.removeModelBuildListener(modelBuildListener)
@ -1157,7 +1160,7 @@ class TimelineFragment @Inject constructor(
else -> state.isAllowedToManageWidgets else -> state.isAllowedToManageWidgets
} }
menu.findItem(R.id.video_call).icon?.alpha = if (callButtonsEnabled) 0xFF else 0x40 menu.findItem(R.id.video_call).icon?.alpha = if (callButtonsEnabled) 0xFF else 0x40
menu.findItem(R.id.voice_call).icon?.alpha = if (callButtonsEnabled || state.hasActiveElementCallWidget()) 0xFF else 0x40 menu.findItem(R.id.voice_call).icon?.alpha = if (callButtonsEnabled || state.hasActiveElementCallWidget()) 0xFF else 0x40
val matrixAppsMenuItem = menu.findItem(R.id.open_matrix_apps) val matrixAppsMenuItem = menu.findItem(R.id.open_matrix_apps)
val widgetsCount = state.activeRoomWidgets.invoke()?.size ?: 0 val widgetsCount = state.activeRoomWidgets.invoke()?.size ?: 0
@ -1827,7 +1830,7 @@ class TimelineFragment @Inject constructor(
attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@TimelineFragment) attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@TimelineFragment)
attachmentTypeSelector.setAttachmentVisibility( attachmentTypeSelector.setAttachmentVisibility(
AttachmentTypeSelectorView.Type.LOCATION, AttachmentTypeSelectorView.Type.LOCATION,
BuildConfig.enableLocationSharing vectorFeatures.isLocationSharingEnabled(),
) )
attachmentTypeSelector.setAttachmentVisibility( attachmentTypeSelector.setAttachmentVisibility(
AttachmentTypeSelectorView.Type.POLL, !isThreadTimeLine() AttachmentTypeSelectorView.Type.POLL, !isThreadTimeLine()
@ -1965,7 +1968,7 @@ class TimelineFragment @Inject constructor(
} }
private fun updateLiveLocationIndicator(isSharingLiveLocation: Boolean) { private fun updateLiveLocationIndicator(isSharingLiveLocation: Boolean) {
views.locationLiveStatusIndicator.isVisible = isSharingLiveLocation views.liveLocationStatusIndicator.isVisible = isSharingLiveLocation
} }
private fun FragmentTimelineBinding.hideComposerViews() { private fun FragmentTimelineBinding.hideComposerViews() {
@ -2380,7 +2383,7 @@ class TimelineFragment @Inject constructor(
handleShowLocationPreview(messageContent, informationData.senderId) handleShowLocationPreview(messageContent, informationData.senderId)
} }
is MessageBeaconInfoContent -> { is MessageBeaconInfoContent -> {
navigateToLocationLiveMap() navigateToLiveLocationMap()
} }
else -> { else -> {
val handled = onThreadSummaryClicked(informationData.eventId, isRootThreadEvent) val handled = onThreadSummaryClicked(informationData.eventId, isRootThreadEvent)
@ -2993,7 +2996,6 @@ class TimelineFragment @Inject constructor(
} }
override fun onContactAttachmentReady(contactAttachment: ContactAttachment) { override fun onContactAttachmentReady(contactAttachment: ContactAttachment) {
super.onContactAttachmentReady(contactAttachment)
val formattedContact = contactAttachment.toHumanReadable() val formattedContact = contactAttachment.toHumanReadable()
messageComposerViewModel.handle(MessageComposerAction.SendMessage(formattedContact, false)) messageComposerViewModel.handle(MessageComposerAction.SendMessage(formattedContact, false))
} }

View File

@ -29,12 +29,13 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import de.spiritcroc.matrixsdk.util.DbgUtil import de.spiritcroc.matrixsdk.util.DbgUtil
import de.spiritcroc.matrixsdk.util.Dimber import de.spiritcroc.matrixsdk.util.Dimber
import im.vector.app.AppStateHandler
import im.vector.app.R import im.vector.app.R
import im.vector.app.SpaceStateHandler
import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.mvrx.runCatchingToAsync import im.vector.app.core.mvrx.runCatchingToAsync
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.BuildMeta
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.BehaviorDataSource import im.vector.app.core.utils.BehaviorDataSource
import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.AnalyticsTracker
@ -54,10 +55,11 @@ import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandle
import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
import im.vector.app.features.home.room.typing.TypingHelper import im.vector.app.features.home.room.typing.TypingHelper
import im.vector.app.features.location.LocationSharingServiceConnection
import im.vector.app.features.location.live.StopLiveLocationShareUseCase import im.vector.app.features.location.live.StopLiveLocationShareUseCase
import im.vector.app.features.location.live.tracking.LocationSharingServiceConnection
import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.notifications.NotificationDrawerManager
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import im.vector.app.features.raw.wellknown.CryptoConfig
import im.vector.app.features.raw.wellknown.getOutboundSessionKeySharingStrategyOrDefault import im.vector.app.features.raw.wellknown.getOutboundSessionKeySharingStrategyOrDefault
import im.vector.app.features.raw.wellknown.withElementWellKnown import im.vector.app.features.raw.wellknown.withElementWellKnown
import im.vector.app.features.session.coroutineScope import im.vector.app.features.session.coroutineScope
@ -140,8 +142,10 @@ class TimelineViewModel @AssistedInject constructor(
private val locationSharingServiceConnection: LocationSharingServiceConnection, private val locationSharingServiceConnection: LocationSharingServiceConnection,
private val stopLiveLocationShareUseCase: StopLiveLocationShareUseCase, private val stopLiveLocationShareUseCase: StopLiveLocationShareUseCase,
private val redactLiveLocationShareEventUseCase: RedactLiveLocationShareEventUseCase, private val redactLiveLocationShareEventUseCase: RedactLiveLocationShareEventUseCase,
private val cryptoConfig: CryptoConfig,
buildMeta: BuildMeta,
timelineFactory: TimelineFactory, timelineFactory: TimelineFactory,
appStateHandler: AppStateHandler, spaceStateHandler: SpaceStateHandler,
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState), ) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener, LocationSharingServiceConnection.Callback { Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener, LocationSharingServiceConnection.Callback {
@ -160,7 +164,7 @@ class TimelineViewModel @AssistedInject constructor(
private val rmDimber = Dimber("ReadMarkerDbg", DbgUtil.DBG_READ_MARKER) private val rmDimber = Dimber("ReadMarkerDbg", DbgUtil.DBG_READ_MARKER)
// Same lifecycle than the ViewModel (survive to screen rotation) // Same lifecycle than the ViewModel (survive to screen rotation)
val previewUrlRetriever = PreviewUrlRetriever(session, viewModelScope) val previewUrlRetriever = PreviewUrlRetriever(session, viewModelScope, buildMeta)
// Slot to keep a pending action during permission request // Slot to keep a pending action during permission request
var pendingAction: RoomDetailAction? = null var pendingAction: RoomDetailAction? = null
@ -233,7 +237,7 @@ class TimelineViewModel @AssistedInject constructor(
// Ensure to share the outbound session keys with all members // Ensure to share the outbound session keys with all members
if (room.roomCryptoService().isEncrypted()) { if (room.roomCryptoService().isEncrypted()) {
rawService.withElementWellKnown(viewModelScope, session.sessionParams) { rawService.withElementWellKnown(viewModelScope, session.sessionParams) {
val strategy = it.getOutboundSessionKeySharingStrategyOrDefault() val strategy = it.getOutboundSessionKeySharingStrategyOrDefault(cryptoConfig.fallbackKeySharingStrategy)
if (strategy == OutboundSessionKeySharingStrategy.WhenEnteringRoom) { if (strategy == OutboundSessionKeySharingStrategy.WhenEnteringRoom) {
prepareForEncryption() prepareForEncryption()
} }
@ -248,16 +252,16 @@ class TimelineViewModel @AssistedInject constructor(
if (initialState.switchToParentSpace) { if (initialState.switchToParentSpace) {
// We are coming from a notification, try to switch to the most relevant space // We are coming from a notification, try to switch to the most relevant space
// so that when hitting back the room will appear in the list // so that when hitting back the room will appear in the list
appStateHandler.getCurrentSpace().let { currentSpace -> spaceStateHandler.getCurrentSpace().let { currentSpace ->
val currentRoomSummary = room.roomSummary() ?: return@let val currentRoomSummary = room.roomSummary() ?: return@let
// nothing we are good // nothing we are good
if ((currentSpace == null && !vectorPreferences.prefSpacesShowAllRoomInHome()) || if ((currentSpace == null && !vectorPreferences.prefSpacesShowAllRoomInHome()) ||
(currentSpace != null && !currentRoomSummary.flattenParentIds.contains(currentSpace.roomId))) { (currentSpace != null && !currentRoomSummary.flattenParentIds.contains(currentSpace.roomId))) {
// take first one or switch to home // take first one or switch to home
appStateHandler.setCurrentSpace( spaceStateHandler.setCurrentSpace(
currentRoomSummary currentRoomSummary
.flattenParentIds.firstOrNull { it.isNotBlank() }, .flattenParentIds.firstOrNull { it.isNotBlank() },
// force persist, because if not on resume the AppStateHandler will resume // force persist, because if not on resume the SpaceStateHandler will resume
// the current space from what was persisted on enter background // the current space from what was persisted on enter background
persistNow = true persistNow = true
) )
@ -731,7 +735,7 @@ class TimelineViewModel @AssistedInject constructor(
// Ensure outbound session keys // Ensure outbound session keys
if (room.roomCryptoService().isEncrypted()) { if (room.roomCryptoService().isEncrypted()) {
rawService.withElementWellKnown(viewModelScope, session.sessionParams) { rawService.withElementWellKnown(viewModelScope, session.sessionParams) {
val strategy = it.getOutboundSessionKeySharingStrategyOrDefault() val strategy = it.getOutboundSessionKeySharingStrategyOrDefault(cryptoConfig.fallbackKeySharingStrategy)
if (strategy == OutboundSessionKeySharingStrategy.WhenTyping && action.focused) { if (strategy == OutboundSessionKeySharingStrategy.WhenTyping && action.focused) {
// Should we add some rate limit here, or do it only once per model lifecycle? // Should we add some rate limit here, or do it only once per model lifecycle?
prepareForEncryption() prepareForEncryption()

View File

@ -20,7 +20,7 @@ import android.content.Context
import android.media.AudioAttributes import android.media.AudioAttributes
import android.media.MediaPlayer import android.media.MediaPlayer
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import im.vector.app.BuildConfig import im.vector.app.core.resources.BuildMeta
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
import im.vector.app.features.voice.VoiceFailure import im.vector.app.features.voice.VoiceFailure
import im.vector.app.features.voice.VoiceRecorder import im.vector.app.features.voice.VoiceRecorder
@ -43,6 +43,7 @@ import javax.inject.Inject
class AudioMessageHelper @Inject constructor( class AudioMessageHelper @Inject constructor(
private val context: Context, private val context: Context,
private val playbackTracker: AudioMessagePlaybackTracker, private val playbackTracker: AudioMessagePlaybackTracker,
private val buildMeta: BuildMeta,
voiceRecorderProvider: VoiceRecorderProvider voiceRecorderProvider: VoiceRecorderProvider
) { ) {
private var mediaPlayer: MediaPlayer? = null private var mediaPlayer: MediaPlayer? = null
@ -88,7 +89,7 @@ class AudioMessageHelper @Inject constructor(
try { try {
voiceMessageFile?.let { voiceMessageFile?.let {
val outputFileUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileProvider", it, "Voice message.${it.extension}") val outputFileUri = FileProvider.getUriForFile(context, buildMeta.applicationId + ".fileProvider", it, "Voice message.${it.extension}")
return outputFileUri return outputFileUri
.toMultiPickerAudioType(context) .toMultiPickerAudioType(context)
?.apply { ?.apply {

View File

@ -41,7 +41,6 @@ sealed class MessageComposerAction : VectorViewModelAction {
object PauseRecordingVoiceMessage : MessageComposerAction() object PauseRecordingVoiceMessage : MessageComposerAction()
data class PlayOrPauseVoicePlayback(val eventId: String, val messageAudioContent: MessageAudioContent) : MessageComposerAction() data class PlayOrPauseVoicePlayback(val eventId: String, val messageAudioContent: MessageAudioContent) : MessageComposerAction()
object PlayOrPauseRecordingPlayback : MessageComposerAction() object PlayOrPauseRecordingPlayback : MessageComposerAction()
data class EndAllVoiceActions(val deleteRecord: Boolean = true) : MessageComposerAction()
data class VoiceWaveformTouchedUp(val eventId: String, val duration: Int, val percentage: Float) : MessageComposerAction() data class VoiceWaveformTouchedUp(val eventId: String, val duration: Int, val percentage: Float) : MessageComposerAction()
data class VoiceWaveformMovedTo(val eventId: String, val duration: Int, val percentage: Float) : MessageComposerAction() data class VoiceWaveformMovedTo(val eventId: String, val duration: Int, val percentage: Float) : MessageComposerAction()
data class AudioSeekBarMovedTo(val eventId: String, val duration: Int, val percentage: Float) : MessageComposerAction() data class AudioSeekBarMovedTo(val eventId: String, val duration: Int, val percentage: Float) : MessageComposerAction()

View File

@ -107,7 +107,6 @@ class MessageComposerViewModel @AssistedInject constructor(
is MessageComposerAction.PlayOrPauseVoicePlayback -> handlePlayOrPauseVoicePlayback(action) is MessageComposerAction.PlayOrPauseVoicePlayback -> handlePlayOrPauseVoicePlayback(action)
MessageComposerAction.PauseRecordingVoiceMessage -> handlePauseRecordingVoiceMessage() MessageComposerAction.PauseRecordingVoiceMessage -> handlePauseRecordingVoiceMessage()
MessageComposerAction.PlayOrPauseRecordingPlayback -> handlePlayOrPauseRecordingPlayback() MessageComposerAction.PlayOrPauseRecordingPlayback -> handlePlayOrPauseRecordingPlayback()
is MessageComposerAction.EndAllVoiceActions -> handleEndAllVoiceActions(action.deleteRecord)
is MessageComposerAction.InitializeVoiceRecorder -> handleInitializeVoiceRecorder(action.attachmentData) is MessageComposerAction.InitializeVoiceRecorder -> handleInitializeVoiceRecorder(action.attachmentData)
is MessageComposerAction.OnEntersBackground -> handleEntersBackground(action.composerText) is MessageComposerAction.OnEntersBackground -> handleEntersBackground(action.composerText)
is MessageComposerAction.VoiceWaveformTouchedUp -> handleVoiceWaveformTouchedUp(action) is MessageComposerAction.VoiceWaveformTouchedUp -> handleVoiceWaveformTouchedUp(action)
@ -889,7 +888,7 @@ class MessageComposerViewModel @AssistedInject constructor(
audioMessageHelper.startOrPauseRecordingPlayback() audioMessageHelper.startOrPauseRecordingPlayback()
} }
private fun handleEndAllVoiceActions(deleteRecord: Boolean) { fun endAllVoiceActions(deleteRecord: Boolean = true) {
audioMessageHelper.clearTracker() audioMessageHelper.clearTracker()
audioMessageHelper.stopAllVoiceActions(deleteRecord) audioMessageHelper.stopAllVoiceActions(deleteRecord)
} }

View File

@ -0,0 +1,21 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.detail.composer.voice
data class VoiceMessageConfig(
val lengthLimitMs: Long
)

View File

@ -21,7 +21,6 @@ import android.util.AttributeSet
import android.view.View import android.view.View
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.BuildConfig
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.hardware.vibrate import im.vector.app.core.hardware.vibrate
import im.vector.app.core.time.Clock import im.vector.app.core.time.Clock
@ -57,6 +56,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
} }
@Inject lateinit var clock: Clock @Inject lateinit var clock: Clock
@Inject lateinit var voiceMessageConfig: VoiceMessageConfig
// We need to define views as lateinit var to be able to check if initialized for the bug fix on api 21 and 22. // We need to define views as lateinit var to be able to check if initialized for the bug fix on api 21 and 22.
@Suppress("UNNECESSARY_LATEINIT") @Suppress("UNNECESSARY_LATEINIT")
@ -202,7 +202,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
private fun onRecordingTick(isLocked: Boolean, milliseconds: Long) { private fun onRecordingTick(isLocked: Boolean, milliseconds: Long) {
voiceMessageViews.renderRecordingTimer(isLocked, milliseconds / 1_000) voiceMessageViews.renderRecordingTimer(isLocked, milliseconds / 1_000)
val timeDiffToRecordingLimit = BuildConfig.VOICE_MESSAGE_DURATION_LIMIT_MS - milliseconds val timeDiffToRecordingLimit = voiceMessageConfig.lengthLimitMs - milliseconds
if (timeDiffToRecordingLimit <= 0) { if (timeDiffToRecordingLimit <= 0) {
post { post {
callback.onRecordingLimitReached() callback.onRecordingLimitReached()

View File

@ -36,6 +36,8 @@ import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
import im.vector.app.features.home.room.detail.timeline.style.granularRoundedCorners import im.vector.app.features.home.room.detail.timeline.style.granularRoundedCorners
import im.vector.app.features.location.MapLoadingErrorView
import im.vector.app.features.location.MapLoadingErrorViewState
abstract class AbsMessageLocationItem<H : AbsMessageLocationItem.Holder>( abstract class AbsMessageLocationItem<H : AbsMessageLocationItem.Holder>(
@LayoutRes layoutId: Int = R.layout.item_timeline_event_base @LayoutRes layoutId: Int = R.layout.item_timeline_event_base
@ -88,8 +90,10 @@ abstract class AbsMessageLocationItem<H : AbsMessageLocationItem.Holder>(
target: Target<Drawable>?, target: Target<Drawable>?,
isFirstResource: Boolean isFirstResource: Boolean
): Boolean { ): Boolean {
holder.staticMapPinImageView.setImageResource(R.drawable.ic_location_pin_failed) holder.staticMapPinImageView.setImageDrawable(null)
holder.staticMapErrorTextView.isVisible = true holder.staticMapLoadingErrorView.isVisible = true
val mapErrorViewState = MapLoadingErrorViewState(imageCornerTransformation)
holder.staticMapLoadingErrorView.render(mapErrorViewState)
holder.staticMapCopyrightTextView.isVisible = false holder.staticMapCopyrightTextView.isVisible = false
return false return false
} }
@ -105,7 +109,7 @@ abstract class AbsMessageLocationItem<H : AbsMessageLocationItem.Holder>(
// we are not using Glide since it does not display it correctly when there is no user photo // we are not using Glide since it does not display it correctly when there is no user photo
holder.staticMapPinImageView.setImageDrawable(pinDrawable) holder.staticMapPinImageView.setImageDrawable(pinDrawable)
} }
holder.staticMapErrorTextView.isVisible = false holder.staticMapLoadingErrorView.isVisible = false
holder.staticMapCopyrightTextView.isVisible = true holder.staticMapCopyrightTextView.isVisible = true
return false return false
} }
@ -117,7 +121,7 @@ abstract class AbsMessageLocationItem<H : AbsMessageLocationItem.Holder>(
abstract class Holder(@IdRes stubId: Int) : AbsMessageItem.Holder(stubId) { abstract class Holder(@IdRes stubId: Int) : AbsMessageItem.Holder(stubId) {
val staticMapImageView by bind<ImageView>(R.id.staticMapImageView) val staticMapImageView by bind<ImageView>(R.id.staticMapImageView)
val staticMapPinImageView by bind<ImageView>(R.id.staticMapPinImageView) val staticMapPinImageView by bind<ImageView>(R.id.staticMapPinImageView)
val staticMapErrorTextView by bind<TextView>(R.id.staticMapErrorTextView) val staticMapLoadingErrorView by bind<MapLoadingErrorView>(R.id.staticMapLoadingError)
val staticMapCopyrightTextView by bind<TextView>(R.id.staticMapCopyrightTextView) val staticMapCopyrightTextView by bind<TextView>(R.id.staticMapCopyrightTextView)
} }
} }

View File

@ -42,8 +42,8 @@ abstract class MessageLiveLocationInactiveItem :
override fun getViewStubId() = STUB_ID override fun getViewStubId() = STUB_ID
class Holder : AbsMessageItem.Holder(STUB_ID) { class Holder : AbsMessageItem.Holder(STUB_ID) {
val bannerImageView by bind<ImageView>(R.id.locationLiveEndedBannerBackground) val bannerImageView by bind<ImageView>(R.id.liveLocationEndedBannerBackground)
val noLocationMapImageView by bind<ImageView>(R.id.locationLiveInactiveMap) val noLocationMapImageView by bind<ImageView>(R.id.liveLocationInactiveMap)
} }
companion object { companion object {

View File

@ -26,8 +26,8 @@ import im.vector.app.core.resources.toTimestamp
import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.RoomDetailAction
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
import im.vector.app.features.location.live.LocationLiveMessageBannerViewState import im.vector.app.features.location.live.LiveLocationMessageBannerViewState
import im.vector.app.features.location.live.LocationLiveRunningBannerView import im.vector.app.features.location.live.LiveLocationRunningBannerView
import org.threeten.bp.LocalDateTime import org.threeten.bp.LocalDateTime
@EpoxyModelClass @EpoxyModelClass
@ -44,17 +44,17 @@ abstract class MessageLiveLocationItem : AbsMessageLocationItem<MessageLiveLocat
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
bindLocationLiveBanner(holder) bindLiveLocationBanner(holder)
} }
private fun bindLocationLiveBanner(holder: Holder) { private fun bindLiveLocationBanner(holder: Holder) {
// TODO in a future PR add check on device id to confirm that is the one that sent the beacon // TODO in a future PR add check on device id to confirm that is the one that sent the beacon
val isEmitter = currentUserId != null && currentUserId == locationUserId val isEmitter = currentUserId != null && currentUserId == locationUserId
val messageLayout = attributes.informationData.messageLayout val messageLayout = attributes.informationData.messageLayout
val viewState = buildViewState(holder, messageLayout, isEmitter) val viewState = buildViewState(holder, messageLayout, isEmitter)
holder.locationLiveRunningBanner.isVisible = true holder.liveLocationRunningBanner.isVisible = true
holder.locationLiveRunningBanner.render(viewState) holder.liveLocationRunningBanner.render(viewState)
holder.locationLiveRunningBanner.stopButton.setOnClickListener { holder.liveLocationRunningBanner.stopButton.setOnClickListener {
attributes.callback?.onTimelineItemAction(RoomDetailAction.StopLiveLocationSharing) attributes.callback?.onTimelineItemAction(RoomDetailAction.StopLiveLocationSharing)
} }
} }
@ -63,24 +63,24 @@ abstract class MessageLiveLocationItem : AbsMessageLocationItem<MessageLiveLocat
holder: Holder, holder: Holder,
messageLayout: TimelineMessageLayout, messageLayout: TimelineMessageLayout,
isEmitter: Boolean isEmitter: Boolean
): LocationLiveMessageBannerViewState { ): LiveLocationMessageBannerViewState {
return when { return when {
messageLayout is TimelineMessageLayout.Bubble && isEmitter -> messageLayout is TimelineMessageLayout.Bubble && isEmitter ->
LocationLiveMessageBannerViewState.Emitter( LiveLocationMessageBannerViewState.Emitter(
remainingTimeInMillis = getRemainingTimeOfLiveInMillis(), remainingTimeInMillis = getRemainingTimeOfLiveInMillis(),
bottomStartCornerRadiusInDp = messageLayout.cornersRadius.bottomStartRadius, bottomStartCornerRadiusInDp = messageLayout.cornersRadius.bottomStartRadius,
bottomEndCornerRadiusInDp = messageLayout.cornersRadius.bottomEndRadius, bottomEndCornerRadiusInDp = messageLayout.cornersRadius.bottomEndRadius,
isStopButtonCenteredVertically = false isStopButtonCenteredVertically = false
) )
messageLayout is TimelineMessageLayout.Bubble -> messageLayout is TimelineMessageLayout.Bubble ->
LocationLiveMessageBannerViewState.Watcher( LiveLocationMessageBannerViewState.Watcher(
bottomStartCornerRadiusInDp = messageLayout.cornersRadius.bottomStartRadius, bottomStartCornerRadiusInDp = messageLayout.cornersRadius.bottomStartRadius,
bottomEndCornerRadiusInDp = messageLayout.cornersRadius.bottomEndRadius, bottomEndCornerRadiusInDp = messageLayout.cornersRadius.bottomEndRadius,
formattedLocalTimeOfEndOfLive = getFormattedLocalTimeEndOfLive(), formattedLocalTimeOfEndOfLive = getFormattedLocalTimeEndOfLive(),
) )
messageLayout is TimelineMessageLayout.ScBubble && isEmitter -> { messageLayout is TimelineMessageLayout.ScBubble && isEmitter -> {
val radius = messageLayout.bubbleAppearance.getBubbleRadiusDp(holder.view.context) val radius = messageLayout.bubbleAppearance.getBubbleRadiusDp(holder.view.context)
LocationLiveMessageBannerViewState.Emitter( LiveLocationMessageBannerViewState.Emitter(
remainingTimeInMillis = getRemainingTimeOfLiveInMillis(), remainingTimeInMillis = getRemainingTimeOfLiveInMillis(),
bottomStartCornerRadiusInDp = radius, bottomStartCornerRadiusInDp = radius,
bottomEndCornerRadiusInDp = radius, bottomEndCornerRadiusInDp = radius,
@ -89,7 +89,7 @@ abstract class MessageLiveLocationItem : AbsMessageLocationItem<MessageLiveLocat
} }
messageLayout is TimelineMessageLayout.ScBubble -> { messageLayout is TimelineMessageLayout.ScBubble -> {
val radius = messageLayout.bubbleAppearance.getBubbleRadiusDp(holder.view.context) val radius = messageLayout.bubbleAppearance.getBubbleRadiusDp(holder.view.context)
LocationLiveMessageBannerViewState.Watcher( LiveLocationMessageBannerViewState.Watcher(
bottomStartCornerRadiusInDp = radius, bottomStartCornerRadiusInDp = radius,
bottomEndCornerRadiusInDp = radius, bottomEndCornerRadiusInDp = radius,
formattedLocalTimeOfEndOfLive = getFormattedLocalTimeEndOfLive(), formattedLocalTimeOfEndOfLive = getFormattedLocalTimeEndOfLive(),
@ -97,7 +97,7 @@ abstract class MessageLiveLocationItem : AbsMessageLocationItem<MessageLiveLocat
} }
isEmitter -> { isEmitter -> {
val cornerRadius = getBannerCornerRadiusForDefaultLayout(holder) val cornerRadius = getBannerCornerRadiusForDefaultLayout(holder)
LocationLiveMessageBannerViewState.Emitter( LiveLocationMessageBannerViewState.Emitter(
remainingTimeInMillis = getRemainingTimeOfLiveInMillis(), remainingTimeInMillis = getRemainingTimeOfLiveInMillis(),
bottomStartCornerRadiusInDp = cornerRadius, bottomStartCornerRadiusInDp = cornerRadius,
bottomEndCornerRadiusInDp = cornerRadius, bottomEndCornerRadiusInDp = cornerRadius,
@ -106,7 +106,7 @@ abstract class MessageLiveLocationItem : AbsMessageLocationItem<MessageLiveLocat
} }
else -> { else -> {
val cornerRadius = getBannerCornerRadiusForDefaultLayout(holder) val cornerRadius = getBannerCornerRadiusForDefaultLayout(holder)
LocationLiveMessageBannerViewState.Watcher( LiveLocationMessageBannerViewState.Watcher(
bottomStartCornerRadiusInDp = cornerRadius, bottomStartCornerRadiusInDp = cornerRadius,
bottomEndCornerRadiusInDp = cornerRadius, bottomEndCornerRadiusInDp = cornerRadius,
formattedLocalTimeOfEndOfLive = getFormattedLocalTimeEndOfLive(), formattedLocalTimeOfEndOfLive = getFormattedLocalTimeEndOfLive(),
@ -129,7 +129,7 @@ abstract class MessageLiveLocationItem : AbsMessageLocationItem<MessageLiveLocat
override fun getViewStubId() = STUB_ID override fun getViewStubId() = STUB_ID
class Holder : AbsMessageLocationItem.Holder(STUB_ID) { class Holder : AbsMessageLocationItem.Holder(STUB_ID) {
val locationLiveRunningBanner by bind<LocationLiveRunningBannerView>(R.id.locationLiveRunningBanner) val liveLocationRunningBanner by bind<LiveLocationRunningBannerView>(R.id.liveLocationRunningBanner)
} }
companion object { companion object {

View File

@ -42,8 +42,8 @@ abstract class MessageLiveLocationStartItem :
override fun getViewStubId() = STUB_ID override fun getViewStubId() = STUB_ID
class Holder : AbsMessageItem.Holder(STUB_ID) { class Holder : AbsMessageItem.Holder(STUB_ID) {
val bannerImageView by bind<ImageView>(R.id.locationLiveStartBanner) val bannerImageView by bind<ImageView>(R.id.liveLocationStartBanner)
val noLocationMapImageView by bind<ImageView>(R.id.locationLiveStartMap) val noLocationMapImageView by bind<ImageView>(R.id.liveLocationStartMap)
} }
companion object { companion object {

View File

@ -16,7 +16,7 @@
package im.vector.app.features.home.room.detail.timeline.url package im.vector.app.features.home.room.detail.timeline.url
import im.vector.app.BuildConfig import im.vector.app.core.resources.BuildMeta
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -27,7 +27,8 @@ import org.matrix.android.sdk.api.session.room.timeline.getLatestEventId
class PreviewUrlRetriever( class PreviewUrlRetriever(
session: Session, session: Session,
private val coroutineScope: CoroutineScope private val coroutineScope: CoroutineScope,
private val buildMeta: BuildMeta,
) { ) {
private val mediaService = session.mediaService() private val mediaService = session.mediaService()
@ -77,7 +78,7 @@ class PreviewUrlRetriever(
mediaService.getPreviewUrl( mediaService.getPreviewUrl(
url = urlToRetrieve, url = urlToRetrieve,
timestamp = null, timestamp = null,
cacheStrategy = if (BuildConfig.DEBUG) CacheStrategy.NoCache else CacheStrategy.TtlCache(CACHE_VALIDITY, false) cacheStrategy = if (buildMeta.isDebug) CacheStrategy.NoCache else CacheStrategy.TtlCache(CACHE_VALIDITY, false)
) )
}.fold( }.fold(
{ {

View File

@ -40,13 +40,15 @@ abstract class FilteredRoomFooterItem : VectorEpoxyModel<FilteredRoomFooterItem.
@EpoxyAttribute @EpoxyAttribute
var inSpace: Boolean = false var inSpace: Boolean = false
@EpoxyAttribute
var simplifiedMode: Boolean = false
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
val vectorPreferences = VectorPreferences(holder.createRoomButton.context, DefaultClock())
holder.createRoomButton.onClick { listener?.createRoom(currentFilter) } holder.createRoomButton.onClick { listener?.createRoom(currentFilter) }
holder.createDirectChat.onClick { listener?.createDirectChat() } holder.createDirectChat.onClick { listener?.createDirectChat() }
holder.openRoomDirectory.onClick { listener?.openRoomDirectory(currentFilter) } holder.openRoomDirectory.onClick { listener?.openRoomDirectory(currentFilter) }
holder.openRoomDirectory.visibility = if (vectorPreferences.simplifiedMode()) View.GONE else View.VISIBLE holder.openRoomDirectory.visibility = if (simplifiedMode) View.GONE else View.VISIBLE
holder.openRoomDirectory.setText( holder.openRoomDirectory.setText(
if (inSpace) R.string.space_explore_activity_title else R.string.room_filtering_footer_open_room_directory if (inSpace) R.string.space_explore_activity_title else R.string.room_filtering_footer_open_room_directory

View File

@ -24,11 +24,13 @@ import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.features.home.RoomListDisplayMode import im.vector.app.features.home.RoomListDisplayMode
import im.vector.app.features.home.room.filtered.FilteredRoomFooterItem import im.vector.app.features.home.room.filtered.FilteredRoomFooterItem
import im.vector.app.features.home.room.filtered.filteredRoomFooterItem import im.vector.app.features.home.room.filtered.filteredRoomFooterItem
import im.vector.app.features.settings.VectorPreferences
import javax.inject.Inject import javax.inject.Inject
class RoomListFooterController @Inject constructor( class RoomListFooterController @Inject constructor(
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val userPreferencesProvider: UserPreferencesProvider private val userPreferencesProvider: UserPreferencesProvider,
private val vectorPreferences: VectorPreferences,
) : TypedEpoxyController<RoomListViewState>() { ) : TypedEpoxyController<RoomListViewState>() {
var listener: FilteredRoomFooterItem.Listener? = null var listener: FilteredRoomFooterItem.Listener? = null
@ -42,6 +44,7 @@ class RoomListFooterController @Inject constructor(
listener(host.listener) listener(host.listener)
currentFilter(data.roomFilter) currentFilter(data.roomFilter)
inSpace(data.asyncSelectedSpace.invoke() != null) inSpace(data.asyncSelectedSpace.invoke() != null)
simplifiedMode(host.vectorPreferences.simplifiedMode())
} }
} }
else -> { else -> {

View File

@ -37,7 +37,6 @@ import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.mvrx.args import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.app.AppStateHandler
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import de.spiritcroc.matrixsdk.util.DbgUtil import de.spiritcroc.matrixsdk.util.DbgUtil
import de.spiritcroc.matrixsdk.util.Dimber import de.spiritcroc.matrixsdk.util.Dimber
@ -81,7 +80,6 @@ data class RoomListParams(
) : Parcelable ) : Parcelable
class RoomListFragment @Inject constructor( class RoomListFragment @Inject constructor(
private val appStateHandler: AppStateHandler,
private val pagedControllerFactory: RoomSummaryPagedControllerFactory, private val pagedControllerFactory: RoomSummaryPagedControllerFactory,
private val notificationDrawerManager: NotificationDrawerManager, private val notificationDrawerManager: NotificationDrawerManager,
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,

View File

@ -25,8 +25,8 @@ import androidx.paging.PagedList
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import de.spiritcroc.matrixsdk.util.DbgUtil import de.spiritcroc.matrixsdk.util.DbgUtil
import de.spiritcroc.matrixsdk.util.Dimber import de.spiritcroc.matrixsdk.util.Dimber
import im.vector.app.AppStateHandler
import im.vector.app.R import im.vector.app.R
import im.vector.app.SpaceStateHandler
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.RoomListDisplayMode import im.vector.app.features.home.RoomListDisplayMode
import im.vector.app.features.invite.AutoAcceptInvites import im.vector.app.features.invite.AutoAcceptInvites
@ -61,7 +61,7 @@ import timber.log.Timber
class RoomListSectionBuilder( class RoomListSectionBuilder(
private val session: Session, private val session: Session,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val appStateHandler: AppStateHandler, private val spaceStateHandler: SpaceStateHandler,
private val viewModelScope: CoroutineScope, private val viewModelScope: CoroutineScope,
private val autoAcceptInvites: AutoAcceptInvites, private val autoAcceptInvites: AutoAcceptInvites,
private val onUpdatable: (UpdatableLivePageResult) -> Unit, private val onUpdatable: (UpdatableLivePageResult) -> Unit,
@ -108,7 +108,7 @@ class RoomListSectionBuilder(
} }
if (explicitSpaceId == SPACE_ID_FOLLOW_APP) { if (explicitSpaceId == SPACE_ID_FOLLOW_APP) {
appStateHandler.selectedSpaceFlow spaceStateHandler.getSelectedSpaceFlow()
.distinctUntilChanged() .distinctUntilChanged()
.onEach { selectedSpaceOption -> .onEach { selectedSpaceOption ->
val selectedSpace = selectedSpaceOption.orNull() val selectedSpace = selectedSpaceOption.orNull()
@ -256,7 +256,7 @@ class RoomListSectionBuilder(
) { ) {
it.memberships = listOf(Membership.JOIN) it.memberships = listOf(Membership.JOIN)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS it.roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
it.roomTagQueryFilter = RoomTagQueryFilter(false, false, false) it.roomTagQueryFilter = RoomTagQueryFilter(isFavorite = false, isLowPriority = false, isServerNotice = false)
} }
addSection( addSection(
@ -300,7 +300,7 @@ class RoomListSectionBuilder(
explicitSpaceId: String?) { explicitSpaceId: String?) {
// add suggested rooms // add suggested rooms
val suggestedRoomsFlow = if (explicitSpaceId == SPACE_ID_FOLLOW_APP) { // MutableLiveData<List<SpaceChildInfo>>() val suggestedRoomsFlow = if (explicitSpaceId == SPACE_ID_FOLLOW_APP) { // MutableLiveData<List<SpaceChildInfo>>()
appStateHandler.selectedSpaceFlow spaceStateHandler.getSelectedSpaceFlow()
.distinctUntilChanged() .distinctUntilChanged()
.flatMapLatest { selectedSpaceOption -> .flatMapLatest { selectedSpaceOption ->
val selectedSpace = selectedSpaceOption.orNull() val selectedSpace = selectedSpaceOption.orNull()
@ -418,7 +418,7 @@ class RoomListSectionBuilder(
) { ) {
it.memberships = listOf(Membership.JOIN) it.memberships = listOf(Membership.JOIN)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM
it.roomTagQueryFilter = RoomTagQueryFilter(false, false, null) it.roomTagQueryFilter = RoomTagQueryFilter(isFavorite = false, isLowPriority = false, isServerNotice = null)
} }
addSection( addSection(
@ -435,14 +435,14 @@ class RoomListSectionBuilder(
) { ) {
it.memberships = listOf(Membership.JOIN) it.memberships = listOf(Membership.JOIN)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM
it.roomTagQueryFilter = RoomTagQueryFilter(false, true, null) it.roomTagQueryFilter = RoomTagQueryFilter(isFavorite = false, isLowPriority = true, isServerNotice = null)
} }
} }
private fun AppStateHandler.explicitOrSafeActiveSpaceId(explicitSpaceId: String?): String? { private fun SpaceStateHandler.explicitOrSafeActiveSpaceId(explicitSpaceId: String?): String? {
return if (explicitSpaceId == SPACE_ID_FOLLOW_APP) { return if (explicitSpaceId == SPACE_ID_FOLLOW_APP) {
safeActiveSpaceId() getSafeActiveSpaceId()
} else { } else {
explicitSpaceId explicitSpaceId
} }
@ -525,7 +525,7 @@ class RoomListSectionBuilder(
query: (RoomSummaryQueryParams.Builder) -> Unit query: (RoomSummaryQueryParams.Builder) -> Unit
) { ) {
withQueryParams(query) { roomQueryParams -> withQueryParams(query) { roomQueryParams ->
val updatedQueryParams = roomQueryParams.process(spaceFilterStrategy, appStateHandler.explicitOrSafeActiveSpaceId(explicitSpaceId)) val updatedQueryParams = roomQueryParams.process(spaceFilterStrategy, spaceStateHandler.explicitOrSafeActiveSpaceId(explicitSpaceId))
val liveQueryParams = MutableStateFlow(updatedQueryParams) val liveQueryParams = MutableStateFlow(updatedQueryParams)
val itemCountFlow = liveQueryParams val itemCountFlow = liveQueryParams
.flatMapLatest { .flatMapLatest {
@ -536,7 +536,7 @@ class RoomListSectionBuilder(
val name = stringProvider.getString(nameRes) val name = stringProvider.getString(nameRes)
val filteredPagedRoomSummariesLive = session.roomService().getFilteredPagedRoomSummariesLive( val filteredPagedRoomSummariesLive = session.roomService().getFilteredPagedRoomSummariesLive(
roomQueryParams.process(spaceFilterStrategy, appStateHandler.explicitOrSafeActiveSpaceId(explicitSpaceId)), roomQueryParams.process(spaceFilterStrategy, spaceStateHandler.explicitOrSafeActiveSpaceId(explicitSpaceId)),
pagedListConfig pagedListConfig
) )
when (spaceFilterStrategy) { when (spaceFilterStrategy) {
@ -583,7 +583,7 @@ class RoomListSectionBuilder(
RoomAggregateNotificationCount(it.size, it.size, 0) RoomAggregateNotificationCount(it.size, it.size, 0)
} else { } else {
session.roomService().getNotificationCountForRooms( session.roomService().getNotificationCountForRooms(
roomQueryParams.process(spaceFilterStrategy, appStateHandler.explicitOrSafeActiveSpaceId(explicitSpaceId)) roomQueryParams.process(spaceFilterStrategy, spaceStateHandler.explicitOrSafeActiveSpaceId(explicitSpaceId))
) )
} }
) )

View File

@ -27,7 +27,7 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import de.spiritcroc.matrixsdk.util.DbgUtil import de.spiritcroc.matrixsdk.util.DbgUtil
import de.spiritcroc.matrixsdk.util.Dimber import de.spiritcroc.matrixsdk.util.Dimber
import im.vector.app.AppStateHandler import im.vector.app.SpaceStateHandler
import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
@ -62,7 +62,7 @@ class RoomListViewModel @AssistedInject constructor(
@Assisted initialState: RoomListViewState, @Assisted initialState: RoomListViewState,
private val session: Session, private val session: Session,
stringProvider: StringProvider, stringProvider: StringProvider,
appStateHandler: AppStateHandler, spaceStateHandler: SpaceStateHandler,
vectorPreferences: VectorPreferences, vectorPreferences: VectorPreferences,
autoAcceptInvites: AutoAcceptInvites, autoAcceptInvites: AutoAcceptInvites,
private val analyticsTracker: AnalyticsTracker private val analyticsTracker: AnalyticsTracker
@ -112,7 +112,7 @@ class RoomListViewModel @AssistedInject constructor(
observeMembershipChanges() observeMembershipChanges()
observeLocalRooms() observeLocalRooms()
appStateHandler.selectedSpaceFlow spaceStateHandler.getSelectedSpaceFlow()
.distinctUntilChanged() .distinctUntilChanged()
.execute { .execute {
copy( copy(
@ -158,17 +158,17 @@ class RoomListViewModel @AssistedInject constructor(
companion object : MavericksViewModelFactory<RoomListViewModel, RoomListViewState> by hiltMavericksViewModelFactory() companion object : MavericksViewModelFactory<RoomListViewModel, RoomListViewState> by hiltMavericksViewModelFactory()
private val roomListSectionBuilder = RoomListSectionBuilder( private val roomListSectionBuilder = RoomListSectionBuilder(
session, session,
stringProvider, stringProvider,
appStateHandler, spaceStateHandler,
viewModelScope, viewModelScope,
autoAcceptInvites, autoAcceptInvites,
{ {
updatableQuery = it updatableQuery = it
}, },
suggestedRoomJoiningState, suggestedRoomJoiningState,
!vectorPreferences.prefSpacesShowAllRoomInHome() !vectorPreferences.prefSpacesShowAllRoomInHome()
) )
val sections: List<RoomsSection> by lazy { val sections: List<RoomsSection> by lazy {
viewPagerDimber.i { "Build sections for ${initialState.displayMode}, ${initialState.explicitSpaceId}" } viewPagerDimber.i { "Build sections for ${initialState.displayMode}, ${initialState.explicitSpaceId}" }

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