Merge branch 'release/v1.4.32'

This commit is contained in:
Adam Brown 2022-08-10 09:47:50 +01:00
commit ce28d7f8a5
252 changed files with 4430 additions and 1458 deletions

View file

@ -7,10 +7,8 @@ on:
# Enrich gradle.properties for CI/CD
env:
CI_GRADLE_ARG_PROPERTIES: >
-Porg.gradle.jvmargs=-Xmx4g
-Porg.gradle.parallel=false
--no-daemon
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options="-Xmx2560m" -Dkotlin.incremental=false
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon
jobs:
debug:
@ -36,7 +34,7 @@ jobs:
restore-keys: |
${{ runner.os }}-gradle-
- 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
uses: actions/upload-artifact@v3
with:
@ -61,7 +59,7 @@ jobs:
restore-keys: |
${{ runner.os }}-gradle-
- 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
uses: actions/upload-artifact@v3
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"
on: [push, pull_request]
on:
pull_request: { }
push:
branches: [ main, develop ]
jobs:
validation:

View file

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

View file

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

View file

@ -7,10 +7,8 @@ on:
# Enrich gradle.properties for CI/CD
env:
CI_GRADLE_ARG_PROPERTIES: >
-Porg.gradle.jvmargs=-Xmx4g
-Porg.gradle.parallel=false
--no-daemon
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options="-Xmx2560m" -Dkotlin.incremental=false
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon
jobs:
check:
@ -29,80 +27,50 @@ jobs:
- uses: actions/checkout@v3
- name: Run knit
run: |
./gradlew knit
./gradlew knitCheck
# ktlint for all the modules
ktlint:
name: Kotlin Linter
# Check the project: ktlint, detekt, lint
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('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
steps:
- uses: actions/checkout@v3
- name: Run ktlint
run: |
./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
if: always()
uses: actions/upload-artifact@v3
with:
name: ktlinting-report
name: linting-report
path: |
*/build/reports/ktlint/ktlint*/ktlint*.txt
- name: Handle Results
*/build/reports/**/*.*
- name: Prepare Danger
if: always()
id: ktlint-results
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")"
if [ -z "$results" ]; then
echo "::set-output name=add_comment::false"
else
body="👎\`Failed${results}\`"
body="${body//'%'/'%25'}"
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
npm install --save-dev @babel/core
npm install --save-dev @babel/plugin-transform-flow-strip-types
yarn add danger-plugin-lint-report --dev
- name: Danger lint
if: always()
uses: danger/danger-js@11.1.1
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: Ktlint Results
- 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 }}
})
args: "--dangerfile tools/danger/dangerfile-lint.js"
env:
DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }}
# Gradle dependency analysis using https://github.com/autonomousapps/dependency-analysis-android-gradle-plugin
dependency-analysis:
@ -122,107 +90,3 @@ jobs:
with:
name: dependency-analysis
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
env:
CI_GRADLE_ARG_PROPERTIES: >
-Porg.gradle.jvmargs=-Xmx4g
-Porg.gradle.parallel=false
--no-daemon
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options="-Xmx2560m" -Dkotlin.incremental=false
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon
jobs:
tests:
@ -51,9 +49,9 @@ jobs:
disable-animations: true
emulator-build: 7425822
script: |
./gradlew unitTestsWithCoverage --stacktrace $CI_GRADLE_ARG_PROPERTIES
./gradlew instrumentationTestsWithCoverage --stacktrace $CI_GRADLE_ARG_PROPERTIES
./gradlew generateCoverageReport --stacktrace $CI_GRADLE_ARG_PROPERTIES
./gradlew unitTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES
./gradlew instrumentationTestsWithCoverage $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'
- name: Run all the codecoverage tests at once (retry if emulator failed)
uses: reactivecircus/android-emulator-runner@v2
@ -67,13 +65,13 @@ jobs:
disable-animations: true
emulator-build: 7425822
script: |
./gradlew unitTestsWithCoverage --stacktrace $CI_GRADLE_ARG_PROPERTIES
./gradlew instrumentationTestsWithCoverage --stacktrace $CI_GRADLE_ARG_PROPERTIES
./gradlew generateCoverageReport --stacktrace $CI_GRADLE_ARG_PROPERTIES
./gradlew unitTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES
./gradlew instrumentationTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES
./gradlew generateCoverageReport $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
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 }}
ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }}
@ -114,5 +112,5 @@ jobs:
# restore-keys: |
# ${{ runner.os }}-gradle-
# - name: Build Android Tests
# run: ./gradlew clean assembleAndroidTest $CI_GRADLE_ARG_PROPERTIES --stacktrace
# run: ./gradlew clean assembleAndroidTest $CI_GRADLE_ARG_PROPERTIES

5
.gitignore vendored
View file

@ -17,3 +17,8 @@
/fastlane/report.xml
/**/build
# 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)
=======================================

View file

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

View file

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

View file

@ -30,7 +30,7 @@ buildscript {
classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5'
classpath "com.likethesalad.android:stem-plugin:2.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"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@ -41,10 +41,10 @@ plugins {
// ktlint Plugin
id "org.jlleitschuh.gradle.ktlint" version "10.3.0"
// Detekt
id "io.gitlab.arturbosch.detekt" version "1.20.0"
id "io.gitlab.arturbosch.detekt" version "1.21.0"
// 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
@ -126,6 +126,11 @@ allprojects {
enableExperimentalRules = true
// display the corresponding rule
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 = [
// TODO Re-enable these 4 rules after reformatting project
"indent",

View file

@ -20,9 +20,9 @@ def retrofit = "2.9.0"
def arrow = "0.8.2"
def markwon = "4.6.2"
def moshi = "1.13.0"
def lifecycle = "2.5.0"
def lifecycle = "2.5.1"
def flowBinding = "1.2.0"
def flipper = "0.154.0"
def flipper = "0.156.0"
def epoxy = "4.6.2"
def mavericks = "2.7.0"
def glide = "4.13.2"
@ -30,7 +30,7 @@ def bigImageViewer = "1.8.1"
def jjwt = "0.11.5"
def vanniktechEmoji = "0.15.0"
def fragment = "1.5.0"
def fragment = "1.5.1"
// 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
@ -50,7 +50,7 @@ ext.libs = [
'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines"
],
androidx : [
'activity' : "androidx.activity:activity:1.5.0",
'activity' : "androidx.activity:activity:1.5.1",
'appCompat' : "androidx.appcompat:appcompat:1.4.2",
'biometric' : "androidx.biometric:biometric:1.1.0",
'core' : "androidx.core:core-ktx:1.8.0",

View file

@ -74,6 +74,7 @@ ext.groups = [
'com.github.javaparser',
'com.github.piasy',
'com.github.shyiko.klob',
'com.github.rubensousa',
'com.google',
'com.google.android',
'com.google.api.grpc',
@ -125,6 +126,7 @@ ext.groups = [
'info.picocli',
'io.arrow-kt',
'io.element.android',
'io.github.davidburstrom.contester',
'io.github.detekt.sarif4k',
'io.github.microutils',
'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
rm towncrier.toml.bak
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.

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

@ -71,4 +71,7 @@
<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_vertical">22dp</dimen>
<!-- Material 3 -->
<dimen name="collapsing_toolbar_layout_medium_size">112dp</dimen>
</resources>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="LocationLiveEndedBannerView">
<declare-styleable name="LiveLocationEndedBannerView">
<attr name="locLiveEndedBkgWithAlpha" format="boolean" />
<attr name="locLiveEndedIconMarginStart" format="dimension" />
</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"?>
<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:background">@android:color/transparent</item>
<item name="android:textSize">12sp</item>
@ -12,7 +12,7 @@
<item name="android:insetLeft">8dp</item>
</style>
<style name="Widget.Vector.Button.Text.LocationLive">
<style name="Widget.Vector.Button.Text.LiveLocation">
<item name="android:foreground">?selectableItemBackground</item>
<item name="android:background">@android:color/transparent</item>
<item name="android:textAppearance">@style/TextAppearance.Vector.Body.Medium</item>

View file

@ -39,4 +39,14 @@
<item name="android:textSize">12sp</item>
</style>
</resources>
<!-- 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>

View file

@ -32,6 +32,15 @@
<item name="android:textColor">?vctr_content_primary</item>
</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">
<item name="fontFamily">sans-serif</item>
<item name="android:fontFamily">sans-serif</item>

View file

@ -25,4 +25,4 @@
<item name="android:backgroundDimEnabled">false</item>
</style>
</resources>
</resources>

View file

@ -149,6 +149,9 @@
<!-- Location sharing -->
<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 name="Theme.Vector.Dark" parent="Base.Theme.Vector.Dark" />

View file

@ -150,8 +150,12 @@
<!-- Location sharing -->
<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 name="Theme.Vector.Light" parent="Base.Theme.Vector.Light" />
</resources>

View file

@ -60,7 +60,7 @@ android {
// that the app's state is completely cleared between tests.
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_UNIX_DATE", "\"${gitRevisionUnixDate()}\""
@ -199,7 +199,7 @@ dependencies {
implementation libs.apache.commonsImaging
// 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
// 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
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 &&
error.code == MatrixError.M_FORBIDDEN &&

View file

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

View file

@ -535,7 +535,7 @@ internal class MXMegolmEncryption(
@Throws
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()}")
val userId = deviceInfo.userId
val deviceId = deviceInfo.deviceId

View file

@ -119,7 +119,7 @@ internal class EventSenderProcessorThread @Inject constructor(
override fun cancel(eventId: String, roomId: String) {
(currentTask as? SendEventQueuedTask)
?.takeIf { it -> it.event.eventId == eventId && it.event.roomId == roomId }
?.takeIf { it.event.eventId == eventId && it.event.roomId == roomId }
?.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
ProtectedMemberInFinalClass:
active: false
UseCheckOrError:
active: false
empty-blocks:
EmptyFunctionBlock:
@ -43,6 +45,8 @@ exceptions:
active: false
TooGenericExceptionThrown:
active: false
InstanceOfCheckForException:
active: false
complexity:
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.
*/
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.FTUE_AUTH
/**
* 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");
* you may not use this file except in compliance with the License.
@ -16,17 +16,20 @@
package im.vector.app.config
import im.vector.app.BuildConfig
import im.vector.app.features.analytics.AnalyticsConfig
enum class KeySharingStrategy {
/**
* 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",
"im.vector.app.nightly",
)
/**
* Keys will be sent for the first time when the timeline displayed.
*/
WhenEnteringRoom,
val analyticsConfig: AnalyticsConfig = object : AnalyticsConfig {
override val isEnabled = BuildConfig.APPLICATION_ID in allowedPackageList
override val postHogHost = "https://posthog.hss.element.io"
override val postHogApiKey = "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO"
override val policyLink = "https://element.io/cookie-policy"
/**
* Keys will be sent for the first time when a typing started.
*/
WhenTyping
}

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 '**/towncrier/template.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.
// When creating a hotfix, you should decrease the value, since the current value
// is the value for the next regular release.
ext.versionPatch = 31
ext.versionPatch = 32
static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct'
@ -155,19 +156,6 @@ android {
buildConfigField "String", "GIT_BRANCH_NAME", "\"${gitBranchName()}\""
buildConfigField "String", "BUILD_NUMBER", "\"${buildNumber}\""
buildConfigField "im.vector.app.features.VectorFeatures.OnboardingVariant", "ONBOARDING_VARIANT", "im.vector.app.features.VectorFeatures.OnboardingVariant.FTUE_AUTH"
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"
// Keep abiFilter for the universalApk
@ -249,10 +237,6 @@ android {
resValue "string", "app_name", "Element dbg"
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
if (project.hasProperty("coverage")) {
@ -264,10 +248,6 @@ android {
resValue "string", "app_name", "Element"
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 {
removeUnusedCode true
removeUnusedResources true
@ -328,7 +308,6 @@ android {
versionName "${versionMajor}.${versionMinor}.${versionPatch}${getGplayVersionSuffix()}"
resValue "bool", "isGplay", "true"
buildConfigField "boolean", "ALLOW_FCM_USE", "true"
buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"G\""
buildConfigField "String", "FLAVOR_DESCRIPTION", "\"GooglePlay\""
}
@ -339,7 +318,6 @@ android {
versionName "${versionMajor}.${versionMinor}.${versionPatch}${getFdroidVersionSuffix()}"
resValue "bool", "isGplay", "false"
buildConfigField "boolean", "ALLOW_FCM_USE", "false"
buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"F\""
buildConfigField "String", "FLAVOR_DESCRIPTION", "\"FDroid\""
}
@ -437,7 +415,7 @@ dependencies {
implementation 'com.facebook.stetho:stetho:1.6.0'
// Phone number https://github.com/google/libphonenumber
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.52'
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.53'
// FlowBinding
implementation libs.github.flowBinding
@ -449,6 +427,9 @@ dependencies {
implementation libs.airbnb.epoxyPaging
implementation libs.airbnb.mavericks
// Snap Helper https://github.com/rubensousa/GravitySnapHelper
implementation 'com.github.rubensousa:gravitysnaphelper:2.2.2'
// Nightly
// API-only library
gplayImplementation libs.google.appdistributionApi
@ -620,4 +601,5 @@ dependencies {
androidTestImplementation libs.mockk.mockkAndroid
androidTestUtil libs.androidx.orchestrator
debugImplementation libs.androidx.fragmentTesting
androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.10"
}

View file

@ -102,6 +102,7 @@
<!-- Wording -->
<issue id="Typos" severity="error" />
<issue id="TypographyDashes" severity="error" />
<issue id="PluralsCandidate" severity="error" />
<!-- DI -->
<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 org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.runner.RunWith
import java.util.UUID
@ -35,7 +36,9 @@ import java.util.UUID
class CantVerifyTest : VerificationTestBase() {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
val testRule = RuleChain
.outerRule(ActivityScenarioRule(MainActivity::class.java))
.around(ClearCurrentSessionRule())
private val elementRobot = ElementRobot()
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 im.vector.app.TestBuildVersionSdkIntProvider
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.crypto.LockScreenCryptoConstants
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 io.mockk.clearAllMocks
import io.mockk.every
import io.mockk.justRun
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.mockkStatic
@ -54,8 +54,10 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.coInvoking
import org.amshove.kluent.shouldBeFalse
import org.amshove.kluent.shouldBeTrue
import org.amshove.kluent.shouldThrow
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
@ -239,36 +241,35 @@ class BiometricHelperTests {
@Test
@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
every { lockScreenKeyRepository.isSystemKeyValid() } returns true
val mockAuthChannel = Channel<Boolean>(capacity = 1)
val biometricUtils = spyk(createBiometricHelper(createDefaultConfiguration(isBiometricsEnabled = true))) {
every { createAuthChannel() } returns mockAuthChannel
every { authenticateWithPromptInternal(any(), any(), any()) } returns mockk()
}
justRun { lockScreenKeyRepository.deleteSystemKey() }
val latch = CountDownLatch(1)
val intent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, LockScreenTestActivity::class.java)
ActivityScenario.launch<LockScreenTestActivity>(intent).onActivity { activity ->
activity.lifecycleScope.launch {
val exception = IllegalStateException("Some error")
launch {
mockAuthChannel.send(true)
mockAuthChannel.close()
mockAuthChannel.close(exception)
}
biometricUtils.authenticate(activity).collect()
coInvoking { biometricUtils.enableAuthentication(activity).collect() } shouldThrow exception
latch.countDown()
}
}
latch.await(1, TimeUnit.SECONDS)
verify { lockScreenKeyRepository.ensureSystemKey() }
verify { lockScreenKeyRepository.deleteSystemKey() }
}
private fun createBiometricHelper(configuration: LockScreenConfiguration): BiometricHelper {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val configProvider = LockScreenConfiguratorProvider(configuration)
return BiometricHelper(context, lockScreenKeyRepository, configProvider, biometricManager, buildVersionSdkIntProvider)
return BiometricHelper(configuration, context, lockScreenKeyRepository, biometricManager, buildVersionSdkIntProvider)
}
private fun createDefaultConfiguration(

View file

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

View file

@ -17,8 +17,6 @@
package im.vector.app.features.pin.lockscreen.crypto
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.every
import io.mockk.mockk
@ -44,8 +42,6 @@ class LockScreenKeyRepositoryTests {
}
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 {
KeyStore.getInstance(LockScreenCryptoConstants.ANDROID_KEY_STORE).also { it.load(null) }

View file

@ -70,6 +70,11 @@ class DebugFeaturesStateFactory @Inject constructor(
key = DebugFeatureKeys.allowExternalUnifiedPushDistributors,
factory = VectorFeatures::allowExternalUnifiedPushDistributors
),
createBooleanFeature(
label = "Enable Live Location Sharing",
key = DebugFeatureKeys.liveLocationSharing,
factory = VectorFeatures::isLocationSharingEnabled
),
createBooleanFeature(
label = "Force usage of OpusEncoder library",
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.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import im.vector.app.config.OnboardingVariant
import im.vector.app.features.DefaultVectorFeatures
import im.vector.app.features.VectorFeatures
import kotlinx.coroutines.flow.first
@ -39,8 +40,8 @@ class DebugVectorFeatures(
private val dataStore = context.dataStore
override fun onboardingVariant(): VectorFeatures.OnboardingVariant {
return readPreferences().getEnum<VectorFeatures.OnboardingVariant>() ?: vectorFeatures.onboardingVariant()
override fun onboardingVariant(): OnboardingVariant {
return readPreferences().getEnum<OnboardingVariant>() ?: vectorFeatures.onboardingVariant()
}
override fun isOnboardingAlreadyHaveAccountSplashEnabled(): Boolean = read(DebugFeatureKeys.onboardingAlreadyHaveAnAccount)
@ -66,6 +67,9 @@ class DebugVectorFeatures(
override fun isScreenSharingEnabled(): Boolean = read(DebugFeatureKeys.screenSharing)
?: vectorFeatures.isScreenSharingEnabled()
override fun isLocationSharingEnabled(): Boolean = read(DebugFeatureKeys.liveLocationSharing)
?: vectorFeatures.isLocationSharingEnabled()
override fun forceUsageOfOpusEncoder(): Boolean = read(DebugFeatureKeys.forceUsageOfOpusEncoder)
?: vectorFeatures.forceUsageOfOpusEncoder()

View file

@ -347,7 +347,7 @@
<activity android:name=".features.spaces.leave.SpaceLeaveAdvancedActivity" />
<activity android:name=".features.poll.create.CreatePollActivity" />
<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"/>
<!-- Services -->
@ -377,7 +377,7 @@
</service>
<service
android:name=".features.location.LocationSharingAndroidService"
android:name=".features.location.live.tracking.LocationSharingAndroidService"
android:exported="false"
android:foregroundServiceType="location" />

View file

@ -0,0 +1,69 @@
/*
* 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,
)
/**
* 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>>
/**
* Gets the id of the active space, or null if there is none.
*/
fun getSafeActiveSpaceId(): String?
}

View file

@ -16,7 +16,6 @@
package im.vector.app
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import arrow.core.Option
import im.vector.app.core.di.ActiveSessionHolder
@ -46,54 +45,60 @@ import javax.inject.Singleton
/**
* 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
class AppStateHandler @Inject constructor(
class SpaceStateHandlerImpl @Inject constructor(
private val sessionDataSource: ActiveSessionDataSource,
private val uiStateRepository: UiStateRepository,
private val activeSessionHolder: ActiveSessionHolder,
private val analyticsTracker: AnalyticsTracker
) : DefaultLifecycleObserver {
) : SpaceStateHandler {
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private val selectedSpaceDataSource = BehaviorDataSource<Option<RoomSummary>>(Option.empty())
val selectedSpaceFlow = selectedSpaceDataSource.stream()
private val selectedSpaceFlow = selectedSpaceDataSource.stream()
private val spaceBackstack = ArrayDeque<String?>()
fun getCurrentSpace(): RoomSummary? {
override fun getCurrentSpace(): RoomSummary? {
return selectedSpaceDataSource.currentValue?.orNull()?.let { spaceSummary ->
activeSessionHolder.getSafeActiveSession()?.roomService()?.getRoomSummary(spaceSummary.roomId)
}
}
fun setCurrentSpace(spaceId: String?, session: Session? = null, persistNow: Boolean = false, isForwardNavigation: Boolean = true) {
override fun setCurrentSpace(
spaceId: String?,
session: Session?,
persistNow: Boolean,
isForwardNavigation: Boolean,
) {
val activeSession = session ?: activeSessionHolder.getSafeActiveSession() ?: return
val currentSpace = selectedSpaceDataSource.currentValue?.orNull()
val uSession = session ?: activeSessionHolder.getSafeActiveSession() ?: return
if (currentSpace != null && spaceId == currentSpace.roomId) return
val spaceSum = spaceId?.let { uSession.getRoomSummary(spaceId) }
val spaceSummary = spaceId?.let { activeSession.getRoomSummary(spaceId) }
val sameSpaceSelected = currentSpace != null && spaceId == currentSpace.roomId
if (sameSpaceSelected) {
return
}
if (isForwardNavigation) {
spaceBackstack.addLast(currentSpace?.roomId)
}
if (persistNow) {
uiStateRepository.storeSelectedSpace(spaceSum?.roomId, uSession.sessionId)
uiStateRepository.storeSelectedSpace(spaceSummary?.roomId, activeSession.sessionId)
}
if (spaceSum == null) {
if (spaceSummary == null) {
selectedSpaceDataSource.post(Option.empty())
} else {
selectedSpaceDataSource.post(Option.just(spaceSum))
selectedSpaceDataSource.post(Option.just(spaceSummary))
}
if (spaceId != null) {
uSession.coroutineScope.launch(Dispatchers.IO) {
activeSession.coroutineScope.launch(Dispatchers.IO) {
tryOrNull {
uSession.getRoom(spaceId)?.membershipService()?.loadRoomMembersIfNeeded()
activeSession.getRoom(spaceId)?.membershipService()?.loadRoomMembersIfNeeded()
}
}
}
@ -122,9 +127,11 @@ class AppStateHandler @Inject constructor(
}.launchIn(session.coroutineScope)
}
fun getSpaceBackstack() = spaceBackstack
override fun getSpaceBackstack() = spaceBackstack
fun safeActiveSpaceId(): String? {
override fun getSelectedSpaceFlow() = selectedSpaceFlow
override fun getSafeActiveSpaceId(): String? {
return selectedSpaceDataSource.currentValue?.orNull()?.roomId
}

View file

@ -25,22 +25,28 @@ import android.content.res.Configuration
import android.os.Handler
import android.os.HandlerThread
import android.os.StrictMode
import android.view.Gravity
import androidx.core.provider.FontRequest
import androidx.core.provider.FontsContractCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.multidex.MultiDex
import androidx.recyclerview.widget.SnapHelper
import com.airbnb.epoxy.Carousel
import com.airbnb.epoxy.EpoxyAsyncUtil
import com.airbnb.epoxy.EpoxyController
import com.airbnb.mvrx.Mavericks
import com.facebook.stetho.Stetho
import com.gabrielittner.threetenbp.LazyThreeTen
import com.github.rubensousa.gravitysnaphelper.GravitySnapHelper
import com.mapbox.mapboxsdk.Mapbox
import com.vanniktech.emoji.EmojiManager
import com.vanniktech.emoji.google.GoogleEmojiProvider
import dagger.hilt.android.HiltAndroidApp
import im.vector.app.config.Config
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.call.webrtc.WebRtcCallManager
import im.vector.app.features.configuration.VectorConfiguration
@ -88,7 +94,7 @@ class VectorApplication :
@Inject lateinit var vectorPreferences: VectorPreferences
@Inject lateinit var versionProvider: VersionProvider
@Inject lateinit var notificationUtils: NotificationUtils
@Inject lateinit var appStateHandler: AppStateHandler
@Inject lateinit var spaceStateHandler: SpaceStateHandler
@Inject lateinit var popupAlertManager: PopupAlertManager
@Inject lateinit var pinLocker: PinLocker
@Inject lateinit var callManager: WebRtcCallManager
@ -99,6 +105,7 @@ class VectorApplication :
@Inject lateinit var flipperProxy: FlipperProxy
@Inject lateinit var matrix: Matrix
@Inject lateinit var fcmHelper: FcmHelper
@Inject lateinit var buildMeta: BuildMeta
// font thread handler
private var fontThreadHandler: Handler? = null
@ -127,19 +134,20 @@ class VectorApplication :
.filterIsInstance(JitsiMeetDefaultLogHandler::class.java)
.forEach { Timber.uproot(it) }
if (BuildConfig.DEBUG) {
if (buildMeta.isDebug) {
Timber.plant(Timber.DebugTree())
}
Timber.plant(vectorFileLogger)
if (BuildConfig.DEBUG) {
if (buildMeta.isDebug) {
Stetho.initializeWithDefaults(this)
}
logInfo()
LazyThreeTen.init(this)
Mavericks.initialize(debugMode = false)
EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
configureEpoxy()
registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks(popupAlertManager))
val fontRequest = FontRequest(
"com.google.android.gms.fonts",
@ -148,7 +156,7 @@ class VectorApplication :
R.array.com_google_android_gms_fonts_certs
)
FontsContractCompat.requestFont(this, fontRequest, emojiCompatFontProvider, getFontThreadHandler())
VectorLocale.init(this)
VectorLocale.init(this, buildMeta)
ThemeUtils.init(this)
vectorConfiguration.applyToApplicationContext()
@ -177,7 +185,7 @@ class VectorApplication :
fcmHelper.onEnterBackground(activeSessionHolder)
}
})
ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler)
ProcessLifecycleOwner.get().lifecycle.addObserver(spaceStateHandler)
ProcessLifecycleOwner.get().lifecycle.addObserver(pinLocker)
ProcessLifecycleOwner.get().lifecycle.addObserver(callManager)
// This should be done as early as possible
@ -195,8 +203,18 @@ class VectorApplication :
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() {
if (BuildConfig.ENABLE_STRICT_MODE_LOGS) {
if (Config.ENABLE_STRICT_MODE_LOGS) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectAll()

View file

@ -0,0 +1,80 @@
/*
* 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 {
val config: Analytics = 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.HomeDrawerFragment
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.detail.TimelineFragment
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.home.HomeRoomListFragment
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.preview.LocationPreviewFragment
import im.vector.app.features.login.LoginCaptchaFragment
import im.vector.app.features.login.LoginFragment
import im.vector.app.features.login.LoginGenericTextInputFormFragment
@ -258,6 +259,11 @@ interface FragmentModule {
@FragmentKey(HomeDetailFragment::class)
fun bindHomeDetailFragment(fragment: HomeDetailFragment): Fragment
@Binds
@IntoMap
@FragmentKey(NewHomeDetailFragment::class)
fun bindNewHomeDetailFragment(fragment: NewHomeDetailFragment): Fragment
@Binds
@IntoMap
@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.invite.InviteUsersToRoomViewModel
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.login2.LoginViewModel2
import im.vector.app.features.login2.created.AccountCreatedViewModel
@ -605,6 +606,11 @@ interface MavericksViewModelModule {
@MavericksViewModelKey(LocationSharingViewModel::class)
fun createLocationSharingViewModelFactory(factory: LocationSharingViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(LocationPreviewViewModel::class)
fun createLocationPreviewViewModelFactory(factory: LocationPreviewViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(VectorAttachmentViewerViewModel::class)
@ -612,8 +618,8 @@ interface MavericksViewModelModule {
@Binds
@IntoMap
@MavericksViewModelKey(LocationLiveMapViewModel::class)
fun locationLiveMapViewModelFactory(factory: LocationLiveMapViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@MavericksViewModelKey(LiveLocationMapViewModel::class)
fun liveLocationMapViewModelFactory(factory: LiveLocationMapViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap

View file

@ -31,7 +31,9 @@ import dagger.hilt.components.SingletonComponent
import im.vector.app.BuildConfig
import im.vector.app.EmojiCompatWrapper
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.error.DefaultErrorFormatter
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.utils.AndroidSystemSettingsProvider
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.VectorAnalytics
import im.vector.app.features.analytics.impl.DefaultVectorAnalytics
@ -108,6 +109,9 @@ abstract class VectorBindModule {
@Binds
abstract fun bindSystemSettingsProvide(provider: AndroidSystemSettingsProvider): SystemSettingsProvider
@Binds
abstract fun bindSpaceStateHandler(spaceStateHandlerImpl: SpaceStateHandlerImpl): SpaceStateHandler
}
@InstallIn(SingletonComponent::class)
@ -200,17 +204,23 @@ object VectorStaticModule {
return GlobalScope
}
@Provides
fun providesAnalyticsConfig(): AnalyticsConfig {
return analyticsConfig
}
@Provides
fun providesPhoneNumberUtil(): PhoneNumberUtil = PhoneNumberUtil.getInstance()
@Provides
@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
@Singleton

View file

@ -37,7 +37,7 @@ import androidx.datastore.preferences.core.Preferences
import dagger.hilt.EntryPoints
import im.vector.app.core.datastore.dataStoreProvider
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 kotlin.math.roundToInt
@ -93,9 +93,9 @@ fun Context.safeOpenOutputStream(uri: Uri): OutputStream? {
*/
@Suppress("deprecation")
@SuppressLint("NewApi") // false positive
fun Context.inferNoConnectivity(buildMeta: BuildMeta): Boolean {
fun Context.inferNoConnectivity(sdkIntProvider: BuildVersionSdkIntProvider): Boolean {
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)
when {
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
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
) {
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 ->
when (hasFocus) {
false -> action()
false -> lifecycleOwner.lifecycleScope.launchWhenResumed { action() }
else -> {
// do nothing
}

View file

@ -50,10 +50,10 @@ import androidx.viewbinding.ViewBinding
import com.airbnb.mvrx.MavericksView
import com.bumptech.glide.util.Util
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.snackbar.Snackbar
import dagger.hilt.android.EntryPointAccessors
import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
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.singletonEntryPoint
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.ToolbarConfig
import im.vector.app.core.utils.toast
import im.vector.app.features.MainActivity
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.plan.MobileScreen
import im.vector.app.features.configuration.VectorConfiguration
@ -155,11 +157,12 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
protected lateinit var bugReporter: BugReporter
private lateinit var pinLocker: PinLocker
@Inject
lateinit var rageShake: RageShake
@Inject lateinit var rageShake: RageShake
@Inject lateinit var buildMeta: BuildMeta
@Inject lateinit var fontScalePreferences: FontScalePreferences
@Inject
lateinit var fontScalePreferences: FontScalePreferences
lateinit var vectorFeatures: VectorFeatures
lateinit var navigator: Navigator
private set
@ -253,6 +256,14 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
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()
if (titleRes != -1) {
supportActionBar?.let {
@ -409,7 +420,7 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
}
DebugReceiver
.getIntentFilter(this)
.takeIf { BuildConfig.DEBUG }
.takeIf { buildMeta.isDebug }
?.let {
debugReceiver = DebugReceiver()
registerReceiver(debugReceiver, it)

View file

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

View file

@ -16,8 +16,15 @@
package im.vector.app.core.resources
import android.os.Build
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.content.FileProvider
import androidx.core.content.getSystemService
import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.features.notifications.NotificationUtils
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) {
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 {
setDataAndType(uri, mimeType)
@ -214,7 +213,7 @@ fun openLocation(activity: Activity, latitude: Double, longitude: Double) {
fun shareMedia(context: Context, file: File, mediaMimeType: String?) {
val mediaUri = try {
FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileProvider", file)
FileProvider.getUriForFile(context, context.packageName + ".fileProvider", file)
} catch (e: Exception) {
Timber.e(e, "onMediaAction Selected File cannot be shared")
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.
*/
fun openPlayStore(activity: Activity, appId: String = BuildConfig.APPLICATION_ID) {
fun openPlayStore(activity: Activity, appId: String) {
try {
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$appId")))
} catch (activityNotFoundException: ActivityNotFoundException) {

View file

@ -159,7 +159,7 @@ fun startInstallFromSourceIntent(context: Context, activityResultLauncher: Activ
}
fun startSharePlainTextIntent(
fragment: Fragment,
context: Context,
activityResultLauncher: ActivityResultLauncher<Intent>?,
chooserTitle: String?,
text: String,
@ -182,10 +182,10 @@ fun startSharePlainTextIntent(
if (activityResultLauncher != null) {
activityResultLauncher.launch(intent)
} else {
fragment.startActivity(intent)
context.startActivity(intent)
}
} 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
import im.vector.app.BuildConfig
import im.vector.app.config.Config
import im.vector.app.config.OnboardingVariant
interface VectorFeatures {
@ -30,19 +30,14 @@ interface VectorFeatures {
fun isOnboardingCombinedLoginEnabled(): Boolean
fun allowExternalUnifiedPushDistributors(): Boolean
fun isScreenSharingEnabled(): Boolean
fun isLocationSharingEnabled(): Boolean
fun forceUsageOfOpusEncoder(): Boolean
fun shouldStartDmOnFirstMessage(): Boolean
fun isNewAppLayoutEnabled(): Boolean
enum class OnboardingVariant {
LEGACY,
LOGIN_2,
FTUE_AUTH
}
}
class DefaultVectorFeatures : VectorFeatures {
override fun onboardingVariant(): VectorFeatures.OnboardingVariant = BuildConfig.ONBOARDING_VARIANT
override fun onboardingVariant() = Config.ONBOARDING_VARIANT
override fun isOnboardingAlreadyHaveAccountSplashEnabled() = true
override fun isOnboardingSplashCarouselEnabled() = true
override fun isOnboardingUseCaseEnabled() = true
@ -51,6 +46,7 @@ class DefaultVectorFeatures : VectorFeatures {
override fun isOnboardingCombinedLoginEnabled() = true
override fun allowExternalUnifiedPushDistributors(): Boolean = Config.ALLOW_EXTERNAL_UNIFIED_PUSH_DISTRIBUTORS
override fun isScreenSharingEnabled(): Boolean = true
override fun isLocationSharingEnabled() = Config.ENABLE_LOCATION_SHARING
override fun forceUsageOfOpusEncoder(): Boolean = false
override fun shouldStartDmOnFirstMessage(): 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");
* you may not use this file except in compliance with the License.
@ -16,9 +16,9 @@
package im.vector.app.features.analytics
interface AnalyticsConfig {
val isEnabled: Boolean
val postHogHost: String
val postHogApiKey: String
val policyLink: String
}
data class AnalyticsConfig(
val isEnabled: Boolean,
val postHogHost: String,
val postHogApiKey: String,
val policyLink: String,
)

View file

@ -18,11 +18,15 @@ package im.vector.app.features.analytics.impl
import android.content.Context
import com.posthog.android.PostHog
import im.vector.app.BuildConfig
import im.vector.app.config.analyticsConfig
import im.vector.app.core.resources.BuildMeta
import im.vector.app.features.analytics.AnalyticsConfig
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 {
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 {
return if (BuildConfig.DEBUG) {
return if (buildMeta.isDebug) {
PostHog.LogLevel.DEBUG
} else {
PostHog.LogLevel.INFO

View file

@ -29,6 +29,9 @@ import kotlinx.coroutines.flow.map
import org.matrix.android.sdk.api.extensions.orFalse
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")
/**

View file

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

View file

@ -23,9 +23,9 @@ import android.os.Bundle
import androidx.activity.result.ActivityResultLauncher
import im.vector.app.core.dialogs.PhotoOrVideoDialog
import im.vector.app.core.platform.Restorable
import im.vector.app.core.resources.BuildMeta
import im.vector.app.features.settings.VectorPreferences
import im.vector.lib.multipicker.MultiPicker
import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import timber.log.Timber
@ -35,15 +35,14 @@ private const val PENDING_TYPE_KEY = "PENDING_TYPE_KEY"
/**
* 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 {
fun onContactAttachmentReady(contactAttachment: ContactAttachment) {
if (BuildConfig.LOG_PRIVATE_DATA) {
Timber.v("On contact attachment ready: $contactAttachment")
}
}
fun onContactAttachmentReady(contactAttachment: ContactAttachment)
fun onContentAttachmentsReady(attachments: List<ContentAttachmentData>)
}
@ -144,6 +143,9 @@ class AttachmentsHelper(val context: Context, val callback: Callback) : Restorab
.firstOrNull()
?.toContactAttachment()
?.let {
if (buildMeta.lowPrivacyLoggingEnabled) {
Timber.v("On contact attachment ready: $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.LifecycleOwner
import im.vector.app.ActiveSessionDataSource
import im.vector.app.BuildConfig
import im.vector.app.core.pushers.UnifiedPushHelper
import im.vector.app.core.services.CallAndroidService
import im.vector.app.features.analytics.AnalyticsTracker
@ -74,6 +73,7 @@ class WebRtcCallManager @Inject constructor(
private val activeSessionDataSource: ActiveSessionDataSource,
private val analyticsTracker: AnalyticsTracker,
private val unifiedPushHelper: UnifiedPushHelper,
private val voipConfig: VoipConfig,
) : CallListener,
DefaultLifecycleObserver {
@ -444,7 +444,7 @@ class WebRtcCallManager @Inject constructor(
}
override fun onCallAssertedIdentityReceived(callAssertedIdentityContent: CallAssertedIdentityContent) {
if (!BuildConfig.handleCallAssertedIdentityEvents) {
if (!voipConfig.handleCallAssertedIdentityEvents) {
return
}
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 {
startSharePlainTextIntent(
fragment = this,
context = requireContext(),
activityResultLauncher = null,
chooserTitle = context?.getString(R.string.keys_backup_setup_step3_share_intent_chooser_title),
text = recoveryKey,

View file

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

View file

@ -35,8 +35,8 @@ import com.airbnb.mvrx.Mavericks
import com.airbnb.mvrx.viewModel
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.AppStateHandler
import im.vector.app.R
import im.vector.app.SpaceStateHandler
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.registerStartForActivityResult
@ -46,6 +46,7 @@ import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.platform.VectorMenuProvider
import im.vector.app.core.pushers.PushersManager
import im.vector.app.core.pushers.UnifiedPushHelper
import im.vector.app.core.utils.startSharePlainTextIntent
import im.vector.app.databinding.ActivityHomeBinding
import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs
@ -130,7 +131,7 @@ class HomeActivity :
@Inject lateinit var permalinkHandler: PermalinkHandler
@Inject lateinit var avatarRenderer: AvatarRenderer
@Inject lateinit var initSyncStepFormatter: InitSyncStepFormatter
@Inject lateinit var appStateHandler: AppStateHandler
@Inject lateinit var spaceStateHandler: SpaceStateHandler
@Inject lateinit var unifiedPushHelper: UnifiedPushHelper
@Inject lateinit var fcmHelper: FcmHelper
@Inject lateinit var nightlyProxy: NightlyProxy
@ -203,11 +204,16 @@ class HomeActivity :
)
}
}
sharedActionViewModel = viewModelProvider.get(HomeSharedActionViewModel::class.java)
sharedActionViewModel = viewModelProvider[HomeSharedActionViewModel::class.java]
views.drawerLayout.addDrawerListener(drawerListener)
if (isFirstCreation()) {
replaceFragment(views.homeDetailFragmentContainer, HomeDetailFragment::class.java)
replaceFragment(views.homeDrawerFragmentContainer, HomeDrawerFragment::class.java)
if (vectorFeatures.isNewAppLayoutEnabled()) {
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
@ -552,7 +558,7 @@ class HomeActivity :
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) {
menu.findItem(R.id.menu_home_init_sync_legacy).isVisible = vectorPreferences.developerMode()
@ -591,10 +597,29 @@ class HomeActivity :
navigator.openSettings(this)
true
}
R.id.menu_home_invite_friends -> {
launchInviteFriends()
true
}
else -> false
}
}
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() {
if (views.drawerLayout.isDrawerOpen(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.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.config.analyticsConfig
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
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.extensions.toAnalyticsType
import im.vector.app.features.analytics.plan.Signup
@ -80,7 +80,8 @@ class HomeActivityViewModel @AssistedInject constructor(
private val analyticsStore: AnalyticsStore,
private val lightweightSettingsStorage: LightweightSettingsStorage,
private val vectorPreferences: VectorPreferences,
private val analyticsTracker: AnalyticsTracker
private val analyticsTracker: AnalyticsTracker,
private val analyticsConfig: AnalyticsConfig,
) : VectorViewModel<HomeActivityViewState, HomeActivityViewActions, HomeActivityViewEvents>(initialState) {
@AssistedFactory

View file

@ -28,8 +28,8 @@ 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.AppStateHandler
import im.vector.app.R
import im.vector.app.SpaceStateHandler
import im.vector.app.core.extensions.commitTransaction
import im.vector.app.core.extensions.toMvRxBundle
import im.vector.app.core.platform.OnBackPressed
@ -41,7 +41,6 @@ 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.FragmentHomeDetailBinding
import im.vector.app.features.VectorFeatures
import im.vector.app.features.call.SharedKnownCallsViewModel
import im.vector.app.features.call.VectorCallActivity
import im.vector.app.features.call.dialpad.DialPadFragment
@ -49,7 +48,6 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager
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.UnreadCounterBadgeView
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
@ -68,8 +66,7 @@ class HomeDetailFragment @Inject constructor(
private val alertManager: PopupAlertManager,
private val callManager: WebRtcCallManager,
private val vectorPreferences: VectorPreferences,
private val appStateHandler: AppStateHandler,
private val vectorFeatures: VectorFeatures,
private val spaceStateHandler: SpaceStateHandler,
) : VectorBaseFragment<FragmentHomeDetailBinding>(),
KeysBackupBanner.Delegate,
CurrentCallsView.Callback,
@ -186,13 +183,13 @@ class HomeDetailFragment @Inject constructor(
}
private fun navigateBack() {
val previousSpaceId = appStateHandler.getSpaceBackstack().removeLastOrNull()
val parentSpaceId = appStateHandler.getCurrentSpace()?.flattenParentIds?.lastOrNull()
val previousSpaceId = spaceStateHandler.getSpaceBackstack().removeLastOrNull()
val parentSpaceId = spaceStateHandler.getCurrentSpace()?.flattenParentIds?.lastOrNull()
setCurrentSpace(previousSpaceId ?: parentSpaceId)
}
private fun setCurrentSpace(spaceId: String?) {
appStateHandler.setCurrentSpace(spaceId, isForwardNavigation = false)
spaceStateHandler.setCurrentSpace(spaceId, isForwardNavigation = false)
sharedActionViewModel.post(HomeActivitySharedAction.OnCloseSpace)
}
@ -215,7 +212,7 @@ class HomeDetailFragment @Inject constructor(
}
private fun refreshSpaceState() {
appStateHandler.getCurrentSpace()?.let {
spaceStateHandler.getCurrentSpace()?.let {
onSpaceChange(it)
}
}
@ -355,12 +352,8 @@ class HomeDetailFragment @Inject constructor(
if (fragmentToShow == null) {
when (tab) {
is HomeTab.RoomList -> {
if (vectorFeatures.isNewAppLayoutEnabled()) {
add(R.id.roomListContainer, HomeRoomListFragment::class.java, null, fragmentTag)
} else {
val params = RoomListParams(tab.displayMode)
add(R.id.roomListContainer, RoomListFragment::class.java, params.toMvRxBundle(), fragmentTag)
}
val params = RoomListParams(tab.displayMode)
add(R.id.roomListContainer, RoomListFragment::class.java, params.toMvRxBundle(), fragmentTag)
}
is HomeTab.DialPad -> {
add(R.id.roomListContainer, createDialPadFragment(), fragmentTag)
@ -473,7 +466,7 @@ class HomeDetailFragment @Inject constructor(
return this
}
override fun onBackPressed(toolbarButton: Boolean) = if (appStateHandler.getCurrentSpace() != null) {
override fun onBackPressed(toolbarButton: Boolean) = if (spaceStateHandler.getCurrentSpace() != null) {
navigateBack()
true
} else {

View file

@ -22,7 +22,7 @@ import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
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.hiltMavericksViewModelFactory
import im.vector.app.core.extensions.singletonEntryPoint
@ -68,7 +68,7 @@ class HomeDetailViewModel @AssistedInject constructor(
private val vectorDataStore: VectorDataStore,
private val callManager: WebRtcCallManager,
private val directRoomHelper: DirectRoomHelper,
private val appStateHandler: AppStateHandler,
private val spaceStateHandler: SpaceStateHandler,
private val autoAcceptInvites: AutoAcceptInvites,
private val vectorOverrides: VectorOverrides
) : VectorViewModel<HomeDetailViewState, HomeDetailAction, HomeDetailViewEvents>(initialState),
@ -206,7 +206,7 @@ class HomeDetailViewModel @AssistedInject constructor(
}
private fun observeRoomGroupingMethod() {
appStateHandler.selectedSpaceFlow
spaceStateHandler.getSelectedSpaceFlow()
.setOnEach {
copy(
selectedSpace = it.orNull()
@ -215,7 +215,7 @@ class HomeDetailViewModel @AssistedInject constructor(
}
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
// the actual models
session.roomService().getPagedRoomSummariesLive(
@ -227,7 +227,7 @@ class HomeDetailViewModel @AssistedInject constructor(
}
.throttleFirst(300)
.onEach {
val activeSpaceRoomId = appStateHandler.getCurrentSpace()?.roomId
val activeSpaceRoomId = spaceStateHandler.getCurrentSpace()?.roomId
var dmInvites = 0
var roomsInvite = 0
if (autoAcceptInvites.showInvites()) {

View file

@ -23,11 +23,11 @@ import android.view.ViewGroup
import androidx.core.app.ActivityOptionsCompat
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.core.extensions.observeK
import im.vector.app.core.extensions.replaceChildFragment
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.databinding.FragmentHomeDrawerBinding
import im.vector.app.features.analytics.plan.MobileScreen
@ -43,7 +43,8 @@ import javax.inject.Inject
class HomeDrawerFragment @Inject constructor(
private val session: Session,
private val vectorPreferences: VectorPreferences,
private val avatarRenderer: AvatarRenderer
private val avatarRenderer: AvatarRenderer,
private val buildMeta: BuildMeta,
) : VectorBaseFragment<FragmentHomeDrawerBinding>() {
private lateinit var sharedActionViewModel: HomeSharedActionViewModel
@ -102,7 +103,7 @@ class HomeDrawerFragment @Inject constructor(
val text = getString(R.string.invite_friends_text, permalink)
startSharePlainTextIntent(
fragment = this,
context = requireContext(),
activityResultLauncher = null,
chooserTitle = getString(R.string.invite_friends),
text = text,
@ -112,7 +113,7 @@ class HomeDrawerFragment @Inject constructor(
}
// Debug menu
views.homeDrawerHeaderDebugView.isVisible = BuildConfig.DEBUG && vectorPreferences.developerMode()
views.homeDrawerHeaderDebugView.isVisible = buildMeta.isDebug && vectorPreferences.developerMode()
views.homeDrawerHeaderDebugView.debouncedClicks {
sharedActionViewModel.post(HomeActivitySharedAction.CloseDrawer)
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.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import im.vector.app.BuildConfig
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.features.MainActivity
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 const val adaptiveIconSizeDp = 108
private const val adaptiveIconOuterSidesDp = 18
private const val directShareCategory = BuildConfig.APPLICATION_ID + ".SHORTCUT_SHARE"
class ShortcutCreator @Inject constructor(
private val context: Context,
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 adaptiveIconOuterSides = dimensionConverter.dpToPx(adaptiveIconOuterSidesDp)
private val iconSize by lazy {

View file

@ -22,7 +22,7 @@ import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
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.hiltMavericksViewModelFactory
import im.vector.app.core.platform.EmptyAction
@ -58,7 +58,7 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(
@Assisted initialState: UnreadMessagesState,
session: Session,
private val vectorPreferences: VectorPreferences,
appStateHandler: AppStateHandler,
spaceStateHandler: SpaceStateHandler,
private val autoAcceptInvites: AutoAcceptInvites
) :
VectorViewModel<UnreadMessagesState, EmptyAction, EmptyViewEvents>(initialState) {
@ -109,8 +109,8 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(
}
combine(
appStateHandler.selectedSpaceFlow.distinctUntilChanged(),
appStateHandler.selectedSpaceFlow.flatMapLatest {
spaceStateHandler.getSelectedSpaceFlow().distinctUntilChanged(),
spaceStateHandler.getSelectedSpaceFlow().flatMapLatest {
roomService.getPagedRoomSummariesLive(
roomSummaryQueryParams {
this.memberships = Membership.activeMemberships()
@ -162,10 +162,10 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(
CountInfo(
homeCount = counts,
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) +
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) +
spaceInviteCount
)

View file

@ -67,7 +67,6 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.vanniktech.emoji.EmojiPopup
import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.core.animations.play
import im.vector.app.core.dialogs.ConfirmationDialogBuilder
@ -92,6 +91,7 @@ import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.platform.VectorMenuProvider
import im.vector.app.core.platform.lifecycleAwareLazy
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.UserPreferencesProvider
import im.vector.app.core.time.Clock
@ -125,6 +125,7 @@ import im.vector.app.core.utils.startInstallFromSourceIntent
import im.vector.app.core.utils.toast
import im.vector.app.databinding.DialogReportContentBinding
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.plan.Interaction
import im.vector.app.features.analytics.plan.MobileScreen
@ -275,7 +276,9 @@ class TimelineFragment @Inject constructor(
private val callManager: WebRtcCallManager,
private val audioMessagePlaybackTracker: AudioMessagePlaybackTracker,
private val shareIntentHandler: ShareIntentHandler,
private val clock: Clock
private val clock: Clock,
private val vectorFeatures: VectorFeatures,
private val buildMeta: BuildMeta,
) :
VectorBaseFragment<FragmentTimelineBinding>(),
TimelineEventController.Callback,
@ -372,7 +375,7 @@ class TimelineFragment @Inject constructor(
sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java)
sharedActivityActionViewModel = activityViewModelProvider.get(RoomDetailSharedActionViewModel::class.java)
knownCallsViewModel = activityViewModelProvider.get(SharedKnownCallsViewModel::class.java)
attachmentsHelper = AttachmentsHelper(requireContext(), this).register()
attachmentsHelper = AttachmentsHelper(requireContext(), this, buildMeta).register()
callActionsHandler = StartCallActionsHandler(
roomId = timelineArgs.roomId,
fragment = this,
@ -656,8 +659,8 @@ class TimelineFragment @Inject constructor(
)
}
private fun navigateToLocationLiveMap() {
navigator.openLocationLiveMap(
private fun navigateToLiveLocationMap() {
navigator.openLiveLocationMap(
context = requireContext(),
roomId = timelineArgs.roomId
)
@ -857,11 +860,11 @@ class TimelineFragment @Inject constructor(
}
private fun setupLiveLocationIndicator() {
views.locationLiveStatusIndicator.stopButton.debouncedClicks {
views.liveLocationStatusIndicator.stopButton.debouncedClicks {
timelineViewModel.handle(RoomDetailAction.StopLiveLocationSharing)
}
views.locationLiveStatusIndicator.debouncedClicks {
navigateToLocationLiveMap()
views.liveLocationStatusIndicator.debouncedClicks {
navigateToLiveLocationMap()
}
}
@ -962,7 +965,7 @@ class TimelineFragment @Inject constructor(
}
override fun onDestroyView() {
audioMessagePlaybackTracker.makeAllPlaybacksIdle()
messageComposerViewModel.endAllVoiceActions()
lazyLoadedViews.unBind()
timelineEventController.callback = null
timelineEventController.removeModelBuildListener(modelBuildListener)
@ -1095,7 +1098,7 @@ class TimelineFragment @Inject constructor(
else -> state.isAllowedToManageWidgets
}
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 widgetsCount = state.activeRoomWidgets.invoke()?.size ?: 0
@ -1559,7 +1562,7 @@ class TimelineFragment @Inject constructor(
attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@TimelineFragment)
attachmentTypeSelector.setAttachmentVisibility(
AttachmentTypeSelectorView.Type.LOCATION,
BuildConfig.enableLocationSharing
vectorFeatures.isLocationSharingEnabled(),
)
attachmentTypeSelector.setAttachmentVisibility(
AttachmentTypeSelectorView.Type.POLL, !isThreadTimeLine()
@ -1688,7 +1691,7 @@ class TimelineFragment @Inject constructor(
}
private fun updateLiveLocationIndicator(isSharingLiveLocation: Boolean) {
views.locationLiveStatusIndicator.isVisible = isSharingLiveLocation
views.liveLocationStatusIndicator.isVisible = isSharingLiveLocation
}
private fun FragmentTimelineBinding.hideComposerViews() {
@ -2068,7 +2071,7 @@ class TimelineFragment @Inject constructor(
handleShowLocationPreview(messageContent, informationData.senderId)
}
is MessageBeaconInfoContent -> {
navigateToLocationLiveMap()
navigateToLiveLocationMap()
}
else -> {
val handled = onThreadSummaryClicked(informationData.eventId, isRootThreadEvent)
@ -2646,7 +2649,6 @@ class TimelineFragment @Inject constructor(
}
override fun onContactAttachmentReady(contactAttachment: ContactAttachment) {
super.onContactAttachmentReady(contactAttachment)
val formattedContact = contactAttachment.toHumanReadable()
messageComposerViewModel.handle(MessageComposerAction.SendMessage(formattedContact, false))
}

View file

@ -27,12 +27,13 @@ import com.airbnb.mvrx.Uninitialized
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.AppStateHandler
import im.vector.app.R
import im.vector.app.SpaceStateHandler
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.mvrx.runCatchingToAsync
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.utils.BehaviorDataSource
import im.vector.app.features.analytics.AnalyticsTracker
@ -52,10 +53,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.url.PreviewUrlRetriever
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.tracking.LocationSharingServiceConnection
import im.vector.app.features.notifications.NotificationDrawerManager
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.withElementWellKnown
import im.vector.app.features.session.coroutineScope
@ -137,8 +139,10 @@ class TimelineViewModel @AssistedInject constructor(
private val locationSharingServiceConnection: LocationSharingServiceConnection,
private val stopLiveLocationShareUseCase: StopLiveLocationShareUseCase,
private val redactLiveLocationShareEventUseCase: RedactLiveLocationShareEventUseCase,
private val cryptoConfig: CryptoConfig,
buildMeta: BuildMeta,
timelineFactory: TimelineFactory,
appStateHandler: AppStateHandler,
spaceStateHandler: SpaceStateHandler,
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener, LocationSharingServiceConnection.Callback {
@ -150,7 +154,7 @@ class TimelineViewModel @AssistedInject constructor(
val timeline = timelineFactory.createTimeline(viewModelScope, room, eventId, initialState.rootThreadEventId)
// 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
var pendingAction: RoomDetailAction? = null
@ -205,7 +209,7 @@ class TimelineViewModel @AssistedInject constructor(
// Ensure to share the outbound session keys with all members
if (room.roomCryptoService().isEncrypted()) {
rawService.withElementWellKnown(viewModelScope, session.sessionParams) {
val strategy = it.getOutboundSessionKeySharingStrategyOrDefault()
val strategy = it.getOutboundSessionKeySharingStrategyOrDefault(cryptoConfig.fallbackKeySharingStrategy)
if (strategy == OutboundSessionKeySharingStrategy.WhenEnteringRoom) {
prepareForEncryption()
}
@ -220,16 +224,16 @@ class TimelineViewModel @AssistedInject constructor(
if (initialState.switchToParentSpace) {
// 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
appStateHandler.getCurrentSpace().let { currentSpace ->
spaceStateHandler.getCurrentSpace().let { currentSpace ->
val currentRoomSummary = room.roomSummary() ?: return@let
// nothing we are good
if ((currentSpace == null && !vectorPreferences.prefSpacesShowAllRoomInHome()) ||
(currentSpace != null && !currentRoomSummary.flattenParentIds.contains(currentSpace.roomId))) {
// take first one or switch to home
appStateHandler.setCurrentSpace(
spaceStateHandler.setCurrentSpace(
currentRoomSummary
.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
persistNow = true
)
@ -688,7 +692,7 @@ class TimelineViewModel @AssistedInject constructor(
// Ensure outbound session keys
if (room.roomCryptoService().isEncrypted()) {
rawService.withElementWellKnown(viewModelScope, session.sessionParams) {
val strategy = it.getOutboundSessionKeySharingStrategyOrDefault()
val strategy = it.getOutboundSessionKeySharingStrategyOrDefault(cryptoConfig.fallbackKeySharingStrategy)
if (strategy == OutboundSessionKeySharingStrategy.WhenTyping && action.focused) {
// Should we add some rate limit here, or do it only once per model lifecycle?
prepareForEncryption()

View file

@ -20,7 +20,7 @@ import android.content.Context
import android.media.AudioAttributes
import android.media.MediaPlayer
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.voice.VoiceFailure
import im.vector.app.features.voice.VoiceRecorder
@ -43,6 +43,7 @@ import javax.inject.Inject
class AudioMessageHelper @Inject constructor(
private val context: Context,
private val playbackTracker: AudioMessagePlaybackTracker,
private val buildMeta: BuildMeta,
voiceRecorderProvider: VoiceRecorderProvider
) {
private var mediaPlayer: MediaPlayer? = null
@ -88,7 +89,7 @@ class AudioMessageHelper @Inject constructor(
try {
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
.toMultiPickerAudioType(context)
?.apply {

View file

@ -41,7 +41,6 @@ sealed class MessageComposerAction : VectorViewModelAction {
object PauseRecordingVoiceMessage : MessageComposerAction()
data class PlayOrPauseVoicePlayback(val eventId: String, val messageAudioContent: MessageAudioContent) : 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 VoiceWaveformMovedTo(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)
MessageComposerAction.PauseRecordingVoiceMessage -> handlePauseRecordingVoiceMessage()
MessageComposerAction.PlayOrPauseRecordingPlayback -> handlePlayOrPauseRecordingPlayback()
is MessageComposerAction.EndAllVoiceActions -> handleEndAllVoiceActions(action.deleteRecord)
is MessageComposerAction.InitializeVoiceRecorder -> handleInitializeVoiceRecorder(action.attachmentData)
is MessageComposerAction.OnEntersBackground -> handleEntersBackground(action.composerText)
is MessageComposerAction.VoiceWaveformTouchedUp -> handleVoiceWaveformTouchedUp(action)
@ -887,7 +886,7 @@ class MessageComposerViewModel @AssistedInject constructor(
audioMessageHelper.startOrPauseRecordingPlayback()
}
private fun handleEndAllVoiceActions(deleteRecord: Boolean) {
fun endAllVoiceActions(deleteRecord: Boolean = true) {
audioMessageHelper.clearTracker()
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 androidx.constraintlayout.widget.ConstraintLayout
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.core.hardware.vibrate
import im.vector.app.core.time.Clock
@ -57,6 +56,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
}
@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.
@Suppress("UNNECESSARY_LATEINIT")
@ -202,7 +202,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
private fun onRecordingTick(isLocked: Boolean, milliseconds: Long) {
voiceMessageViews.renderRecordingTimer(isLocked, milliseconds / 1_000)
val timeDiffToRecordingLimit = BuildConfig.VOICE_MESSAGE_DURATION_LIMIT_MS - milliseconds
val timeDiffToRecordingLimit = voiceMessageConfig.lengthLimitMs - milliseconds
if (timeDiffToRecordingLimit <= 0) {
post {
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.style.TimelineMessageLayout
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>(
@LayoutRes layoutId: Int = R.layout.item_timeline_event_base
@ -86,8 +88,10 @@ abstract class AbsMessageLocationItem<H : AbsMessageLocationItem.Holder>(
target: Target<Drawable>?,
isFirstResource: Boolean
): Boolean {
holder.staticMapPinImageView.setImageResource(R.drawable.ic_location_pin_failed)
holder.staticMapErrorTextView.isVisible = true
holder.staticMapPinImageView.setImageDrawable(null)
holder.staticMapLoadingErrorView.isVisible = true
val mapErrorViewState = MapLoadingErrorViewState(imageCornerTransformation)
holder.staticMapLoadingErrorView.render(mapErrorViewState)
holder.staticMapCopyrightTextView.isVisible = false
return false
}
@ -103,7 +107,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
holder.staticMapPinImageView.setImageDrawable(pinDrawable)
}
holder.staticMapErrorTextView.isVisible = false
holder.staticMapLoadingErrorView.isVisible = false
holder.staticMapCopyrightTextView.isVisible = true
return false
}
@ -115,7 +119,7 @@ abstract class AbsMessageLocationItem<H : AbsMessageLocationItem.Holder>(
abstract class Holder(@IdRes stubId: Int) : AbsMessageItem.Holder(stubId) {
val staticMapImageView by bind<ImageView>(R.id.staticMapImageView)
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)
}
}

View file

@ -42,8 +42,8 @@ abstract class MessageLiveLocationInactiveItem :
override fun getViewStubId() = STUB_ID
class Holder : AbsMessageItem.Holder(STUB_ID) {
val bannerImageView by bind<ImageView>(R.id.locationLiveEndedBannerBackground)
val noLocationMapImageView by bind<ImageView>(R.id.locationLiveInactiveMap)
val bannerImageView by bind<ImageView>(R.id.liveLocationEndedBannerBackground)
val noLocationMapImageView by bind<ImageView>(R.id.liveLocationInactiveMap)
}
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.features.home.room.detail.RoomDetailAction
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.LocationLiveRunningBannerView
import im.vector.app.features.location.live.LiveLocationMessageBannerViewState
import im.vector.app.features.location.live.LiveLocationRunningBannerView
import org.threeten.bp.LocalDateTime
@EpoxyModelClass
@ -44,17 +44,17 @@ abstract class MessageLiveLocationItem : AbsMessageLocationItem<MessageLiveLocat
override fun bind(holder: 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
val isEmitter = currentUserId != null && currentUserId == locationUserId
val messageLayout = attributes.informationData.messageLayout
val viewState = buildViewState(holder, messageLayout, isEmitter)
holder.locationLiveRunningBanner.isVisible = true
holder.locationLiveRunningBanner.render(viewState)
holder.locationLiveRunningBanner.stopButton.setOnClickListener {
holder.liveLocationRunningBanner.isVisible = true
holder.liveLocationRunningBanner.render(viewState)
holder.liveLocationRunningBanner.stopButton.setOnClickListener {
attributes.callback?.onTimelineItemAction(RoomDetailAction.StopLiveLocationSharing)
}
}
@ -63,24 +63,24 @@ abstract class MessageLiveLocationItem : AbsMessageLocationItem<MessageLiveLocat
holder: Holder,
messageLayout: TimelineMessageLayout,
isEmitter: Boolean
): LocationLiveMessageBannerViewState {
): LiveLocationMessageBannerViewState {
return when {
messageLayout is TimelineMessageLayout.Bubble && isEmitter ->
LocationLiveMessageBannerViewState.Emitter(
LiveLocationMessageBannerViewState.Emitter(
remainingTimeInMillis = getRemainingTimeOfLiveInMillis(),
bottomStartCornerRadiusInDp = messageLayout.cornersRadius.bottomStartRadius,
bottomEndCornerRadiusInDp = messageLayout.cornersRadius.bottomEndRadius,
isStopButtonCenteredVertically = false
)
messageLayout is TimelineMessageLayout.Bubble ->
LocationLiveMessageBannerViewState.Watcher(
LiveLocationMessageBannerViewState.Watcher(
bottomStartCornerRadiusInDp = messageLayout.cornersRadius.bottomStartRadius,
bottomEndCornerRadiusInDp = messageLayout.cornersRadius.bottomEndRadius,
formattedLocalTimeOfEndOfLive = getFormattedLocalTimeEndOfLive(),
)
isEmitter -> {
val cornerRadius = getBannerCornerRadiusForDefaultLayout(holder)
LocationLiveMessageBannerViewState.Emitter(
LiveLocationMessageBannerViewState.Emitter(
remainingTimeInMillis = getRemainingTimeOfLiveInMillis(),
bottomStartCornerRadiusInDp = cornerRadius,
bottomEndCornerRadiusInDp = cornerRadius,
@ -89,7 +89,7 @@ abstract class MessageLiveLocationItem : AbsMessageLocationItem<MessageLiveLocat
}
else -> {
val cornerRadius = getBannerCornerRadiusForDefaultLayout(holder)
LocationLiveMessageBannerViewState.Watcher(
LiveLocationMessageBannerViewState.Watcher(
bottomStartCornerRadiusInDp = cornerRadius,
bottomEndCornerRadiusInDp = cornerRadius,
formattedLocalTimeOfEndOfLive = getFormattedLocalTimeEndOfLive(),
@ -112,7 +112,7 @@ abstract class MessageLiveLocationItem : AbsMessageLocationItem<MessageLiveLocat
override fun getViewStubId() = 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 {

View file

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

View file

@ -16,7 +16,7 @@
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.Dispatchers
import kotlinx.coroutines.launch
@ -27,7 +27,8 @@ import org.matrix.android.sdk.api.session.room.timeline.getLatestEventId
class PreviewUrlRetriever(
session: Session,
private val coroutineScope: CoroutineScope
private val coroutineScope: CoroutineScope,
private val buildMeta: BuildMeta,
) {
private val mediaService = session.mediaService()
@ -77,7 +78,7 @@ class PreviewUrlRetriever(
mediaService.getPreviewUrl(
url = urlToRetrieve,
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(
{

View file

@ -23,8 +23,8 @@ import androidx.lifecycle.asFlow
import androidx.lifecycle.liveData
import androidx.paging.PagedList
import com.airbnb.mvrx.Async
import im.vector.app.AppStateHandler
import im.vector.app.R
import im.vector.app.SpaceStateHandler
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.RoomListDisplayMode
import im.vector.app.features.invite.AutoAcceptInvites
@ -59,7 +59,7 @@ import timber.log.Timber
class RoomListSectionBuilder(
private val session: Session,
private val stringProvider: StringProvider,
private val appStateHandler: AppStateHandler,
private val spaceStateHandler: SpaceStateHandler,
private val viewModelScope: CoroutineScope,
private val autoAcceptInvites: AutoAcceptInvites,
private val onUpdatable: (UpdatableLivePageResult) -> Unit,
@ -95,7 +95,7 @@ class RoomListSectionBuilder(
}
}
appStateHandler.selectedSpaceFlow
spaceStateHandler.getSelectedSpaceFlow()
.distinctUntilChanged()
.onEach { selectedSpaceOption ->
val selectedSpace = selectedSpaceOption.orNull()
@ -150,7 +150,7 @@ class RoomListSectionBuilder(
) {
it.memberships = listOf(Membership.JOIN)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
it.roomTagQueryFilter = RoomTagQueryFilter(false, false, false)
it.roomTagQueryFilter = RoomTagQueryFilter(isFavorite = false, isLowPriority = false, isServerNotice = false)
}
addSection(
@ -187,7 +187,7 @@ class RoomListSectionBuilder(
// add suggested rooms
val suggestedRoomsFlow = // MutableLiveData<List<SpaceChildInfo>>()
appStateHandler.selectedSpaceFlow
spaceStateHandler.getSelectedSpaceFlow()
.distinctUntilChanged()
.flatMapLatest { selectedSpaceOption ->
val selectedSpace = selectedSpaceOption.orNull()
@ -271,7 +271,7 @@ class RoomListSectionBuilder(
) {
it.memberships = listOf(Membership.JOIN)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM
it.roomTagQueryFilter = RoomTagQueryFilter(false, false, null)
it.roomTagQueryFilter = RoomTagQueryFilter(isFavorite = false, isLowPriority = false, isServerNotice = null)
}
addSection(
@ -283,7 +283,7 @@ class RoomListSectionBuilder(
) {
it.memberships = listOf(Membership.JOIN)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM
it.roomTagQueryFilter = RoomTagQueryFilter(false, true, null)
it.roomTagQueryFilter = RoomTagQueryFilter(isFavorite = false, isLowPriority = true, isServerNotice = null)
}
}
@ -360,7 +360,7 @@ class RoomListSectionBuilder(
query: (RoomSummaryQueryParams.Builder) -> Unit
) {
withQueryParams(query) { roomQueryParams ->
val updatedQueryParams = roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId())
val updatedQueryParams = roomQueryParams.process(spaceFilterStrategy, spaceStateHandler.getSafeActiveSpaceId())
val liveQueryParams = MutableStateFlow(updatedQueryParams)
val itemCountFlow = liveQueryParams
.flatMapLatest {
@ -371,7 +371,7 @@ class RoomListSectionBuilder(
val name = stringProvider.getString(nameRes)
val filteredPagedRoomSummariesLive = session.roomService().getFilteredPagedRoomSummariesLive(
roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId()),
roomQueryParams.process(spaceFilterStrategy, spaceStateHandler.getSafeActiveSpaceId()),
pagedListConfig
)
when (spaceFilterStrategy) {
@ -418,7 +418,7 @@ class RoomListSectionBuilder(
RoomAggregateNotificationCount(it.size, it.size)
} else {
session.roomService().getNotificationCountForRooms(
roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId())
roomQueryParams.process(spaceFilterStrategy, spaceStateHandler.getSafeActiveSpaceId())
)
}
)

View file

@ -25,7 +25,7 @@ import com.airbnb.mvrx.Success
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
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.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel
@ -60,7 +60,7 @@ class RoomListViewModel @AssistedInject constructor(
@Assisted initialState: RoomListViewState,
private val session: Session,
stringProvider: StringProvider,
appStateHandler: AppStateHandler,
spaceStateHandler: SpaceStateHandler,
vectorPreferences: VectorPreferences,
autoAcceptInvites: AutoAcceptInvites,
private val analyticsTracker: AnalyticsTracker
@ -100,7 +100,7 @@ class RoomListViewModel @AssistedInject constructor(
observeMembershipChanges()
observeLocalRooms()
appStateHandler.selectedSpaceFlow
spaceStateHandler.getSelectedSpaceFlow()
.distinctUntilChanged()
.execute {
copy(
@ -146,17 +146,17 @@ class RoomListViewModel @AssistedInject constructor(
companion object : MavericksViewModelFactory<RoomListViewModel, RoomListViewState> by hiltMavericksViewModelFactory()
private val roomListSectionBuilder = RoomListSectionBuilder(
session,
stringProvider,
appStateHandler,
viewModelScope,
autoAcceptInvites,
{
updatableQuery = it
},
suggestedRoomJoiningState,
!vectorPreferences.prefSpacesShowAllRoomInHome()
)
session,
stringProvider,
spaceStateHandler,
viewModelScope,
autoAcceptInvites,
{
updatableQuery = it
},
suggestedRoomJoiningState,
!vectorPreferences.prefSpacesShowAllRoomInHome()
)
val sections: List<RoomsSection> by lazy {
roomListSectionBuilder.buildSections(initialState.displayMode)

View file

@ -30,6 +30,7 @@ import com.airbnb.mvrx.withState
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R
import im.vector.app.core.epoxy.LayoutManagerStateRestorer
import im.vector.app.core.extensions.cleanup
import im.vector.app.core.platform.StateView
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.UserPreferencesProvider
@ -43,6 +44,7 @@ import im.vector.app.features.home.room.list.RoomSummaryPagedController
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedAction
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
import im.vector.app.features.home.room.list.home.recent.RecentRoomCarouselController
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.session.room.model.RoomSummary
@ -53,7 +55,8 @@ import javax.inject.Inject
class HomeRoomListFragment @Inject constructor(
private val roomSummaryItemFactory: RoomSummaryItemFactory,
private val userPreferencesProvider: UserPreferencesProvider
private val userPreferencesProvider: UserPreferencesProvider,
private val recentRoomCarouselController: RecentRoomCarouselController
) : VectorBaseFragment<FragmentRoomListBinding>(),
RoomListListener {
@ -180,6 +183,12 @@ class HomeRoomListFragment @Inject constructor(
}
}.adapter
}
is HomeRoomSection.RecentRoomsData -> recentRoomCarouselController.also { controller ->
controller.listener = this
data.list.observe(viewLifecycleOwner) { list ->
controller.submitList(list)
}
}.adapter
}
}
@ -192,6 +201,12 @@ class HomeRoomListFragment @Inject constructor(
)
}
override fun onDestroyView() {
views.roomListView.cleanup()
recentRoomCarouselController.listener = null
super.onDestroyView()
}
// region RoomListListener
override fun onRoomClicked(room: RoomSummary) {

View file

@ -22,7 +22,7 @@ import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
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.hiltMavericksViewModelFactory
import im.vector.app.core.platform.StateView
@ -37,6 +37,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.query.SpaceFilter
import org.matrix.android.sdk.api.query.toActiveSpaceOrNoFilter
import org.matrix.android.sdk.api.query.toActiveSpaceOrOrphanRooms
@ -45,12 +46,13 @@ import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.state.isPublic
class HomeRoomListViewModel @AssistedInject constructor(
@Assisted initialState: HomeRoomListViewState,
private val session: Session,
private val appStateHandler: AppStateHandler,
private val spaceStateHandler: SpaceStateHandler,
private val vectorPreferences: VectorPreferences,
) : VectorViewModel<HomeRoomListViewState, HomeRoomListAction, HomeRoomListViewEvents>(initialState) {
@ -78,6 +80,7 @@ class HomeRoomListViewModel @AssistedInject constructor(
private fun configureSections() {
val newSections = mutableSetOf<HomeRoomSection>()
newSections.add(getRecentRoomsSection())
newSections.add(getAllRoomsSection())
viewModelScope.launch {
@ -89,6 +92,18 @@ class HomeRoomListViewModel @AssistedInject constructor(
}
}
private fun getRecentRoomsSection(): HomeRoomSection {
val liveList = session.roomService()
.getBreadcrumbsLive(roomSummaryQueryParams {
displayName = QueryStringValue.NoCondition
memberships = listOf(Membership.JOIN)
})
return HomeRoomSection.RecentRoomsData(
list = liveList
)
}
private fun getAllRoomsSection(): HomeRoomSection.RoomSummaryData {
val builder = RoomSummaryQueryParams.Builder().also {
it.memberships = listOf(Membership.JOIN)
@ -99,10 +114,10 @@ class HomeRoomListViewModel @AssistedInject constructor(
pagedListConfig
)
appStateHandler.selectedSpaceFlow
spaceStateHandler.getSelectedSpaceFlow()
.distinctUntilChanged()
.onStart {
emit(appStateHandler.getCurrentSpace().toOption())
emit(spaceStateHandler.getCurrentSpace().toOption())
}
.onEach { selectedSpaceOption ->
val selectedSpace = selectedSpaceOption.orNull()

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