diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index c4eccd5b1f..a7daaac14b 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -57,8 +57,9 @@ body: id: homeserver attributes: label: Homeserver - description: Which server is your account registered on? - placeholder: e.g. matrix.org + description: | + Which server is your account registered on? If it is a local or non-public homeserver, please tell us what is the homeserver implementation (ex: Synapse/Dendrite/etc.) and the version. + placeholder: e.g. matrix.org or Synapse 1.50.0rc1 validations: required: false - type: dropdown diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8c2f1041e0..0573461e7a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,6 +10,8 @@ updates: directory: "/" schedule: interval: "weekly" + ignore: + - dependency-name: "*github-script*" # Updates for Gradle dependencies used in the app - package-ecosystem: gradle directory: "/" diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml deleted file mode 100644 index c18ca69fde..0000000000 --- a/.github/workflows/integration.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: Integration Test - -on: - pull_request: { } - push: - branches: [ main, develop ] - -# Enrich gradle.properties for CI/CD -env: - CI_GRADLE_ARG_PROPERTIES: > - -Porg.gradle.jvmargs=-Xmx2g - -Porg.gradle.parallel=false - -jobs: - # Temporary add build of Android tests, which cannot be run on the CI right now, but they need to at least compile - # So it will be mandatory for this action to be successful on every PRs - compile-android-test: - name: Compile Android tests - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/cache@v2 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle- - - name: Compile Android tests - run: ./gradlew clean assembleAndroidTest $CI_GRADLE_ARG_PROPERTIES --stacktrace -PallWarningsAsErrors=false - - integration-tests: - name: Integration Tests (Synapse) - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - api-level: [28] - steps: - - uses: actions/checkout@v2 - - uses: gradle/wrapper-validation-action@v1 - - uses: actions/setup-java@v2 - with: - distribution: 'adopt' - java-version: 11 - - name: Set up Python 3.8 - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - name: Cache pip - uses: actions/cache@v2 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip - restore-keys: | - ${{ runner.os }}-pip- - ${{ runner.os }}- - - uses: actions/cache@v2 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle- - - name: Start synapse server - run: | - python3 -m venv .synapse - source .synapse/bin/activate - pip install synapse matrix-synapse - curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh --no-rate-limit \ - | sed s/127.0.0.1/0.0.0.0/g | bash - - name: Run integration tests on API ${{ matrix.api-level }} - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: ${{ matrix.api-level }} - #arch: x86_64 - #disable-animations: true - # script: ./gradlew -PallWarningsAsErrors=false vector:connectedAndroidTest matrix-sdk-android:connectedAndroidTest - arch: x86 - profile: Nexus 5X - force-avd-creation: false - emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none - emulator-build: 7425822 - script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedCheck --stacktrace diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml new file mode 100644 index 0000000000..bf78356947 --- /dev/null +++ b/.github/workflows/integration_tests.yml @@ -0,0 +1,208 @@ +name: Integration Tests + +on: + pull_request: { } + push: + branches: [ main, develop ] + +# Enrich gradle.properties for CI/CD +env: + CI_GRADLE_ARG_PROPERTIES: > + -Porg.gradle.jvmargs=-Xmx2g + -Porg.gradle.parallel=false + +jobs: + # Build Android Tests [Matrix SDK] + build-android-test-matrix-sdk: + name: Matrix SDK - Build Android Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Build Android Tests for matrix-sdk-android + run: ./gradlew clean matrix-sdk-android:assembleAndroidTest $CI_GRADLE_ARG_PROPERTIES --stacktrace -PallWarningsAsErrors=false + + # Build Android Tests [Matrix APP] + build-android-test-app: + name: App - Build Android Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Build Android Tests for vector + run: ./gradlew clean vector:assembleAndroidTest $CI_GRADLE_ARG_PROPERTIES --stacktrace -PallWarningsAsErrors=false + + # Run Android Tests + integration-tests: + name: Matrix SDK - Running Integration Tests + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + api-level: [ 28 ] + steps: + - uses: actions/checkout@v2 + - uses: gradle/wrapper-validation-action@v1 + - uses: actions/setup-java@v2 + with: + distribution: 'adopt' + java-version: 11 + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Cache pip + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + - uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Start synapse server + run: | + python3 -m venv .synapse + source .synapse/bin/activate + pip install synapse matrix-synapse + curl https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh -o start.sh + chmod 777 start.sh + ./start.sh --no-rate-limit +# package: org.matrix.android.sdk.session + - name: Run integration tests for Matrix SDK [org.matrix.android.sdk.session] API[${{ matrix.api-level }}] + continue-on-error: true + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + arch: x86 + profile: Nexus 5X + force-avd-creation: false + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + emulator-build: 7425822 + script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.package='org.matrix.android.sdk.session' matrix-sdk-android:connectedDebugAndroidTest + - name: Read Results [org.matrix.android.sdk.session] + continue-on-error: true + id: get-comment-body-session + run: | + body="$(cat ./matrix-sdk-android/build/outputs/androidTest-results/connected/*.xml | grep "${{ steps.get-comment-body-session.outputs.session }} + - `[org.matrix.android.sdk.account]`
${{ steps.get-comment-body-account.outputs.account }} + - `[org.matrix.android.sdk.internal]`
${{ steps.get-comment-body-internal.outputs.internal }} + - `[org.matrix.android.sdk.ordering]`
${{ steps.get-comment-body-ordering.outputs.ordering }} + - `[org.matrix.android.sdk.PermalinkParserTest]`
${{ steps.get-comment-body-permalink.outputs.permalink }} + edit-mode: replace +## Useful commands +# script: ./integration_tests_script.sh +# script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.package='org.matrix.android.sdk.session' matrix-sdk-android:connectedDebugAndroidTest --info +# script: ./gradlew $CI_GRADLE_ARG_PROPERTIES matrix-sdk-android:connectedAndroidTest --info +# script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedCheck --stacktrace +# script: ./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.class=org.matrix.android.sdk.session.room.timeline.ChunkEntityTest matrix-sdk-android:connectedAndroidTest --info diff --git a/.github/workflows/triage-move-labelled.yml b/.github/workflows/triage-labelled.yml similarity index 60% rename from .github/workflows/triage-move-labelled.yml rename to .github/workflows/triage-labelled.yml index e2f5cc32e9..71b1cde40d 100644 --- a/.github/workflows/triage-move-labelled.yml +++ b/.github/workflows/triage-labelled.yml @@ -5,6 +5,31 @@ on: types: [labeled] jobs: + apply_Z-Labs_label: + name: Add Z-Labs label for features behind labs flags + runs-on: ubuntu-latest + if: > + contains(github.event.issue.labels.*.name, 'A-Maths') || + contains(github.event.issue.labels.*.name, 'A-Message-Pinning') || + contains(github.event.issue.labels.*.name, 'A-Threads') || + contains(github.event.issue.labels.*.name, 'A-Polls') || + contains(github.event.issue.labels.*.name, 'A-Location-Sharing') || + contains(github.event.issue.labels.*.name, 'A-Message-Bubbles') || + contains(github.event.issue.labels.*.name, 'Z-IA') || + contains(github.event.issue.labels.*.name, 'A-Themes-Custom') || + contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') || + contains(github.event.issue.labels.*.name, 'A-Tags') + steps: + - uses: actions/github-script@v5 + with: + script: | + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['Z-Labs'] + }) + move_needs_info_issues: name: X-Needs-Info issues to Need info column on triage board runs-on: ubuntu-latest @@ -51,32 +76,57 @@ jobs: PROJECT_ID: "PN_kwDOAM0swc0sUA" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} - # delight_issues_to_board: - # name: Spaces issues to new Delight project board - # runs-on: ubuntu-latest - # # Skip in forks - # if: > - # github.repository == 'vector-im/element-android' && - # contains(github.event.issue.labels.*.name, 'A-Spaces') || - # contains(github.event.issue.labels.*.name, 'A-Space-Settings') || - # contains(github.event.issue.labels.*.name, 'A-Subspaces') - # steps: - # - uses: octokit/graphql-action@v2.x - # with: - # headers: '{"GraphQL-Features": "projects_next_graphql"}' - # query: | - # mutation add_to_project($projectid:ID!,$contentid:ID!) { - # addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - # projectNextItem { - # id - # } - # } - # } - # projectid: ${{ env.PROJECT_ID }} - # contentid: ${{ github.event.issue.node_id }} - # env: - # PROJECT_ID: "PN_kwDOAM0swc1HvQ" - # GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + add_product_issues: + name: X-Needs-Product to Design project board + runs-on: ubuntu-latest + if: > + contains(github.event.issue.labels.*.name, 'X-Needs-Product') + steps: + - uses: octokit/graphql-action@v2.x + id: add_to_project + with: + headers: '{"GraphQL-Features": "projects_next_graphql"}' + query: | + mutation add_to_project($projectid:ID!,$contentid:ID!) { + addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { + projectNextItem { + id + } + } + } + projectid: ${{ env.PROJECT_ID }} + contentid: ${{ github.event.issue.node_id }} + env: + PROJECT_ID: "PN_kwDOAM0swc4AAg6N" + GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + + delight_issues_to_board: + name: Spaces issues to Delight project board + runs-on: ubuntu-latest + # Skip in forks + if: > + github.repository == 'vector-im/element-android' && + (contains(github.event.issue.labels.*.name, 'A-Spaces') || + contains(github.event.issue.labels.*.name, 'A-Space-Settings') || + contains(github.event.issue.labels.*.name, 'A-Subspaces') || + contains(github.event.issue.labels.*.name, 'Z-IA')) + steps: + - uses: octokit/graphql-action@v2.x + with: + headers: '{"GraphQL-Features": "projects_next_graphql"}' + query: | + mutation add_to_project($projectid:ID!,$contentid:ID!) { + addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { + projectNextItem { + id + } + } + } + projectid: ${{ env.PROJECT_ID }} + contentid: ${{ github.event.issue.node_id }} + env: + PROJECT_ID: "PN_kwDOAM0swc1HvQ" + GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} move_voice-message_issues: name: A-Voice Messages to voice message board diff --git a/.github/workflows/triage-move-review-requests.yml b/.github/workflows/triage-move-review-requests.yml new file mode 100644 index 0000000000..75738a53a9 --- /dev/null +++ b/.github/workflows/triage-move-review-requests.yml @@ -0,0 +1,139 @@ +name: Move pull requests asking for review to the relevant project +on: + pull_request_target: + types: [review_requested] + +jobs: + add_design_pr_to_project: + name: Move PRs asking for design review to the design board + runs-on: ubuntu-latest + steps: + - uses: octokit/graphql-action@v2.x + id: find_team_members + with: + headers: '{"GraphQL-Features": "projects_next_graphql"}' + query: | + query find_team_members($team: String!) { + organization(login: "vector-im") { + team(slug: $team) { + members { + nodes { + login + } + } + } + } + } + team: ${{ env.TEAM }} + env: + TEAM: "design" + GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + - id: any_matching_reviewers + run: | + # Fetch requested reviewers, and people who are on the team + echo '${{ tojson(fromjson(steps.find_team_members.outputs.data).organization.team.members.nodes[*].login) }}' | tee /tmp/team_members.json + echo '${{ tojson(github.event.pull_request.requested_reviewers[*].login) }}' | tee /tmp/reviewers.json + jq --raw-output .[] < /tmp/team_members.json | sort | tee /tmp/team_members.txt + jq --raw-output .[] < /tmp/reviewers.json | sort | tee /tmp/reviewers.txt + + # Fetch requested team reviewers, and the name of the team + echo '${{ tojson(github.event.pull_request.requested_teams[*].slug) }}' | tee /tmp/team_reviewers.json + jq --raw-output .[] < /tmp/team_reviewers.json | sort | tee /tmp/team_reviewers.txt + echo '${{ env.TEAM }}' | tee /tmp/team.txt + + # If either a reviewer matches a team member, or a team matches our team, say "true" + if [ $(join /tmp/team_members.txt /tmp/reviewers.txt | wc -l) != 0 ]; then + echo "::set-output name=match::true" + elif [ $(join /tmp/team.txt /tmp/team_reviewers.txt | wc -l) != 0 ]; then + echo "::set-output name=match::true" + else + echo "::set-output name=match::false" + fi + env: + TEAM: "design" + - uses: octokit/graphql-action@v2.x + id: add_to_project + if: steps.any_matching_reviewers.outputs.match == 'true' + with: + headers: '{"GraphQL-Features": "projects_next_graphql"}' + query: | + mutation add_to_project($projectid:ID!, $contentid:ID!) { + addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { + projectNextItem { + id + } + } + } + projectid: ${{ env.PROJECT_ID }} + contentid: ${{ github.event.pull_request.node_id }} + env: + PROJECT_ID: "PN_kwDOAM0swc0sUA" + TEAM: "design" + GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + + add_product_pr_to_project: + name: Move PRs asking for product review to the product board + runs-on: ubuntu-latest + steps: + - uses: octokit/graphql-action@v2.x + id: find_team_members + with: + headers: '{"GraphQL-Features": "projects_next_graphql"}' + query: | + query find_team_members($team: String!) { + organization(login: "vector-im") { + team(slug: $team) { + members { + nodes { + login + } + } + } + } + } + team: ${{ env.TEAM }} + env: + TEAM: "product" + GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + - id: any_matching_reviewers + run: | + # Fetch requested reviewers, and people who are on the team + echo '${{ tojson(fromjson(steps.find_team_members.outputs.data).organization.team.members.nodes[*].login) }}' | tee /tmp/team_members.json + echo '${{ tojson(github.event.pull_request.requested_reviewers[*].login) }}' | tee /tmp/reviewers.json + jq --raw-output .[] < /tmp/team_members.json | sort | tee /tmp/team_members.txt + jq --raw-output .[] < /tmp/reviewers.json | sort | tee /tmp/reviewers.txt + + # Fetch requested team reviewers, and the name of the team + echo '${{ tojson(github.event.pull_request.requested_teams[*].slug) }}' | tee /tmp/team_reviewers.json + jq --raw-output .[] < /tmp/team_reviewers.json | sort | tee /tmp/team_reviewers.txt + echo '${{ env.TEAM }}' | tee /tmp/team.txt + + # If either a reviewer matches a team member, or a team matches our team, say "true" + if [ $(join /tmp/team_members.txt /tmp/reviewers.txt | wc -l) != 0 ]; then + echo "::set-output name=match::true" + elif [ $(join /tmp/team.txt /tmp/team_reviewers.txt | wc -l) != 0 ]; then + echo "::set-output name=match::true" + else + echo "::set-output name=match::false" + fi + env: + TEAM: "product" + - uses: octokit/graphql-action@v2.x + id: add_to_project + if: steps.any_matching_reviewers.outputs.match == 'true' + with: + headers: '{"GraphQL-Features": "projects_next_graphql"}' + query: | + mutation add_to_project($projectid:ID!, $contentid:ID!) { + addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { + projectNextItem { + id + } + } + } + projectid: ${{ env.PROJECT_ID }} + contentid: ${{ github.event.pull_request.node_id }} + env: + PROJECT_ID: "PN_kwDOAM0swc4AAg6N" + TEAM: "product" + GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} diff --git a/.github/workflows/triage-priority-bugs.yml b/.github/workflows/triage-priority-bugs.yml index daea78de19..70c337e748 100644 --- a/.github/workflows/triage-priority-bugs.yml +++ b/.github/workflows/triage-priority-bugs.yml @@ -38,7 +38,8 @@ jobs: # Skip in forks if: > github.repository == 'vector-im/element-android' && - (contains(github.event.issue.labels.*.name, 'A-E2EE') || + (contains(github.event.issue.labels.*.name, 'Z-UISI') || + (contains(github.event.issue.labels.*.name, 'A-E2EE') || contains(github.event.issue.labels.*.name, 'A-E2EE-Cross-Signing') || contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') || contains(github.event.issue.labels.*.name, 'A-E2EE-Key-Backup') || @@ -50,7 +51,7 @@ jobs: contains(github.event.issue.labels.*.name, 'S-Major') && contains(github.event.issue.labels.*.name, 'O-Frequent') || contains(github.event.issue.labels.*.name, 'A11y') && - contains(github.event.issue.labels.*.name, 'O-Frequent')) + contains(github.event.issue.labels.*.name, 'O-Frequent'))) steps: - uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488 with: diff --git a/.github/workflows/triage-move-unlabelled.yml b/.github/workflows/triage-unlabelled.yml similarity index 58% rename from .github/workflows/triage-move-unlabelled.yml rename to .github/workflows/triage-unlabelled.yml index eb90cdb503..06df286d09 100644 --- a/.github/workflows/triage-move-unlabelled.yml +++ b/.github/workflows/triage-unlabelled.yml @@ -34,3 +34,29 @@ jobs: project: Issue triage column: Triaged repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }} + + remove_Z-Labs_label: + name: Remove Z-Labs label when features behind labs flags are removed + runs-on: ubuntu-latest + if: > + !(contains(github.event.issue.labels.*.name, 'A-Maths') || + contains(github.event.issue.labels.*.name, 'A-Message-Pinning') || + contains(github.event.issue.labels.*.name, 'A-Threads') || + contains(github.event.issue.labels.*.name, 'A-Polls') || + contains(github.event.issue.labels.*.name, 'A-Location-Sharing') || + contains(github.event.issue.labels.*.name, 'A-Message-Bubbles') || + contains(github.event.issue.labels.*.name, 'Z-IA') || + contains(github.event.issue.labels.*.name, 'A-Themes-Custom') || + contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') || + contains(github.event.issue.labels.*.name, 'A-Tags')) && + contains(github.event.issue.labels.*.name, 'Z-Labs') + steps: + - uses: actions/github-script@v5 + with: + script: | + github.rest.issues.removeLabel({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + name: ['Z-Labs'] + }) diff --git a/CHANGES.md b/CHANGES.md index cf885d5cd5..37779cca96 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,53 @@ +Changes in Element 1.3.16 (2022-01-25) +====================================== + +Features ✨ +---------- + - Static location sharing and rendering ([#2210](https://github.com/vector-im/element-android/issues/2210)) + - Enables the FTUE splash carousel ([#4584](https://github.com/vector-im/element-android/issues/4584)) + - Allow editing polls ([#5036](https://github.com/vector-im/element-android/issues/5036)) + +Bugfixes 🐛 +---------- + - Fixing missing notifications in FDroid variants using `optimised for battery` background sync mode ([#5003](https://github.com/vector-im/element-android/issues/5003)) + - Fix for stuck local event messages at the bottom of the screen ([#516](https://github.com/vector-im/element-android/issues/516)) + - Notification does not take me to the room when another space was last viewed ([#3839](https://github.com/vector-im/element-android/issues/3839)) + - Explore Rooms overflow menu - content update include "Create room" ([#3932](https://github.com/vector-im/element-android/issues/3932)) + - Fix sync timeout after returning from background ([#4669](https://github.com/vector-im/element-android/issues/4669)) + - Fix a wrong network error issue in the Legals screen ([#4935](https://github.com/vector-im/element-android/issues/4935)) + - Prevent Alerts to be displayed in the automatically displayed analytics opt-in screen ([#4948](https://github.com/vector-im/element-android/issues/4948)) + - EmojiPopupDismissListener not being triggered after dismissing the EmojiPopup ([#4991](https://github.com/vector-im/element-android/issues/4991)) + - Fix an error in string resource ([#4997](https://github.com/vector-im/element-android/issues/4997)) + - Big messages taking inappropriately long to evaluate .m.rule.roomnotif push rules ([#5008](https://github.com/vector-im/element-android/issues/5008)) + - Improve auto rageshake lab feature ([#5021](https://github.com/vector-im/element-android/issues/5021)) + +In development 🚧 +---------------- + - Updates the onboarding carousel images, copy and improves the handling of different device sizes ([#4880](https://github.com/vector-im/element-android/issues/4880)) + - Disabling onboarding automatic carousel transitions on user interaction ([#4914](https://github.com/vector-im/element-android/issues/4914)) + - Locking phones to portrait during the FTUE onboarding ([#4918](https://github.com/vector-im/element-android/issues/4918)) + - Adds a messaging use case screen to the FTUE onboarding ([#4927](https://github.com/vector-im/element-android/issues/4927)) + - Updating the FTUE use case icons ([#5025](https://github.com/vector-im/element-android/issues/5025)) + - Support undisclosed polls ([#5037](https://github.com/vector-im/element-android/issues/5037)) + +Other changes +------------- + - Enabling native support for window resizing ([#4811](https://github.com/vector-im/element-android/issues/4811)) + - Analytics: send more Events ([#4734](https://github.com/vector-im/element-android/issues/4734)) + - Fix integration tests and add a comment with results (still not perfect due to github actions resource limitations) ([#4842](https://github.com/vector-im/element-android/issues/4842)) + - "/kick" command is replaced with "/remove". Also replaced all occurrences in string resources ([#4865](https://github.com/vector-im/element-android/issues/4865)) + - Toolbar management rework. Toolbar title's and subtitle's text appearance now controlled by theme without local overrides. Helper class introduced to + help with toolbar configuration. Toolbar title, subtitle and navigation button widgets are removed where it is possible and replaced with built-in + toolbar widgets. ([#4884](https://github.com/vector-im/element-android/issues/4884)) + - Add signing config for the release buildType. No secret added ([#4926](https://github.com/vector-im/element-android/issues/4926)) + - Remove unused module matrix-sdk-android-rx and do some cleanup ([#4942](https://github.com/vector-im/element-android/issues/4942)) + - Sync issue automation with element-web ([#4949](https://github.com/vector-im/element-android/issues/4949)) + - Improves local echo blinking when non room events received ([#4960](https://github.com/vector-im/element-android/issues/4960)) + - Including onboarding server options in the all screen sanity test suite ([#4975](https://github.com/vector-im/element-android/issues/4975)) + - Exclude dependabot upgrade for @github-script@v3 ([#4988](https://github.com/vector-im/element-android/issues/4988)) + - Small iteration on command parser and unit test it. ([#4998](https://github.com/vector-im/element-android/issues/4998)) + + Changes in Element v1.3.15 (2022-01-18) ======================================= diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 22d12ac663..2512052953 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -61,8 +61,9 @@ Supported filename extensions are: - ``.feature``: Signifying a new feature in Element Android or in the Matrix SDK. - ``.bugfix``: Signifying a bug fix. +- ``.wip``: Signifying a work in progress change, typically a component of a larger feature which will be enabled once all tasks are complete. - ``.doc``: Signifying a documentation improvement. -- ``.removal``: Signifying a deprecation or removal of public API. Can be used to notifying about API change in the Matrix SDK +- ``.sdk``: Signifying a change to the Matrix SDK, this could be an addition, deprecation or removal of a public API. - ``.misc``: Any other changes. See https://github.com/twisted/towncrier#news-fragments if you need more details. diff --git a/README.md b/README.md index 0345c50146..dedc9da2dd 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ # Element Android -Element Android is an Android Matrix Client provided by [Element](https://element.io/). +Element Android is an Android Matrix Client provided by [Element](https://element.io/). The app can be run on every Android devices with Android OS Lollipop and more (API 21). It is a total rewrite of [Riot-Android](https://github.com/vector-im/riot-android) with a new user experience. @@ -51,4 +51,4 @@ Come chat with the community in the dedicated Matrix [room](https://matrix.to/#/ Issues are triaged by community members and the Android App Team, following the [triage process](https://github.com/vector-im/element-meta/wiki/Triage-process). -We use [issue labels](https://github.com/vector-im/element-meta/wiki/Issue-labelling) to sort all incoming issues. \ No newline at end of file +We use [issue labels](https://github.com/vector-im/element-meta/wiki/Issue-labelling) to sort all incoming issues. diff --git a/attachment-viewer/build.gradle b/attachment-viewer/build.gradle index 02fbfc794c..048710f62c 100644 --- a/attachment-viewer/build.gradle +++ b/attachment-viewer/build.gradle @@ -47,12 +47,10 @@ android { dependencies { implementation project(":library:ui-styles") + implementation project(":library:core-utils") implementation 'com.github.chrisbanes:PhotoView:2.3.0' - implementation libs.rx.rxKotlin - implementation libs.rx.rxAndroid - implementation libs.androidx.core implementation libs.androidx.appCompat implementation libs.androidx.recyclerview diff --git a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt index 0b72ef36f0..12213a8786 100644 --- a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt +++ b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt @@ -20,12 +20,9 @@ import android.util.Log import android.view.View import androidx.core.view.isVisible import im.vector.lib.attachmentviewer.databinding.ItemVideoAttachmentBinding -import io.reactivex.Observable -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.Disposable +import im.vector.lib.core.utils.timer.CountUpTimer import java.io.File import java.lang.ref.WeakReference -import java.util.concurrent.TimeUnit // TODO, it would be probably better to use a unique media player // for better customization and control @@ -35,7 +32,7 @@ class VideoViewHolder constructor(itemView: View) : private var isSelected = false private var mVideoPath: String? = null - private var progressDisposable: Disposable? = null + private var countUpTimer: CountUpTimer? = null private var progress: Int = 0 private var wasPaused = false @@ -47,8 +44,7 @@ class VideoViewHolder constructor(itemView: View) : override fun onRecycled() { super.onRecycled() - progressDisposable?.dispose() - progressDisposable = null + stopTimer() mVideoPath = null } @@ -72,8 +68,7 @@ class VideoViewHolder constructor(itemView: View) : override fun entersBackground() { if (views.videoView.isPlaying) { progress = views.videoView.currentPosition - progressDisposable?.dispose() - progressDisposable = null + stopTimer() views.videoView.stopPlayback() views.videoView.pause() } @@ -91,8 +86,7 @@ class VideoViewHolder constructor(itemView: View) : } else { progress = 0 } - progressDisposable?.dispose() - progressDisposable = null + stopTimer() } else { if (mVideoPath != null) { startPlaying() @@ -107,17 +101,19 @@ class VideoViewHolder constructor(itemView: View) : views.videoView.isVisible = true views.videoView.setOnPreparedListener { - progressDisposable?.dispose() - progressDisposable = Observable.interval(100, TimeUnit.MILLISECONDS) - .timeInterval() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { + stopTimer() + countUpTimer = CountUpTimer(100).also { + it.tickListener = object : CountUpTimer.TickListener { + override fun onTick(milliseconds: Long) { val duration = views.videoView.duration val progress = views.videoView.currentPosition val isPlaying = views.videoView.isPlaying // Log.v("FOO", "isPlaying $isPlaying $progress/$duration") eventListener?.get()?.onEvent(AttachmentEvents.VideoEvent(isPlaying, progress, duration)) } + } + it.resume() + } } try { views.videoView.setVideoPath(mVideoPath) @@ -134,6 +130,11 @@ class VideoViewHolder constructor(itemView: View) : } } + private fun stopTimer() { + countUpTimer?.stop() + countUpTimer = null + } + override fun handleCommand(commands: AttachmentCommands) { if (!isSelected) return when (commands) { diff --git a/build.gradle b/build.gradle index b7299b01f7..5fdeba24de 100644 --- a/build.gradle +++ b/build.gradle @@ -153,13 +153,3 @@ project(":diff-match-patch") { // } // } //} -// -//project(":matrix-sdk-android-rx") { -// sonarqube { -// properties { -// property "sonar.sources", project(":matrix-sdk-android-rx").android.sourceSets.main.java.srcDirs -// // exclude source code from analyses separated by a colon (:) -// // property "sonar.exclusions", "**/*.*" -// } -// } -//} diff --git a/changelog.d/4895.removal b/changelog.d/4895.removal new file mode 100644 index 0000000000..8b3e3adba4 --- /dev/null +++ b/changelog.d/4895.removal @@ -0,0 +1 @@ +`StateService.sendStateEvent()` now takes a non-nullable String for the parameter `stateKey`. If null was used, just now use an empty string. \ No newline at end of file diff --git a/changelog.d/4995.removal b/changelog.d/4995.removal new file mode 100644 index 0000000000..9eacff87cd --- /dev/null +++ b/changelog.d/4995.removal @@ -0,0 +1 @@ +429 are not automatically retried anymore in case of too long retry delay \ No newline at end of file diff --git a/dependencies.gradle b/dependencies.gradle index 3fb47ba711..77d072e7c7 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -29,6 +29,7 @@ def vanniktechEmoji = "0.8.0" def mockk = "1.12.1" def espresso = "3.4.0" def androidxTest = "1.4.0" +def androidxOrchestrator = "1.4.1" ext.libs = [ @@ -41,7 +42,6 @@ ext.libs = [ jetbrains : [ 'coroutinesCore' : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutines", 'coroutinesAndroid' : "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinCoroutines", - 'coroutinesRx2' : "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$kotlinCoroutines", 'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines" ], androidx : [ @@ -63,7 +63,7 @@ ext.libs = [ 'pagingRuntimeKtx' : "androidx.paging:paging-runtime-ktx:2.1.2", 'coreTesting' : "androidx.arch.core:core-testing:2.1.0", 'testCore' : "androidx.test:core:$androidxTest", - 'orchestrator' : "androidx.test:orchestrator:$androidxTest", + 'orchestrator' : "androidx.test:orchestrator:$androidxOrchestrator", 'testRunner' : "androidx.test:runner:$androidxTest", 'testRules' : "androidx.test:rules:$androidxTest", 'espressoCore' : "androidx.test.espresso:espresso-core:$espresso", @@ -87,8 +87,7 @@ ext.libs = [ 'retrofitMoshi' : "com.squareup.retrofit2:converter-moshi:$retrofit" ], rx : [ - 'rxKotlin' : "io.reactivex.rxjava2:rxkotlin:2.4.0", - 'rxAndroid' : "io.reactivex.rxjava2:rxandroid:2.1.1" + 'rxKotlin' : "io.reactivex.rxjava2:rxkotlin:2.4.0" ], arrow : [ 'core' : "io.arrow-kt:arrow-core:$arrow", diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index fd36f5110c..7de8100469 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -83,6 +83,7 @@ ext.groups = [ 'com.jakewharton.android.repackaged', 'com.jakewharton.timber', 'com.linkedin.dexmaker', + 'com.mapbox.mapboxsdk', 'com.nulab-inc', 'com.otaliastudios.opengl', 'com.parse.bolts', @@ -159,6 +160,7 @@ ext.groups = [ 'org.junit.jupiter', 'org.junit.platform', 'org.jvnet.staxex', + 'org.maplibre.gl', 'org.matrix.android', 'org.mockito', 'org.mongodb', diff --git a/fastlane/README.md b/fastlane/README.md index dc33f422d6..7fea7afdd5 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -1,49 +1,64 @@ fastlane documentation -================ +---- + # Installation Make sure you have the latest version of the Xcode command line tools installed: -``` +```sh xcode-select --install ``` -Install _fastlane_ using -``` -[sudo] gem install fastlane -NV -``` -or alternatively using `brew install fastlane` +For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) # Available Actions + ## Android + ### android test + +```sh +[bundle exec] fastlane android test ``` -fastlane android test -``` + Runs all the tests + ### android beta + +```sh +[bundle exec] fastlane android beta ``` -fastlane android beta -``` + Submit a new Beta Build to Crashlytics Beta + ### android deploy + +```sh +[bundle exec] fastlane android deploy ``` -fastlane android deploy -``` + Deploy a new version to the Google Play + ### android deployMeta + +```sh +[bundle exec] fastlane android deployMeta ``` -fastlane android deployMeta -``` + Deploy Google Play metadata + ### android getVersionCode + +```sh +[bundle exec] fastlane android getVersionCode ``` -fastlane android getVersionCode -``` + Get version code ---- This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. -More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). -The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). + +More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). + +The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). diff --git a/fastlane/metadata/android/de-DE/changelogs/40103100.txt b/fastlane/metadata/android/de-DE/changelogs/40103100.txt new file mode 100644 index 0000000000..8daa7b51a5 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40103100.txt @@ -0,0 +1,2 @@ +Änderungen: Die Websitevorschau hat ein neues Design erhalten. Außerdem gibt es in den experimentellen Einstellungen Abstimmungen. +Alle Änderungen: https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/de-DE/changelogs/40103110.txt b/fastlane/metadata/android/de-DE/changelogs/40103110.txt new file mode 100644 index 0000000000..a3d6aad6ca --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40103110.txt @@ -0,0 +1,2 @@ +Hauptänderungen: Bugfixes! +Alle Änderungen: https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/de-DE/changelogs/40103120.txt b/fastlane/metadata/android/de-DE/changelogs/40103120.txt new file mode 100644 index 0000000000..6930764750 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40103120.txt @@ -0,0 +1,2 @@ +Hauptänderungen: Bugfixes! +Alle Änderungen: https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/en-US/changelogs/40103160.txt b/fastlane/metadata/android/en-US/changelogs/40103160.txt new file mode 100644 index 0000000000..900abee127 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40103160.txt @@ -0,0 +1,2 @@ +Main changes in this version: send your location to any room. Edit poll. +Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.16 \ No newline at end of file diff --git a/fastlane/metadata/android/et/title.txt b/fastlane/metadata/android/et/title.txt index b0bf39ba23..907f907f99 100644 --- a/fastlane/metadata/android/et/title.txt +++ b/fastlane/metadata/android/et/title.txt @@ -1 +1 @@ -Element - turvaline sõnumiklient +Element diff --git a/fastlane/metadata/android/fr-FR/changelogs/40103100.txt b/fastlane/metadata/android/fr-FR/changelogs/40103100.txt new file mode 100644 index 0000000000..b6484603d4 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40103100.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : prise en charge des sondages (dans les labs). Nouvel affichage des prévisualisations d’URL +Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/fr-FR/changelogs/40103110.txt b/fastlane/metadata/android/fr-FR/changelogs/40103110.txt new file mode 100644 index 0000000000..aef05c238d --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40103110.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : corrections de bugs ! +Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/fr-FR/changelogs/40103120.txt b/fastlane/metadata/android/fr-FR/changelogs/40103120.txt new file mode 100644 index 0000000000..18cba17990 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40103120.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : corrections de bugs ! +Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/hu-HU/title.txt b/fastlane/metadata/android/hu-HU/title.txt index c463dea393..907f907f99 100644 --- a/fastlane/metadata/android/hu-HU/title.txt +++ b/fastlane/metadata/android/hu-HU/title.txt @@ -1 +1 @@ -Element - Biztonságos üzenetküldő +Element diff --git a/fastlane/metadata/android/nl/changelogs/40103070.txt b/fastlane/metadata/android/nl-NL/changelogs/40103070.txt similarity index 100% rename from fastlane/metadata/android/nl/changelogs/40103070.txt rename to fastlane/metadata/android/nl-NL/changelogs/40103070.txt diff --git a/fastlane/metadata/android/nl/changelogs/40103080.txt b/fastlane/metadata/android/nl-NL/changelogs/40103080.txt similarity index 100% rename from fastlane/metadata/android/nl/changelogs/40103080.txt rename to fastlane/metadata/android/nl-NL/changelogs/40103080.txt diff --git a/fastlane/metadata/android/nl/changelogs/40103090.txt b/fastlane/metadata/android/nl-NL/changelogs/40103090.txt similarity index 100% rename from fastlane/metadata/android/nl/changelogs/40103090.txt rename to fastlane/metadata/android/nl-NL/changelogs/40103090.txt diff --git a/fastlane/metadata/android/nl/changelogs/40103100.txt b/fastlane/metadata/android/nl-NL/changelogs/40103100.txt similarity index 100% rename from fastlane/metadata/android/nl/changelogs/40103100.txt rename to fastlane/metadata/android/nl-NL/changelogs/40103100.txt diff --git a/fastlane/metadata/android/nl/changelogs/40103110.txt b/fastlane/metadata/android/nl-NL/changelogs/40103110.txt similarity index 100% rename from fastlane/metadata/android/nl/changelogs/40103110.txt rename to fastlane/metadata/android/nl-NL/changelogs/40103110.txt diff --git a/fastlane/metadata/android/nl/changelogs/40103120.txt b/fastlane/metadata/android/nl-NL/changelogs/40103120.txt similarity index 100% rename from fastlane/metadata/android/nl/changelogs/40103120.txt rename to fastlane/metadata/android/nl-NL/changelogs/40103120.txt diff --git a/fastlane/metadata/android/nl/short_description.txt b/fastlane/metadata/android/nl-NL/short_description.txt similarity index 100% rename from fastlane/metadata/android/nl/short_description.txt rename to fastlane/metadata/android/nl-NL/short_description.txt diff --git a/fastlane/metadata/android/nl/title.txt b/fastlane/metadata/android/nl-NL/title.txt similarity index 100% rename from fastlane/metadata/android/nl/title.txt rename to fastlane/metadata/android/nl-NL/title.txt diff --git a/fastlane/metadata/android/pl/changelogs/40103070.txt b/fastlane/metadata/android/pl-PL/changelogs/40103070.txt similarity index 100% rename from fastlane/metadata/android/pl/changelogs/40103070.txt rename to fastlane/metadata/android/pl-PL/changelogs/40103070.txt diff --git a/fastlane/metadata/android/pl/full_description.txt b/fastlane/metadata/android/pl-PL/full_description.txt similarity index 100% rename from fastlane/metadata/android/pl/full_description.txt rename to fastlane/metadata/android/pl-PL/full_description.txt diff --git a/fastlane/metadata/android/pl/short_description.txt b/fastlane/metadata/android/pl-PL/short_description.txt similarity index 100% rename from fastlane/metadata/android/pl/short_description.txt rename to fastlane/metadata/android/pl-PL/short_description.txt diff --git a/fastlane/metadata/android/pl-PL/title.txt b/fastlane/metadata/android/pl-PL/title.txt new file mode 100644 index 0000000000..907f907f99 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/title.txt @@ -0,0 +1 @@ +Element diff --git a/fastlane/metadata/android/pl/title.txt b/fastlane/metadata/android/pl/title.txt deleted file mode 100644 index 3f4f1ba418..0000000000 --- a/fastlane/metadata/android/pl/title.txt +++ /dev/null @@ -1 +0,0 @@ -Element - Bezpieczny Komunikator diff --git a/fastlane/metadata/android/ru-RU/title.txt b/fastlane/metadata/android/ru-RU/title.txt index b7b25082a4..907f907f99 100644 --- a/fastlane/metadata/android/ru-RU/title.txt +++ b/fastlane/metadata/android/ru-RU/title.txt @@ -1 +1 @@ -Element - Безопасный мессенджер +Element diff --git a/fastlane/metadata/android/sq/changelogs/40103100.txt b/fastlane/metadata/android/sq/changelogs/40103100.txt new file mode 100644 index 0000000000..c6916fa0ab --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40103100.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Shtim mbulimi për anketime (në zhvillim). Skemë e re grafike për paraprje URL-sh. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/sq/changelogs/40103110.txt b/fastlane/metadata/android/sq/changelogs/40103110.txt new file mode 100644 index 0000000000..f66779b5ef --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40103110.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje të metash! +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/sq/changelogs/40103120.txt b/fastlane/metadata/android/sq/changelogs/40103120.txt new file mode 100644 index 0000000000..279c523a82 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40103120.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje të metash! +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/sq/title.txt b/fastlane/metadata/android/sq/title.txt index 097f9c48ea..907f907f99 100644 --- a/fastlane/metadata/android/sq/title.txt +++ b/fastlane/metadata/android/sq/title.txt @@ -1 +1 @@ -Element - Shkëmbyes i Sigurt Mesazhesh +Element diff --git a/gradle.properties b/gradle.properties index 23538c5285..5c99297107 100644 --- a/gradle.properties +++ b/gradle.properties @@ -25,4 +25,10 @@ vector.httpLogLevel=BASIC # Note: to debug, you can put and uncomment the following lines in the file ~/.gradle/gradle.properties to override the value above #vector.debugPrivateData=true -#vector.httpLogLevel=BODY \ No newline at end of file +#vector.httpLogLevel=BODY + +# Dummy values for signing secrets +signing.element.storePath=pathTo.keystore +signing.element.storePassword=Secret +signing.element.keyId=Secret +signing.element.keyPassword=Secret diff --git a/integration_tests_script.sh b/integration_tests_script.sh new file mode 100755 index 0000000000..fe72ae6f5c --- /dev/null +++ b/integration_tests_script.sh @@ -0,0 +1,3 @@ +#!/bin/bash +./gradlew -Pandroid.testInstrumentationRunnerArguments.class=org.matrix.android.sdk.session.room.timeline.ChunkEntityTest matrix-sdk-android:connectedAndroidTest +./gradlew -Pandroid.testInstrumentationRunnerArguments.class=org.matrix.android.sdk.session.room.timeline.TimelineForwardPaginationTest matrix-sdk-android:connectedAndroidTest diff --git a/integration_tests_script_github.sh b/integration_tests_script_github.sh new file mode 100755 index 0000000000..bbf666e4f0 --- /dev/null +++ b/integration_tests_script_github.sh @@ -0,0 +1,3 @@ +#!/bin/bash +./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.class=org.matrix.android.sdk.session.room.timeline.ChunkEntityTest matrix-sdk-android:connectedAndroidTest +./gradlew $CI_GRADLE_ARG_PROPERTIES -Pandroid.testInstrumentationRunnerArguments.class=org.matrix.android.sdk.session.room.timeline.TimelineForwardPaginationTest matrix-sdk-android:connectedAndroidTest diff --git a/library/core-utils/.gitignore b/library/core-utils/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/library/core-utils/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/library/core-utils/build.gradle b/library/core-utils/build.gradle new file mode 100644 index 0000000000..ad3a948808 --- /dev/null +++ b/library/core-utils/build.gradle @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2021 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. + */ + +plugins { + id 'com.android.library' + id 'kotlin-android' +} + +android { + compileSdk versions.compileSdk + defaultConfig { + minSdk versions.minSdk + targetSdk versions.targetSdk + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility versions.sourceCompat + targetCompatibility versions.targetCompat + } + + kotlinOptions { + jvmTarget = "11" + freeCompilerArgs += [ + "-Xopt-in=kotlin.RequiresOptIn" + ] + } +} + +dependencies { + implementation libs.androidx.appCompat + implementation libs.jetbrains.coroutinesAndroid +} \ No newline at end of file diff --git a/library/core-utils/src/main/AndroidManifest.xml b/library/core-utils/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..20a9414519 --- /dev/null +++ b/library/core-utils/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/vector/src/main/java/im/vector/app/core/utils/compat/MutableCollectionCompat.kt b/library/core-utils/src/main/java/im/vector/lib/core/utils/compat/MutableCollectionCompat.kt similarity index 95% rename from vector/src/main/java/im/vector/app/core/utils/compat/MutableCollectionCompat.kt rename to library/core-utils/src/main/java/im/vector/lib/core/utils/compat/MutableCollectionCompat.kt index e131b5f328..332ed27ca3 100644 --- a/vector/src/main/java/im/vector/app/core/utils/compat/MutableCollectionCompat.kt +++ b/library/core-utils/src/main/java/im/vector/lib/core/utils/compat/MutableCollectionCompat.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.app.core.utils.compat +package im.vector.lib.core.utils.compat import android.os.Build diff --git a/vector/src/main/java/im/vector/app/core/epoxy/charsequence/EpoxyCharSequence.kt b/library/core-utils/src/main/java/im/vector/lib/core/utils/epoxy/charsequence/EpoxyCharSequence.kt similarity index 94% rename from vector/src/main/java/im/vector/app/core/epoxy/charsequence/EpoxyCharSequence.kt rename to library/core-utils/src/main/java/im/vector/lib/core/utils/epoxy/charsequence/EpoxyCharSequence.kt index dcd19b598d..77e2d58001 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/charsequence/EpoxyCharSequence.kt +++ b/library/core-utils/src/main/java/im/vector/lib/core/utils/epoxy/charsequence/EpoxyCharSequence.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.app.core.epoxy.charsequence +package im.vector.lib.core.utils.epoxy.charsequence /** * Wrapper for a CharSequence, which support mutation of the CharSequence, which can happen during rendering diff --git a/vector/src/main/java/im/vector/app/core/epoxy/charsequence/Extensions.kt b/library/core-utils/src/main/java/im/vector/lib/core/utils/epoxy/charsequence/Extensions.kt similarity index 93% rename from vector/src/main/java/im/vector/app/core/epoxy/charsequence/Extensions.kt rename to library/core-utils/src/main/java/im/vector/lib/core/utils/epoxy/charsequence/Extensions.kt index b6d918c575..ba0f0b9ad6 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/charsequence/Extensions.kt +++ b/library/core-utils/src/main/java/im/vector/lib/core/utils/epoxy/charsequence/Extensions.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.app.core.epoxy.charsequence +package im.vector.lib.core.utils.epoxy.charsequence /** * Extensions to wrap CharSequence to EpoxyCharSequence diff --git a/vector/src/main/java/im/vector/app/core/flow/TimingOperators.kt b/library/core-utils/src/main/java/im/vector/lib/core/utils/flow/TimingOperators.kt similarity index 97% rename from vector/src/main/java/im/vector/app/core/flow/TimingOperators.kt rename to library/core-utils/src/main/java/im/vector/lib/core/utils/flow/TimingOperators.kt index 621a80d96e..065c19c17a 100644 --- a/vector/src/main/java/im/vector/app/core/flow/TimingOperators.kt +++ b/library/core-utils/src/main/java/im/vector/lib/core/utils/flow/TimingOperators.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.app.core.flow +package im.vector.lib.core.utils.flow import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -85,10 +85,12 @@ fun Flow.throttleFirst(windowDuration: Long): Flow = flow { } } +@ExperimentalCoroutinesApi fun tickerFlow(scope: CoroutineScope, delayMillis: Long, initialDelayMillis: Long = delayMillis): Flow { return scope.fixedPeriodTicker(delayMillis, initialDelayMillis).consumeAsFlow() } +@ExperimentalCoroutinesApi private fun CoroutineScope.fixedPeriodTicker(delayMillis: Long, initialDelayMillis: Long = delayMillis): ReceiveChannel { require(delayMillis >= 0) { "Expected non-negative delay, but has $delayMillis ms" } require(initialDelayMillis >= 0) { "Expected non-negative initial delay, but has $initialDelayMillis ms" } diff --git a/vector/src/main/java/im/vector/app/core/utils/CountUpTimer.kt b/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt similarity index 93% rename from vector/src/main/java/im/vector/app/core/utils/CountUpTimer.kt rename to library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt index b58d0fb3f6..e9d311fe03 100644 --- a/vector/src/main/java/im/vector/app/core/utils/CountUpTimer.kt +++ b/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt @@ -14,9 +14,9 @@ * limitations under the License. */ -package im.vector.app.core.utils +package im.vector.lib.core.utils.timer -import im.vector.app.core.flow.tickerFlow +import im.vector.lib.core.utils.flow.tickerFlow import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel @@ -27,6 +27,7 @@ import kotlinx.coroutines.flow.onEach import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicLong +@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) class CountUpTimer(private val intervalInMs: Long = 1_000) { private val coroutineScope = CoroutineScope(Dispatchers.Main) diff --git a/library/jsonviewer/build.gradle b/library/jsonviewer/build.gradle index ee2be6fd25..15f46754b3 100644 --- a/library/jsonviewer/build.gradle +++ b/library/jsonviewer/build.gradle @@ -42,6 +42,8 @@ android { } dependencies { + implementation project(":library:core-utils") + implementation libs.androidx.appCompat implementation libs.androidx.core @@ -57,7 +59,7 @@ dependencies { implementation libs.jetbrains.coroutinesCore implementation libs.jetbrains.coroutinesAndroid - testImplementation 'org.json:json:20190722' + testImplementation 'org.json:json:20211205' testImplementation libs.tests.junit androidTestImplementation libs.androidx.junit androidTestImplementation libs.androidx.espressoCore diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerEpoxyController.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerEpoxyController.kt index 9c48a137da..96b5a9c997 100644 --- a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerEpoxyController.kt +++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerEpoxyController.kt @@ -21,6 +21,7 @@ import android.view.View import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Success +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.Span import me.gujun.android.span.span @@ -40,7 +41,7 @@ internal class JSonViewerEpoxyController(private val context: Context) : is Fail -> { valueItem { id("fail") - text(async.error.localizedMessage?.toSafeCharSequence()) + text(async.error.localizedMessage?.toEpoxyCharSequence()) } } is Success -> { @@ -94,7 +95,7 @@ internal class JSonViewerEpoxyController(private val context: Context) : +"{+${model.keys.size}}" textColor = host.styleProvider.baseColor } - }.toSafeCharSequence() + }.toEpoxyCharSequence() ) itemClickListener(View.OnClickListener { host.itemClicked(model) }) } @@ -133,7 +134,7 @@ internal class JSonViewerEpoxyController(private val context: Context) : +"[+${model.items.size}]" textColor = host.styleProvider.baseColor } - }.toSafeCharSequence() + }.toEpoxyCharSequence() ) itemClickListener(View.OnClickListener { host.itemClicked(model) }) } @@ -163,7 +164,7 @@ internal class JSonViewerEpoxyController(private val context: Context) : } } append(host.valueToSpan(model)) - }.toSafeCharSequence() + }.toEpoxyCharSequence() ) copyValue(model.stringRes) } @@ -233,7 +234,7 @@ internal class JSonViewerEpoxyController(private val context: Context) : span("{".takeIf { isObject } ?: "[") { textColor = host.styleProvider.baseColor } - }.toSafeCharSequence() + }.toEpoxyCharSequence() ) itemClickListener(View.OnClickListener { host.itemClicked(composed) }) } @@ -253,7 +254,7 @@ internal class JSonViewerEpoxyController(private val context: Context) : span { text = "}".takeIf { isObject } ?: "]" textColor = host.styleProvider.baseColor - }.toSafeCharSequence() + }.toEpoxyCharSequence() ) } } diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/Utils.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/Utils.kt index 6536a3401e..efb2bfd855 100644 --- a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/Utils.kt +++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/Utils.kt @@ -19,9 +19,6 @@ package org.billcarsonfr.jsonviewer import android.content.Context import android.util.TypedValue -/** - * TODO Mutualize - */ internal object Utils { fun dpToPx(dp: Int, context: Context): Int { return TypedValue.applyDimension( diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt index 9193a20ab2..227ac2a71d 100644 --- a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt +++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt @@ -28,12 +28,13 @@ import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyHolder import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelWithHolder +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence @EpoxyModelClass(layout = R2.layout.item_jv_base_value) internal abstract class ValueItem : EpoxyModelWithHolder() { @EpoxyAttribute - var text: SafeCharSequence? = null + var text: EpoxyCharSequence? = null @EpoxyAttribute var depth: Int = 0 diff --git a/library/ui-styles/src/main/java/MaterialProgressDialog.kt b/library/ui-styles/src/main/java/im/vector/lib/ui/styles/dialogs/MaterialProgressDialog.kt similarity index 100% rename from library/ui-styles/src/main/java/MaterialProgressDialog.kt rename to library/ui-styles/src/main/java/im/vector/lib/ui/styles/dialogs/MaterialProgressDialog.kt diff --git a/library/ui-styles/src/main/res/drawable/bg_carousel_page_dark.xml b/library/ui-styles/src/main/res/drawable/bg_carousel_page_dark.xml new file mode 100644 index 0000000000..2542ff2b1d --- /dev/null +++ b/library/ui-styles/src/main/res/drawable/bg_carousel_page_dark.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/library/ui-styles/src/main/res/values-h720dp/dimens.xml b/library/ui-styles/src/main/res/values-h720dp/dimens.xml new file mode 100644 index 0000000000..1a7791720d --- /dev/null +++ b/library/ui-styles/src/main/res/values-h720dp/dimens.xml @@ -0,0 +1,5 @@ + + + 0.05 + 0.40 + \ No newline at end of file diff --git a/library/ui-styles/src/main/res/values-sw600dp/tablet.xml b/library/ui-styles/src/main/res/values-sw600dp/tablet.xml index 39f467cf0d..86bab06371 100644 --- a/library/ui-styles/src/main/res/values-sw600dp/tablet.xml +++ b/library/ui-styles/src/main/res/values-sw600dp/tablet.xml @@ -2,5 +2,6 @@ 0.6 + true \ No newline at end of file diff --git a/library/ui-styles/src/main/res/values/dimens.xml b/library/ui-styles/src/main/res/values/dimens.xml index a2a6b34b0f..d184cd06df 100644 --- a/library/ui-styles/src/main/res/values/dimens.xml +++ b/library/ui-styles/src/main/res/values/dimens.xml @@ -51,4 +51,7 @@ 0.05 0.95 + + 0.01 + 0.35 \ No newline at end of file diff --git a/library/ui-styles/src/main/res/values/styles_toolbar.xml b/library/ui-styles/src/main/res/values/styles_toolbar.xml index 9f6ba102ed..505419c6fe 100644 --- a/library/ui-styles/src/main/res/values/styles_toolbar.xml +++ b/library/ui-styles/src/main/res/values/styles_toolbar.xml @@ -6,10 +6,12 @@ 0dp - @style/Widget.Vector.TextView.ActionBarTitle + @style/TextAppearance.Vector.Widget.ActionBarTitle - @style/Widget.Vector.TextView.ActionBarSubTitle + @style/TextAppearance.Vector.Widget.ActionBarSubTitle + + ?vctr_content_secondary @@ -22,16 +24,18 @@ - - diff --git a/library/ui-styles/src/main/res/values/tablet.xml b/library/ui-styles/src/main/res/values/tablet.xml index a5df8fe17c..8460f0ccf8 100644 --- a/library/ui-styles/src/main/res/values/tablet.xml +++ b/library/ui-styles/src/main/res/values/tablet.xml @@ -2,5 +2,6 @@ 1 + false \ No newline at end of file diff --git a/matrix-sdk-android-rx/.gitignore b/matrix-sdk-android-rx/.gitignore deleted file mode 100644 index 796b96d1c4..0000000000 --- a/matrix-sdk-android-rx/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/matrix-sdk-android-rx/build.gradle b/matrix-sdk-android-rx/build.gradle deleted file mode 100644 index dbd761cee3..0000000000 --- a/matrix-sdk-android-rx/build.gradle +++ /dev/null @@ -1,47 +0,0 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' - -android { - compileSdk versions.compileSdk - - defaultConfig { - minSdk versions.minSdk - targetSdk versions.targetSdk - - // Multidex is useful for tests - multiDexEnabled true - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - - compileOptions { - sourceCompatibility versions.sourceCompat - targetCompatibility versions.targetCompat - } - - kotlinOptions { - jvmTarget = "11" - } -} - -dependencies { - - implementation project(":matrix-sdk-android") - implementation libs.androidx.appCompat - implementation libs.rx.rxKotlin - implementation libs.rx.rxAndroid - implementation libs.jetbrains.coroutinesRx2 - - // Paging - implementation libs.androidx.pagingRuntimeKtx - - // Logging - implementation libs.jakewharton.timber -} diff --git a/matrix-sdk-android-rx/proguard-rules.pro b/matrix-sdk-android-rx/proguard-rules.pro deleted file mode 100644 index f1b424510d..0000000000 --- a/matrix-sdk-android-rx/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile diff --git a/matrix-sdk-android-rx/src/main/AndroidManifest.xml b/matrix-sdk-android-rx/src/main/AndroidManifest.xml deleted file mode 100644 index 5f399e9f84..0000000000 --- a/matrix-sdk-android-rx/src/main/AndroidManifest.xml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/LiveDataObservable.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/LiveDataObservable.kt deleted file mode 100644 index 56b52facf9..0000000000 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/LiveDataObservable.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.rx - -import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer -import io.reactivex.Observable -import io.reactivex.android.MainThreadDisposable -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.schedulers.Schedulers - -private class LiveDataObservable( - private val liveData: LiveData, - private val valueIfNull: T? = null -) : Observable() { - - override fun subscribeActual(observer: io.reactivex.Observer) { - val relay = RemoveObserverInMainThread(observer) - observer.onSubscribe(relay) - liveData.observeForever(relay) - } - - private inner class RemoveObserverInMainThread(private val observer: io.reactivex.Observer) : - MainThreadDisposable(), Observer { - - override fun onChanged(t: T?) { - if (!isDisposed) { - if (t == null) { - if (valueIfNull != null) { - observer.onNext(valueIfNull) - } else { - observer.onError(NullPointerException( - "convert liveData value t to RxJava onNext(t), t cannot be null")) - } - } else { - observer.onNext(t) - } - } - } - - override fun onDispose() { - liveData.removeObserver(this) - } - } -} - -fun LiveData.asObservable(): Observable { - return LiveDataObservable(this).observeOn(Schedulers.computation()) -} - -internal fun Observable.startWithCallable(supplier: () -> T): Observable { - val startObservable = Observable - .fromCallable(supplier) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - return startWith(startObservable) -} diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt deleted file mode 100644 index b3495c4493..0000000000 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.rx - -import android.net.Uri -import io.reactivex.Completable -import io.reactivex.Observable -import io.reactivex.Single -import kotlinx.coroutines.rx2.rxCompletable -import kotlinx.coroutines.rx2.rxSingle -import org.matrix.android.sdk.api.query.QueryStringValue -import org.matrix.android.sdk.api.session.content.ContentAttachmentData -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.identity.ThreePid -import org.matrix.android.sdk.api.session.room.Room -import org.matrix.android.sdk.api.session.room.members.RoomMemberQueryParams -import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary -import org.matrix.android.sdk.api.session.room.model.GuestAccess -import org.matrix.android.sdk.api.session.room.model.ReadReceipt -import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility -import org.matrix.android.sdk.api.session.room.model.RoomJoinRules -import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary -import org.matrix.android.sdk.api.session.room.model.RoomSummary -import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState -import org.matrix.android.sdk.api.session.room.send.UserDraft -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent -import org.matrix.android.sdk.api.util.Optional -import org.matrix.android.sdk.api.util.toOptional - -class RxRoom(private val room: Room) { - - fun liveRoomSummary(): Observable> { - return room.getRoomSummaryLive() - .asObservable() - .startWithCallable { room.roomSummary().toOptional() } - } - - fun liveRoomMembers(queryParams: RoomMemberQueryParams): Observable> { - return room.getRoomMembersLive(queryParams).asObservable() - .startWithCallable { - room.getRoomMembers(queryParams) - } - } - - fun liveAnnotationSummary(eventId: String): Observable> { - return room.getEventAnnotationsSummaryLive(eventId).asObservable() - .startWithCallable { - room.getEventAnnotationsSummary(eventId).toOptional() - } - } - - fun liveTimelineEvent(eventId: String): Observable> { - return room.getTimeLineEventLive(eventId).asObservable() - .startWithCallable { - room.getTimeLineEvent(eventId).toOptional() - } - } - - fun liveStateEvent(eventType: String, stateKey: QueryStringValue): Observable> { - return room.getStateEventLive(eventType, stateKey).asObservable() - .startWithCallable { - room.getStateEvent(eventType, stateKey).toOptional() - } - } - - fun liveStateEvents(eventTypes: Set): Observable> { - return room.getStateEventsLive(eventTypes).asObservable() - .startWithCallable { - room.getStateEvents(eventTypes) - } - } - - fun liveReadMarker(): Observable> { - return room.getReadMarkerLive().asObservable() - } - - fun liveReadReceipt(): Observable> { - return room.getMyReadReceiptLive().asObservable() - } - - fun loadRoomMembersIfNeeded(): Single = rxSingle { - room.loadRoomMembersIfNeeded() - } - - fun joinRoom(reason: String? = null, - viaServers: List = emptyList()): Single = rxSingle { - room.join(reason, viaServers) - } - - fun liveEventReadReceipts(eventId: String): Observable> { - return room.getEventReadReceiptsLive(eventId).asObservable() - } - - fun liveDraft(): Observable> { - return room.getDraftLive().asObservable() - .startWithCallable { - room.getDraft().toOptional() - } - } - - fun liveNotificationState(): Observable { - return room.getLiveRoomNotificationState().asObservable() - } - - fun invite(userId: String, reason: String? = null): Completable = rxCompletable { - room.invite(userId, reason) - } - - fun invite3pid(threePid: ThreePid): Completable = rxCompletable { - room.invite3pid(threePid) - } - - fun updateTopic(topic: String): Completable = rxCompletable { - room.updateTopic(topic) - } - - fun updateName(name: String): Completable = rxCompletable { - room.updateName(name) - } - - fun updateHistoryReadability(readability: RoomHistoryVisibility): Completable = rxCompletable { - room.updateHistoryReadability(readability) - } - - fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?): Completable = rxCompletable { - room.updateJoinRule(joinRules, guestAccess) - } - - fun updateAvatar(avatarUri: Uri, fileName: String): Completable = rxCompletable { - room.updateAvatar(avatarUri, fileName) - } - - fun deleteAvatar(): Completable = rxCompletable { - room.deleteAvatar() - } - - fun sendMedia(attachment: ContentAttachmentData, compressBeforeSending: Boolean, roomIds: Set): Completable = rxCompletable { - room.sendMedia(attachment, compressBeforeSending, roomIds) - } -} - -fun Room.rx(): RxRoom { - return RxRoom(this) -} diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt deleted file mode 100644 index 47203816b4..0000000000 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt +++ /dev/null @@ -1,251 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.rx - -import androidx.paging.PagedList -import io.reactivex.Observable -import io.reactivex.Single -import io.reactivex.functions.Function3 -import kotlinx.coroutines.rx2.rxSingle -import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.query.QueryStringValue -import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent -import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo -import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.group.GroupSummaryQueryParams -import org.matrix.android.sdk.api.session.group.model.GroupSummary -import org.matrix.android.sdk.api.session.identity.FoundThreePid -import org.matrix.android.sdk.api.session.identity.ThreePid -import org.matrix.android.sdk.api.session.pushers.Pusher -import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams -import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataEvent -import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState -import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary -import org.matrix.android.sdk.api.session.room.model.RoomSummary -import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams -import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams -import org.matrix.android.sdk.api.session.sync.SyncState -import org.matrix.android.sdk.api.session.user.model.User -import org.matrix.android.sdk.api.session.widgets.model.Widget -import org.matrix.android.sdk.api.util.JsonDict -import org.matrix.android.sdk.api.util.Optional -import org.matrix.android.sdk.api.util.toOptional -import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo -import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo -import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription - -class RxSession(private val session: Session) { - - fun liveRoomSummaries(queryParams: RoomSummaryQueryParams): Observable> { - return session.getRoomSummariesLive(queryParams).asObservable() - .startWithCallable { - session.getRoomSummaries(queryParams) - } - } - - fun liveGroupSummaries(queryParams: GroupSummaryQueryParams): Observable> { - return session.getGroupSummariesLive(queryParams).asObservable() - .startWithCallable { - session.getGroupSummaries(queryParams) - } - } - - fun liveSpaceSummaries(queryParams: SpaceSummaryQueryParams): Observable> { - return session.spaceService().getSpaceSummariesLive(queryParams).asObservable() - .startWithCallable { - session.spaceService().getSpaceSummaries(queryParams) - } - } - - fun liveBreadcrumbs(queryParams: RoomSummaryQueryParams): Observable> { - return session.getBreadcrumbsLive(queryParams).asObservable() - .startWithCallable { - session.getBreadcrumbs(queryParams) - } - } - - fun liveMyDevicesInfo(): Observable> { - return session.cryptoService().getLiveMyDevicesInfo().asObservable() - .startWithCallable { - session.cryptoService().getMyDevicesInfo() - } - } - - fun liveSyncState(): Observable { - return session.getSyncStateLive().asObservable() - } - - fun livePushers(): Observable> { - return session.getPushersLive().asObservable() - } - - fun liveUser(userId: String): Observable> { - return session.getUserLive(userId).asObservable() - .startWithCallable { - session.getUser(userId).toOptional() - } - } - - fun liveRoomMember(userId: String, roomId: String): Observable> { - return session.getRoomMemberLive(userId, roomId).asObservable() - .startWithCallable { - session.getRoomMember(userId, roomId).toOptional() - } - } - - fun liveUsers(): Observable> { - return session.getUsersLive().asObservable() - } - - fun liveIgnoredUsers(): Observable> { - return session.getIgnoredUsersLive().asObservable() - } - - fun livePagedUsers(filter: String? = null, excludedUserIds: Set? = null): Observable> { - return session.getPagedUsersLive(filter, excludedUserIds).asObservable() - } - - fun liveThreePIds(refreshData: Boolean): Observable> { - return session.getThreePidsLive(refreshData).asObservable() - .startWithCallable { session.getThreePids() } - } - - fun livePendingThreePIds(): Observable> { - return session.getPendingThreePidsLive().asObservable() - .startWithCallable { session.getPendingThreePids() } - } - - fun createRoom(roomParams: CreateRoomParams): Single = rxSingle { - session.createRoom(roomParams) - } - - fun searchUsersDirectory(search: String, - limit: Int, - excludedUserIds: Set): Single> = rxSingle { - session.searchUsersDirectory(search, limit, excludedUserIds) - } - - fun joinRoom(roomIdOrAlias: String, - reason: String? = null, - viaServers: List = emptyList()): Single = rxSingle { - session.joinRoom(roomIdOrAlias, reason, viaServers) - } - - fun getRoomIdByAlias(roomAlias: String, - searchOnServer: Boolean): Single> = rxSingle { - session.getRoomIdByAlias(roomAlias, searchOnServer) - } - - fun getProfileInfo(userId: String): Single = rxSingle { - session.getProfile(userId) - } - - fun liveUserCryptoDevices(userId: String): Observable> { - return session.cryptoService().getLiveCryptoDeviceInfo(userId).asObservable().startWithCallable { - session.cryptoService().getCryptoDeviceInfo(userId) - } - } - - fun liveCrossSigningInfo(userId: String): Observable> { - return session.cryptoService().crossSigningService().getLiveCrossSigningKeys(userId).asObservable() - .startWithCallable { - session.cryptoService().crossSigningService().getUserCrossSigningKeys(userId).toOptional() - } - } - - fun liveCrossSigningPrivateKeys(): Observable> { - return session.cryptoService().crossSigningService().getLiveCrossSigningPrivateKeys().asObservable() - .startWithCallable { - session.cryptoService().crossSigningService().getCrossSigningPrivateKeys().toOptional() - } - } - - fun liveUserAccountData(types: Set): Observable> { - return session.accountDataService().getLiveUserAccountDataEvents(types).asObservable() - .startWithCallable { - session.accountDataService().getUserAccountDataEvents(types) - } - } - - fun liveRoomAccountData(types: Set): Observable> { - return session.accountDataService().getLiveRoomAccountDataEvents(types).asObservable() - .startWithCallable { - session.accountDataService().getRoomAccountDataEvents(types) - } - } - - fun liveRoomWidgets( - roomId: String, - widgetId: QueryStringValue, - widgetTypes: Set? = null, - excludedTypes: Set? = null - ): Observable> { - return session.widgetService().getRoomWidgetsLive(roomId, widgetId, widgetTypes, excludedTypes).asObservable() - .startWithCallable { - session.widgetService().getRoomWidgets(roomId, widgetId, widgetTypes, excludedTypes) - } - } - - fun liveRoomChangeMembershipState(): Observable> { - return session.getChangeMembershipsLive().asObservable() - } - - fun liveSecretSynchronisationInfo(): Observable { - return Observable.combineLatest, Optional, Optional, SecretsSynchronisationInfo>( - liveUserAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME)), - liveCrossSigningInfo(session.myUserId), - liveCrossSigningPrivateKeys(), - Function3 { _, crossSigningInfo, pInfo -> - // first check if 4S is already setup - val is4SSetup = session.sharedSecretStorageService.isRecoverySetup() - val isCrossSigningEnabled = crossSigningInfo.getOrNull() != null - val isCrossSigningTrusted = crossSigningInfo.getOrNull()?.isTrusted() == true - val allPrivateKeysKnown = pInfo.getOrNull()?.allKnown().orFalse() - - val keysBackupService = session.cryptoService().keysBackupService() - val currentBackupVersion = keysBackupService.currentBackupVersion - val megolmBackupAvailable = currentBackupVersion != null - val savedBackupKey = keysBackupService.getKeyBackupRecoveryKeyInfo() - - val megolmKeyKnown = savedBackupKey?.version == currentBackupVersion - SecretsSynchronisationInfo( - isBackupSetup = is4SSetup, - isCrossSigningEnabled = isCrossSigningEnabled, - isCrossSigningTrusted = isCrossSigningTrusted, - allPrivateKeysKnown = allPrivateKeysKnown, - megolmBackupAvailable = megolmBackupAvailable, - megolmSecretKnown = megolmKeyKnown, - isMegolmKeyIn4S = session.sharedSecretStorageService.isMegolmKeyInBackup() - ) - } - ) - .distinctUntilChanged() - } - - fun lookupThreePid(threePid: ThreePid): Single> = rxSingle { - session.identityService().lookUp(listOf(threePid)).firstOrNull().toOptional() - } -} - -fun Session.rx(): RxSession { - return RxSession(this) -} diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 8b32b7dbc5..7b9a611c72 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -31,7 +31,7 @@ android { // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' - buildConfigField "String", "SDK_VERSION", "\"1.3.15\"" + buildConfigField "String", "SDK_VERSION", "\"1.3.16\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" resValue "string", "git_sdk_revision", "\"${gitRevision()}\"" @@ -45,7 +45,7 @@ android { testOptions { // Comment to run on Android 12 - execution 'ANDROIDX_TEST_ORCHESTRATOR' +// execution 'ANDROIDX_TEST_ORCHESTRATOR' } buildTypes { @@ -64,6 +64,7 @@ android { adbOptions { installOptions "-g" +// timeOutInMs 350 * 1000 } compileOptions { @@ -115,6 +116,11 @@ dependencies { implementation libs.squareup.retrofit implementation libs.squareup.retrofitMoshi + // When version of okhttp is updated (current is 4.9.3), consider removing the workaround + // to force usage of Protocol.HTTP_1_1. Check the status of: + // - https://github.com/square/okhttp/issues/3278 + // - https://github.com/square/okhttp/issues/4455 + // - https://github.com/square/okhttp/issues/3146 implementation(platform("com.squareup.okhttp3:okhttp-bom:4.9.3")) implementation 'com.squareup.okhttp3:okhttp' implementation 'com.squareup.okhttp3:logging-interceptor' @@ -158,7 +164,7 @@ dependencies { implementation libs.apache.commonsImaging // Phone number https://github.com/google/libphonenumber - implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.40' + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.41' testImplementation libs.tests.junit testImplementation 'org.robolectric:robolectric:4.7.3' diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt index e0451bea38..486bc02769 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/AccountCreationTest.kt @@ -16,7 +16,9 @@ package org.matrix.android.sdk.account +import androidx.test.filters.LargeTest import org.junit.FixMethodOrder +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 @@ -29,6 +31,7 @@ import org.matrix.android.sdk.common.TestConstants @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) +@LargeTest class AccountCreationTest : InstrumentedTest { private val commonTestHelper = CommonTestHelper(context()) @@ -42,6 +45,7 @@ class AccountCreationTest : InstrumentedTest { } @Test + @Ignore("This test will be ignored until it is fixed") fun createAccountAndLoginAgainTest() { val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true)) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt index d32bcb3fe5..933074cdce 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.account import org.amshove.kluent.shouldBeTrue import org.junit.FixMethodOrder +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 @@ -30,6 +31,7 @@ import org.matrix.android.sdk.common.TestConstants @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) +@Ignore("This test will be ignored until it is fixed") class ChangePasswordTest : InstrumentedTest { private val commonTestHelper = CommonTestHelper(context()) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/RetryTestRule.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/RetryTestRule.kt new file mode 100644 index 0000000000..b16ab98e6c --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/RetryTestRule.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.common + +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +/** + * Retry test rule used to retry test that failed. + * Retry failed test 3 times + */ +class RetryTestRule(val retryCount: Int = 3) : TestRule { + + override fun apply(base: Statement, description: Description): Statement { + return statement(base) + } + + private fun statement(base: Statement): Statement { + return object : Statement() { + @Throws(Throwable::class) + override fun evaluate() { + var caughtThrowable: Throwable? = null + + // implement retry logic here + for (i in 0 until retryCount) { + try { + base.evaluate() + return + } catch (t: Throwable) { + caughtThrowable = t + } + } + throw caughtThrowable!! + } + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestConstants.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestConstants.kt index 8eb7e251e2..5c9b79361e 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestConstants.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestConstants.kt @@ -22,8 +22,8 @@ object TestConstants { const val TESTS_HOME_SERVER_URL = "http://10.0.2.2:8080" - // Time out to use when waiting for server response. 20s - private const val AWAIT_TIME_OUT_MILLIS = 20_000 + // Time out to use when waiting for server response. + private const val AWAIT_TIME_OUT_MILLIS = 30_000 // Time out to use when waiting for server response, when the debugger is connected. 10 minutes private const val AWAIT_TIME_OUT_WITH_DEBUGGER_MILLIS = 10 * 60_000 diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt index d0f63227f5..c95cc6b4ca 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt @@ -21,6 +21,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.FixMethodOrder +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -40,6 +41,7 @@ class PreShareKeysTest : InstrumentedTest { private val cryptoTestHelper = CryptoTestHelper(testHelper) @Test + @Ignore("This test will be ignored until it is fixed") fun ensure_outbound_session_happy_path() { val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val e2eRoomID = testData.roomId @@ -97,7 +99,6 @@ class PreShareKeysTest : InstrumentedTest { } } - testHelper.signOutAndClose(aliceSession) - testHelper.signOutAndClose(bobSession) + testData.cleanUp(testHelper) } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt index 458eae6ab2..0a8ce67680 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt @@ -21,6 +21,7 @@ import org.amshove.kluent.shouldBe import org.junit.Assert import org.junit.Before import org.junit.FixMethodOrder +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -84,6 +85,7 @@ class UnwedgingTest : InstrumentedTest { * -> This is automatically fixed after SDKs restarted the olm session */ @Test + @Ignore("This test will be ignored until it is fixed") fun testUnwedging() { val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt index d9cc7a8ac0..a6e8f94c91 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.crypto.crosssigning import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull @@ -24,6 +25,7 @@ import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.FixMethodOrder +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -43,6 +45,7 @@ import kotlin.coroutines.resume @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) +@LargeTest class XSigningTest : InstrumentedTest { private val testHelper = CommonTestHelper(context()) @@ -124,11 +127,11 @@ class XSigningTest : InstrumentedTest { assertFalse("Bob keys from alice pov should not be trusted", bobKeysFromAlicePOV.isTrusted()) - testHelper.signOutAndClose(aliceSession) - testHelper.signOutAndClose(bobSession) + cryptoTestData.cleanUp(testHelper) } @Test + @Ignore("This test will be ignored until it is fixed") fun test_CrossSigningTestAliceTrustBobNewDevice() { val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt index 189fc405eb..060201d624 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt @@ -62,7 +62,7 @@ class EncryptionTest : InstrumentedTest { // Send an encryption Event as a State Event room.sendStateEvent( eventType = EventType.STATE_ROOM_ENCRYPTION, - stateKey = null, + stateKey = "", body = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent() ) } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt index 975d481628..e0605db0b8 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt @@ -18,12 +18,14 @@ package org.matrix.android.sdk.internal.crypto.gossiping import android.util.Log import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertTrue import junit.framework.TestCase.fail import org.junit.Assert import org.junit.FixMethodOrder +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -59,11 +61,13 @@ import kotlin.coroutines.resume @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.JVM) +@LargeTest class KeyShareTests : InstrumentedTest { private val commonTestHelper = CommonTestHelper(context()) @Test + @Ignore("This test will be ignored until it is fixed") fun test_DoNotSelfShareIfNotTrusted() { val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) @@ -195,6 +199,7 @@ class KeyShareTests : InstrumentedTest { } @Test + @Ignore("This test will be ignored until it is fixed") fun test_ShareSSSSSecret() { val aliceSession1 = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) @@ -307,6 +312,7 @@ class KeyShareTests : InstrumentedTest { } @Test + @Ignore("This test will be ignored until it is fixed") fun test_ImproperKeyShareBug() { val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt index c835c2d40b..586d96b007 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt @@ -18,8 +18,10 @@ package org.matrix.android.sdk.internal.crypto.gossiping import android.util.Log import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest import org.junit.Assert import org.junit.FixMethodOrder +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -39,12 +41,14 @@ import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.JVM) +@LargeTest class WithHeldTests : InstrumentedTest { private val testHelper = CommonTestHelper(context()) private val cryptoTestHelper = CryptoTestHelper(testHelper) @Test + @Ignore("This test will be ignored until it is fixed") fun test_WithHeldUnverifiedReason() { // ============================= // ARRANGE @@ -129,6 +133,7 @@ class WithHeldTests : InstrumentedTest { } @Test + @Ignore("This test will be ignored until it is fixed") fun test_WithHeldNoOlm() { val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = testData.firstSession @@ -199,6 +204,7 @@ class WithHeldTests : InstrumentedTest { } @Test + @Ignore("This test will be ignored until it is fixed") fun test_WithHeldKeyRequest() { val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = testData.firstSession diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt index 2a07b74115..4c94566219 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt @@ -17,12 +17,14 @@ package org.matrix.android.sdk.internal.crypto.keysbackup import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.FixMethodOrder +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -47,6 +49,7 @@ import java.util.concurrent.CountDownLatch @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.JVM) +@LargeTest class KeysBackupTest : InstrumentedTest { private val testHelper = CommonTestHelper(context()) @@ -59,6 +62,7 @@ class KeysBackupTest : InstrumentedTest { * - Reset keys backup markers */ @Test + @Ignore("This test will be ignored until it is fixed") fun roomKeysTest_testBackupStore_ok() { val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() @@ -157,6 +161,7 @@ class KeysBackupTest : InstrumentedTest { * - Check the backup completes */ @Test + @Ignore("This test will be ignored until it is fixed") fun backupAfterCreateKeysBackupVersionTest() { val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() @@ -197,6 +202,7 @@ class KeysBackupTest : InstrumentedTest { * Check that backupAllGroupSessions() returns valid data */ @Test + @Ignore("This test will be ignored until it is fixed") fun backupAllGroupSessionsTest() { val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() @@ -241,6 +247,7 @@ class KeysBackupTest : InstrumentedTest { * - Compare the decrypted megolm key with the original one */ @Test + @Ignore("This test will be ignored until it is fixed") fun testEncryptAndDecryptKeysBackupData() { val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() @@ -282,6 +289,7 @@ class KeysBackupTest : InstrumentedTest { * - Restore must be successful */ @Test + @Ignore("This test will be ignored until it is fixed") fun restoreKeysBackupTest() { val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null) @@ -365,6 +373,7 @@ class KeysBackupTest : InstrumentedTest { * - It must be trusted and must have with 2 signatures now */ @Test + @Ignore("This test will be ignored until it is fixed") fun trustKeyBackupVersionTest() { // - Do an e2e backup to the homeserver with a recovery key // - And log Alice on a new device @@ -424,6 +433,7 @@ class KeysBackupTest : InstrumentedTest { * - It must be trusted and must have with 2 signatures now */ @Test + @Ignore("This test will be ignored until it is fixed") fun trustKeyBackupVersionWithRecoveryKeyTest() { // - Do an e2e backup to the homeserver with a recovery key // - And log Alice on a new device @@ -481,6 +491,7 @@ class KeysBackupTest : InstrumentedTest { * - The backup must still be untrusted and disabled */ @Test + @Ignore("This test will be ignored until it is fixed") fun trustKeyBackupVersionWithWrongRecoveryKeyTest() { // - Do an e2e backup to the homeserver with a recovery key // - And log Alice on a new device @@ -522,6 +533,7 @@ class KeysBackupTest : InstrumentedTest { * - It must be trusted and must have with 2 signatures now */ @Test + @Ignore("This test will be ignored until it is fixed") fun trustKeyBackupVersionWithPasswordTest() { val password = "Password" @@ -581,6 +593,7 @@ class KeysBackupTest : InstrumentedTest { * - The backup must still be untrusted and disabled */ @Test + @Ignore("This test will be ignored until it is fixed") fun trustKeyBackupVersionWithWrongPasswordTest() { val password = "Password" val badPassword = "Bad Password" @@ -621,6 +634,7 @@ class KeysBackupTest : InstrumentedTest { * - It must fail */ @Test + @Ignore("This test will be ignored until it is fixed") fun restoreKeysBackupWithAWrongRecoveryKeyTest() { val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null) @@ -654,6 +668,7 @@ class KeysBackupTest : InstrumentedTest { * - Restore must be successful */ @Test + @Ignore("This test will be ignored until it is fixed") fun testBackupWithPassword() { val password = "password" @@ -709,6 +724,7 @@ class KeysBackupTest : InstrumentedTest { * - It must fail */ @Test + @Ignore("This test will be ignored until it is fixed") fun restoreKeysBackupWithAWrongPasswordTest() { val password = "password" val wrongPassword = "passw0rd" @@ -745,6 +761,7 @@ class KeysBackupTest : InstrumentedTest { * - Restore must be successful */ @Test + @Ignore("This test will be ignored until it is fixed") fun testUseRecoveryKeyToRestoreAPasswordBasedKeysBackup() { val password = "password" @@ -773,6 +790,7 @@ class KeysBackupTest : InstrumentedTest { * - It must fail */ @Test + @Ignore("This test will be ignored until it is fixed") fun testUsePasswordToRestoreARecoveryKeyBasedKeysBackup() { val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null) @@ -804,6 +822,7 @@ class KeysBackupTest : InstrumentedTest { * - Check the returned KeysVersionResult is trusted */ @Test + @Ignore("This test will be ignored until it is fixed") fun testIsKeysBackupTrusted() { // - Create a backup version val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() @@ -847,6 +866,7 @@ class KeysBackupTest : InstrumentedTest { * -> The new alice session must back up to the same version */ @Test + @Ignore("This test will be ignored until it is fixed") fun testCheckAndStartKeysBackupWhenRestartingAMatrixSession() { // - Create a backup version val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() @@ -978,6 +998,7 @@ class KeysBackupTest : InstrumentedTest { * -> It must success */ @Test + @Ignore("This test will be ignored until it is fixed") fun testBackupAfterVerifyingADevice() { // - Create a backup version val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt index 43f8dc0762..67f17727b1 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt @@ -22,6 +22,7 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.FixMethodOrder +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -47,8 +48,6 @@ import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorage @FixMethodOrder(MethodSorters.JVM) class QuadSTests : InstrumentedTest { - private val testHelper = CommonTestHelper(context()) - private val emptyKeySigner = object : KeySigner { override fun sign(canonicalJson: String): Map>? { return null @@ -57,6 +56,8 @@ class QuadSTests : InstrumentedTest { @Test fun test_Generate4SKey() { + val testHelper = CommonTestHelper(context()) + val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) val quadS = aliceSession.sharedSecretStorageService @@ -108,6 +109,8 @@ class QuadSTests : InstrumentedTest { @Test fun test_StoreSecret() { + val testHelper = CommonTestHelper(context()) + val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) val keyId = "My.Key" val info = generatedSecret(aliceSession, keyId, true) @@ -151,6 +154,8 @@ class QuadSTests : InstrumentedTest { @Test fun test_SetDefaultLocalEcho() { + val testHelper = CommonTestHelper(context()) + val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) val quadS = aliceSession.sharedSecretStorageService @@ -171,6 +176,8 @@ class QuadSTests : InstrumentedTest { @Test fun test_StoreSecretWithMultipleKey() { + val testHelper = CommonTestHelper(context()) + val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) val keyId1 = "Key.1" val key1Info = generatedSecret(aliceSession, keyId1, true) @@ -217,7 +224,10 @@ class QuadSTests : InstrumentedTest { } @Test + @Ignore("Test is working locally, not in GitHub actions") fun test_GetSecretWithBadPassphrase() { + val testHelper = CommonTestHelper(context()) + val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) val keyId1 = "Key.1" val passphrase = "The good pass phrase" @@ -264,6 +274,8 @@ class QuadSTests : InstrumentedTest { } private fun assertAccountData(session: Session, type: String): UserAccountDataEvent { + val testHelper = CommonTestHelper(context()) + var accountData: UserAccountDataEvent? = null testHelper.waitWithLatch { val liveAccountData = session.accountDataService().getLiveUserAccountDataEvent(type) @@ -281,6 +293,7 @@ class QuadSTests : InstrumentedTest { private fun generatedSecret(session: Session, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo { val quadS = session.sharedSecretStorageService + val testHelper = CommonTestHelper(context()) val creationInfo = testHelper.runBlockingTest { quadS.generateKey(keyId, null, keyId, emptyKeySigner) @@ -300,6 +313,7 @@ class QuadSTests : InstrumentedTest { private fun generatedSecretFromPassphrase(session: Session, passphrase: String, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo { val quadS = session.sharedSecretStorageService + val testHelper = CommonTestHelper(context()) val creationInfo = testHelper.runBlockingTest { quadS.generateKeyWithPassphrase( diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt index c914da6f71..8cd725504d 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt @@ -25,6 +25,7 @@ import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.FixMethodOrder +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -53,11 +54,11 @@ import java.util.concurrent.CountDownLatch @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) class SASTest : InstrumentedTest { - private val testHelper = CommonTestHelper(context()) - private val cryptoTestHelper = CryptoTestHelper(testHelper) @Test fun test_aliceStartThenAliceCancel() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = cryptoTestData.firstSession @@ -137,7 +138,10 @@ class SASTest : InstrumentedTest { } @Test + @Ignore("This test will be ignored until it is fixed") fun test_key_agreement_protocols_must_include_curve25519() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) fail("Not passing for the moment") val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() @@ -194,7 +198,10 @@ class SASTest : InstrumentedTest { } @Test + @Ignore("This test will be ignored until it is fixed") fun test_key_agreement_macs_Must_include_hmac_sha256() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) fail("Not passing for the moment") val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() @@ -232,7 +239,10 @@ class SASTest : InstrumentedTest { } @Test + @Ignore("This test will be ignored until it is fixed") fun test_key_agreement_short_code_include_decimal() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) fail("Not passing for the moment") val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() @@ -303,6 +313,8 @@ class SASTest : InstrumentedTest { // If a device has two verifications in progress with the same device, then it should cancel both verifications. @Test fun test_aliceStartTwoRequests() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = cryptoTestData.firstSession @@ -342,7 +354,10 @@ class SASTest : InstrumentedTest { * Test that when alice starts a 'correct' request, bob agrees. */ @Test + @Ignore("This test will be ignored until it is fixed") fun test_aliceAndBobAgreement() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = cryptoTestData.firstSession @@ -402,6 +417,8 @@ class SASTest : InstrumentedTest { @Test fun test_aliceAndBobSASCode() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = cryptoTestData.firstSession @@ -458,6 +475,8 @@ class SASTest : InstrumentedTest { @Test fun test_happyPath() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = cryptoTestData.firstSession @@ -527,9 +546,6 @@ class SASTest : InstrumentedTest { val bobDeviceInfoFromAlicePOV: CryptoDeviceInfo? = aliceSession.cryptoService().getDeviceInfo(bobUserId, bobDeviceId) val aliceDeviceInfoFromBobPOV: CryptoDeviceInfo? = bobSession.cryptoService().getDeviceInfo(aliceSession.myUserId, aliceSession.cryptoService().getMyDevice().deviceId) - // latch wait a bit again - Thread.sleep(1000) - assertTrue("alice device should be verified from bob point of view", aliceDeviceInfoFromBobPOV!!.isVerified) assertTrue("bob device should be verified from alice point of view", bobDeviceInfoFromAlicePOV!!.isVerified) cryptoTestData.cleanUp(testHelper) @@ -537,6 +553,8 @@ class SASTest : InstrumentedTest { @Test fun test_ConcurrentStart() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = cryptoTestData.firstSession diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt index 36306aa383..35c5a4dab9 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt @@ -40,8 +40,6 @@ import kotlin.coroutines.resume @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.JVM) class VerificationTest : InstrumentedTest { - private val testHelper = CommonTestHelper(context()) - private val cryptoTestHelper = CryptoTestHelper(testHelper) data class ExpectedResult( val sasIsSupported: Boolean = false, @@ -155,6 +153,8 @@ class VerificationTest : InstrumentedTest { bobSupportedMethods: List, expectedResultForAlice: ExpectedResult, expectedResultForBob: ExpectedResult) { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = cryptoTestData.firstSession diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt index 8625e97902..9856ee7770 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt @@ -21,6 +21,7 @@ import org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer import org.junit.Assert.assertEquals import org.junit.FixMethodOrder +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -132,6 +133,7 @@ class MarkdownParserTest : InstrumentedTest { * Note: the test is not passing, it does not work on Element Web neither */ @Test + @Ignore("This test will be ignored until it is fixed") fun parseStrike_not_passing() { testType( name = "strike", @@ -141,6 +143,7 @@ class MarkdownParserTest : InstrumentedTest { } @Test + @Ignore("This test will be ignored until it is fixed") fun parseStrikeNewLines() { testTypeNewLines( name = "strike", @@ -160,6 +163,7 @@ class MarkdownParserTest : InstrumentedTest { // TODO. Improve testTypeNewLines function to cover
test
@Test + @Ignore("This test will be ignored until it is fixed") fun parseCodeNewLines_not_passing() { testTypeNewLines( name = "code", @@ -179,6 +183,7 @@ class MarkdownParserTest : InstrumentedTest { } @Test + @Ignore("This test will be ignored until it is fixed") fun parseCode2NewLines_not_passing() { testTypeNewLines( name = "code", @@ -197,6 +202,7 @@ class MarkdownParserTest : InstrumentedTest { } @Test + @Ignore("This test will be ignored until it is fixed") fun parseCode3NewLines_not_passing() { testTypeNewLines( name = "code", @@ -233,6 +239,7 @@ class MarkdownParserTest : InstrumentedTest { } @Test + @Ignore("This test will be ignored until it is fixed") fun parseQuote_not_passing() { "> quoted\nline2".let { markdownParser.parse(it).expect(it, "

quoted
line2

") } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/SpaceOrderTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/ordering/SpaceOrderTest.kt similarity index 99% rename from matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/SpaceOrderTest.kt rename to matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/ordering/SpaceOrderTest.kt index 3270dfb757..50f4692edf 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/SpaceOrderTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/ordering/SpaceOrderTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.matrix.android.sdk +package org.matrix.android.sdk.ordering import org.amshove.kluent.internal.assertEquals import org.junit.Assert diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/StringOrderTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/ordering/StringOrderTest.kt similarity index 99% rename from matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/StringOrderTest.kt rename to matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/ordering/StringOrderTest.kt index a625362c04..728986441a 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/StringOrderTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/ordering/StringOrderTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.matrix.android.sdk +package org.matrix.android.sdk.ordering import org.amshove.kluent.internal.assertEquals import org.junit.Assert diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt index 05a43de0ac..ee44af58b3 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.session.room.timeline +import androidx.test.filters.LargeTest import kotlinx.coroutines.runBlocking import org.amshove.kluent.internal.assertEquals import org.amshove.kluent.shouldBeFalse @@ -40,16 +41,20 @@ import java.util.concurrent.CountDownLatch @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) +@LargeTest class TimelineForwardPaginationTest : InstrumentedTest { - private val commonTestHelper = CommonTestHelper(context()) - private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) +// @Rule +// @JvmField +// val mRetryTestRule = RetryTestRule() /** * This test ensure that if we click to permalink, we will be able to go back to the live */ @Test fun forwardPaginationTest() { + val commonTestHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(commonTestHelper) val numberOfMessagesToSend = 90 val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt index c6fdec150d..c6d40bcaa2 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.session.room.timeline +import androidx.test.filters.LargeTest import org.amshove.kluent.shouldBeFalse import org.amshove.kluent.shouldBeTrue import org.junit.FixMethodOrder @@ -38,16 +39,17 @@ import java.util.concurrent.CountDownLatch @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) +@LargeTest class TimelinePreviousLastForwardTest : InstrumentedTest { - private val commonTestHelper = CommonTestHelper(context()) - private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) - /** * This test ensure that if we have a chunk in the timeline which is due to a sync, and we click to permalink, we will be able to go back to the live */ + @Test fun previousLastForwardTest() { + val commonTestHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(commonTestHelper) val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false) val aliceSession = cryptoTestData.firstSession diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt index b75df9b5a2..53f76f1c46 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.session.room.timeline +import androidx.test.filters.LargeTest import kotlinx.coroutines.runBlocking import org.amshove.kluent.internal.assertEquals import org.junit.FixMethodOrder @@ -36,13 +37,13 @@ import org.matrix.android.sdk.common.TestConstants @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) +@LargeTest class TimelineSimpleBackPaginationTest : InstrumentedTest { - private val commonTestHelper = CommonTestHelper(context()) - private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) - @Test fun timeline_backPaginate_shouldReachEndOfTimeline() { + val commonTestHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(commonTestHelper) val numberOfMessagesToSent = 200 val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt index ace48cef77..ce02b2b527 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt @@ -16,8 +16,10 @@ package org.matrix.android.sdk.session.room.timeline +import androidx.test.filters.LargeTest import org.junit.Assert.fail import org.junit.FixMethodOrder +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 @@ -31,8 +33,13 @@ import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CryptoTestHelper import java.util.concurrent.CountDownLatch +/** !! Not working with the new timeline + * Disabling it until the fix is made + */ @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) +@Ignore("This test will be ignored until it is fixed") +@LargeTest class TimelineWithManyMembersTest : InstrumentedTest { companion object { @@ -45,6 +52,7 @@ class TimelineWithManyMembersTest : InstrumentedTest { /** * Ensures when someone sends a message to a crowded room, everyone can decrypt the message. */ + @Test fun everyone_should_decrypt_message_in_a_crowded_room() { val cryptoTestData = cryptoTestHelper.doE2ETestWithManyMembers(NUMBER_OF_MEMBERS) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt index 45e4b53c77..fa07cf5a02 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt @@ -37,9 +37,6 @@ class SearchMessagesTest : InstrumentedTest { private const val MESSAGE = "Lorem ipsum dolor sit amet" } - private val commonTestHelper = CommonTestHelper(context()) - private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) - @Test fun sendTextMessageAndSearchPartOfItUsingSession() { doTest { cryptoTestData -> @@ -76,6 +73,8 @@ class SearchMessagesTest : InstrumentedTest { } private fun doTest(block: suspend (CryptoTestData) -> SearchResult) { + val commonTestHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(commonTestHelper) val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false) val aliceSession = cryptoTestData.firstSession val aliceRoomId = cryptoTestData.roomId diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt index d7be19295c..3b0f7586cc 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.session.space +import androidx.test.filters.LargeTest import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals @@ -43,12 +44,12 @@ import org.matrix.android.sdk.common.SessionTestParams @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) +@LargeTest class SpaceCreationTest : InstrumentedTest { - private val commonTestHelper = CommonTestHelper(context()) - @Test fun createSimplePublicSpace() { + val commonTestHelper = CommonTestHelper(context()) val session = commonTestHelper.createAccount("Hubble", SessionTestParams(true)) val roomName = "My Space" val topic = "A public space for test" @@ -58,6 +59,7 @@ class SpaceCreationTest : InstrumentedTest { // wait a bit to let the summary update it self :/ it.countDown() } + Thread.sleep(4_000) val syncedSpace = session.spaceService().getSpace(spaceId) commonTestHelper.waitWithLatch { @@ -99,6 +101,8 @@ class SpaceCreationTest : InstrumentedTest { @Test fun testJoinSimplePublicSpace() { + val commonTestHelper = CommonTestHelper(context()) + val aliceSession = commonTestHelper.createAccount("alice", SessionTestParams(true)) val bobSession = commonTestHelper.createAccount("bob", SessionTestParams(true)) @@ -130,6 +134,7 @@ class SpaceCreationTest : InstrumentedTest { @Test fun testSimplePublicSpaceWithChildren() { + val commonTestHelper = CommonTestHelper(context()) val aliceSession = commonTestHelper.createAccount("alice", SessionTestParams(true)) val bobSession = commonTestHelper.createAccount("bob", SessionTestParams(true)) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt index 1c38edbbd9..5fbfaf99a0 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt @@ -23,6 +23,7 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.FixMethodOrder +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 @@ -50,10 +51,10 @@ import org.matrix.android.sdk.common.SessionTestParams @FixMethodOrder(MethodSorters.JVM) class SpaceHierarchyTest : InstrumentedTest { - private val commonTestHelper = CommonTestHelper(context()) - @Test fun createCanonicalChildRelation() { + val commonTestHelper = CommonTestHelper(context()) + val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val spaceName = "My Space" val topic = "A public space for test" @@ -170,6 +171,7 @@ class SpaceHierarchyTest : InstrumentedTest { @Test fun testFilteringBySpace() { + val commonTestHelper = CommonTestHelper(context()) val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val spaceAInfo = createPublicSpace(session, "SpaceA", listOf( @@ -236,7 +238,7 @@ class SpaceHierarchyTest : InstrumentedTest { it.countDown() } - Thread.sleep(2_000) + Thread.sleep(6_000) val orphansUpdate = session.getRoomSummaries(roomSummaryQueryParams { activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null) }) @@ -244,7 +246,9 @@ class SpaceHierarchyTest : InstrumentedTest { } @Test + @Ignore("This test will be ignored until it is fixed") fun testBreakCycle() { + val commonTestHelper = CommonTestHelper(context()) val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val spaceAInfo = createPublicSpace(session, "SpaceA", listOf( @@ -273,8 +277,6 @@ class SpaceHierarchyTest : InstrumentedTest { it.countDown() } - Thread.sleep(1000) - // A -> C -> A val aChildren = session.getFlattenRoomSummaryChildrenOf(spaceAInfo.spaceId) @@ -288,6 +290,7 @@ class SpaceHierarchyTest : InstrumentedTest { @Test fun testLiveFlatChildren() { + val commonTestHelper = CommonTestHelper(context()) val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val spaceAInfo = createPublicSpace(session, "SpaceA", listOf( @@ -374,6 +377,7 @@ class SpaceHierarchyTest : InstrumentedTest { childInfo: List> /** Name, auto-join, canonical*/ ): TestSpaceCreationResult { + val commonTestHelper = CommonTestHelper(context()) var spaceId = "" var roomIds: List = emptyList() commonTestHelper.waitWithLatch { latch -> @@ -401,6 +405,7 @@ class SpaceHierarchyTest : InstrumentedTest { childInfo: List> /** Name, auto-join, canonical*/ ): TestSpaceCreationResult { + val commonTestHelper = CommonTestHelper(context()) var spaceId = "" var roomIds: List = emptyList() commonTestHelper.waitWithLatch { latch -> @@ -435,6 +440,7 @@ class SpaceHierarchyTest : InstrumentedTest { @Test fun testRootSpaces() { + val commonTestHelper = CommonTestHelper(context()) val session = commonTestHelper.createAccount("John", SessionTestParams(true)) /* val spaceAInfo = */ createPublicSpace(session, "SpaceA", listOf( @@ -459,9 +465,10 @@ class SpaceHierarchyTest : InstrumentedTest { runBlocking { val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId) spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) + Thread.sleep(6_000) } - Thread.sleep(2000) +// Thread.sleep(4_000) // + A // a1, a2 // + B @@ -478,6 +485,7 @@ class SpaceHierarchyTest : InstrumentedTest { @Test fun testParentRelation() { + val commonTestHelper = CommonTestHelper(context()) val aliceSession = commonTestHelper.createAccount("Alice", SessionTestParams(true)) val bobSession = commonTestHelper.createAccount("Bib", SessionTestParams(true)) @@ -542,7 +550,7 @@ class SpaceHierarchyTest : InstrumentedTest { ?.setUserPowerLevel(aliceSession.myUserId, Role.Admin.value) ?.toContent() - room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, null, newPowerLevelsContent!!) + room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, stateKey = "", newPowerLevelsContent!!) it.countDown() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt index 13a26c89c1..aabe6e0d06 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt @@ -32,13 +32,18 @@ fun Throwable.is401() = fun Throwable.isTokenError() = this is Failure.ServerError && (error.code == MatrixError.M_UNKNOWN_TOKEN || - error.code == MatrixError.M_MISSING_TOKEN || - error.code == MatrixError.ORG_MATRIX_EXPIRED_ACCOUNT) + error.code == MatrixError.M_MISSING_TOKEN || + error.code == MatrixError.ORG_MATRIX_EXPIRED_ACCOUNT) + +fun Throwable.isLimitExceededError() = + this is Failure.ServerError && + httpCode == 429 && + error.code == MatrixError.M_LIMIT_EXCEEDED fun Throwable.shouldBeRetried(): Boolean { return this is Failure.NetworkConnection || this is IOException || - (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED) + this.isLimitExceededError() } /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/EventMatchCondition.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/EventMatchCondition.kt index eec5b0a402..65a13b4fec 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/EventMatchCondition.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/EventMatchCondition.kt @@ -56,7 +56,13 @@ class EventMatchCondition( if (wordsOnly) { value.caseInsensitiveFind(pattern) } else { - val modPattern = if (pattern.hasSpecialGlobChar()) pattern.simpleGlobToRegExp() else "*$pattern*".simpleGlobToRegExp() + val modPattern = if (pattern.hasSpecialGlobChar()) { + // Regex.containsMatchIn() is way faster without leading and trailing + // stars, that don't make any difference for the evaluation result + pattern.removePrefix("*").removeSuffix("*").simpleGlobToRegExp() + } else { + pattern.simpleGlobToRegExp() + } val regex = Regex(modPattern, RegexOption.DOT_MATCHES_ALL) regex.containsMatchIn(value) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt index 36ab007314..be924e2063 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt @@ -54,6 +54,7 @@ import org.matrix.android.sdk.api.session.securestorage.SecureStorageService import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService import org.matrix.android.sdk.api.session.signout.SignOutService import org.matrix.android.sdk.api.session.space.SpaceService +import org.matrix.android.sdk.api.session.statistics.StatisticsListener import org.matrix.android.sdk.api.session.sync.FilterService import org.matrix.android.sdk.api.session.sync.SyncState import org.matrix.android.sdk.api.session.sync.model.SyncResponse @@ -287,7 +288,7 @@ interface Session : /** * A global session listener to get notified for some events. */ - interface Listener : SessionLifecycleObserver { + interface Listener : StatisticsListener, SessionLifecycleObserver { /** * Called when the session received new invites to room so the client can react to it once. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/MembershipService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/MembershipService.kt index 198d6677a0..d5bc65c142 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/MembershipService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/MembershipService.kt @@ -75,9 +75,12 @@ interface MembershipService { suspend fun unban(userId: String, reason: String? = null) /** - * Kick a user from the room + * Remove a user from the room */ - suspend fun kick(userId: String, reason: String? = null) + suspend fun remove(userId: String, reason: String? = null) + + @Deprecated("Use remove instead", ReplaceWith("remove(userId, reason)")) + suspend fun kick(userId: String, reason: String? = null) = remove(userId, reason) /** * Join the room, or accept an invitation. diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/SecretsSynchronisationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAsset.kt similarity index 63% rename from matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/SecretsSynchronisationInfo.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAsset.kt index 6da3217070..e8b3cf2488 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/SecretsSynchronisationInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAsset.kt @@ -14,14 +14,12 @@ * limitations under the License. */ -package org.matrix.android.sdk.rx +package org.matrix.android.sdk.api.session.room.model.message -data class SecretsSynchronisationInfo( - val isBackupSetup: Boolean, - val isCrossSigningEnabled: Boolean, - val isCrossSigningTrusted: Boolean, - val allPrivateKeysKnown: Boolean, - val megolmBackupAvailable: Boolean, - val megolmSecretKnown: Boolean, - val isMegolmKeyIn4S: Boolean +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class LocationAsset( + @Json(name = "type") val type: LocationAssetType? = null ) diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/OptionalRx.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAssetType.kt similarity index 62% rename from matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/OptionalRx.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAssetType.kt index 936bd824e7..ef40e21c47 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/OptionalRx.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAssetType.kt @@ -14,17 +14,13 @@ * limitations under the License. */ -package org.matrix.android.sdk.rx +package org.matrix.android.sdk.api.session.room.model.message -import io.reactivex.Observable -import org.matrix.android.sdk.api.util.Optional +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass -fun Observable>.unwrap(): Observable { - return filter { it.hasValue() }.map { it.get() } -} - -fun Observable>.mapOptional(fn: (T) -> U?): Observable> { - return map { - it.map(fn) - } +@JsonClass(generateAdapter = false) +enum class LocationAssetType { + @Json(name = "m.self") + SELF } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt index a76c3c5b64..a1fd3bd2ec 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt @@ -18,29 +18,17 @@ package org.matrix.android.sdk.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo @JsonClass(generateAdapter = true) data class LocationInfo( /** - * The URL to the thumbnail of the file. Only present if the thumbnail is unencrypted. + * Required. RFC5870 formatted geo uri 'geo:latitude,longitude;uncertainty' like 'geo:40.05,29.24;30' representing this location. */ - @Json(name = "thumbnail_url") val thumbnailUrl: String? = null, + @Json(name = "uri") val geoUri: String? = null, /** - * Metadata about the image referred to in thumbnail_url. + * Required. A description of the location e.g. 'Big Ben, London, UK', or some kind + * of content description for accessibility e.g. 'location attachment'. */ - @Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null, - - /** - * Information on the encrypted thumbnail file, as specified in End-to-end encryption. Only present if the thumbnail is encrypted. - */ - @Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null + @Json(name = "description") val description: String? = null ) - -/** - * Get the url of the encrypted thumbnail or of the thumbnail - */ -fun LocationInfo.getThumbnailUrl(): String? { - return thumbnailFile?.url ?: thumbnailUrl -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt index 6881c09924..2f3db8ff51 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt @@ -26,7 +26,7 @@ data class MessageLocationContent( /** * Required. Must be 'm.location'. */ - @Json(name = MessageContent.MSG_TYPE_JSON_KEY) override val msgType: String, + @Json(name = MessageContent.MSG_TYPE_JSON_KEY) override val msgType: String = MessageType.MSGTYPE_LOCATION, /** * Required. A description of the location e.g. 'Big Ben, London, UK', or some kind @@ -35,15 +35,32 @@ data class MessageLocationContent( @Json(name = "body") override val body: String, /** - * Required. A geo URI representing this location. + * Required. RFC5870 formatted geo uri 'geo:latitude,longitude;uncertainty' like 'geo:40.05,29.24;30' representing this location. */ @Json(name = "geo_uri") val geoUri: String, /** - * + * See https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md */ - @Json(name = "info") val locationInfo: LocationInfo? = null, + @Json(name = "org.matrix.msc3488.location") val locationInfo: LocationInfo? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, - @Json(name = "m.new_content") override val newContent: Content? = null -) : MessageContent + @Json(name = "m.new_content") override val newContent: Content? = null, + + /** + * m.asset defines a generic asset that can be used for location tracking but also in other places like inventories, geofencing, checkins/checkouts etc. + * It should contain a mandatory namespaced type key defining what particular asset is being referred to. + * For the purposes of user location tracking m.self should be used in order to avoid duplicating the mxid. + */ + @Json(name = "m.asset") val locationAsset: LocationAsset? = null, + + /** + * Exact time that the data in the event refers to (milliseconds since the UNIX epoch) + */ + @Json(name = "org.matrix.msc3488.ts") val ts: Long? = null, + + @Json(name = "org.matrix.msc1767.text") val text: String? = null +) : MessageContent { + + fun getUri() = locationInfo?.geoUri ?: geoUri +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt index e652514b92..a82c01b159 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt @@ -22,7 +22,7 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class PollCreationInfo( @Json(name = "question") val question: PollQuestion? = null, - @Json(name = "kind") val kind: String? = "org.matrix.msc3381.poll.disclosed", + @Json(name = "kind") val kind: PollType? = PollType.DISCLOSED, @Json(name = "max_selections") val maxSelections: Int = 1, @Json(name = "answers") val answers: List? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollType.kt new file mode 100644 index 0000000000..3a8066b9bc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollType.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = false) +enum class PollType { + /** + * Voters should see results as soon as they have voted. + */ + @Json(name = "org.matrix.msc3381.poll.disclosed") + DISCLOSED, + + /** + * Results should be only revealed when the poll is ended. + */ + @Json(name = "org.matrix.msc3381.poll.undisclosed") + UNDISCLOSED +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt index 59d84ef40f..763d4bb892 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.room.model.relation import androidx.lifecycle.LiveData import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary +import org.matrix.android.sdk.api.session.room.model.message.PollType import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Optional @@ -64,6 +65,18 @@ interface RelationService { fun undoReaction(targetEventId: String, reaction: String): Cancelable + /** + * Edit a poll. + * @param pollType indicates open or closed polls + * @param targetEvent The poll event to edit + * @param question The edited question + * @param options The edited options + */ + fun editPoll(targetEvent: TimelineEvent, + pollType: PollType, + question: String, + options: List): Cancelable + /** * Edit a text message body. Limited to "m.text" contentType * @param targetEvent The event to edit diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt index 606500c4e7..20d00394df 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt @@ -20,6 +20,7 @@ import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.api.session.room.model.message.PollType import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.util.Cancelable @@ -91,11 +92,12 @@ interface SendService { /** * Send a poll to the room. + * @param pollType indicates open or closed polls * @param question the question * @param options list of options * @return a [Cancelable] */ - fun sendPoll(question: String, options: List): Cancelable + fun sendPoll(pollType: PollType, question: String, options: List): Cancelable /** * Method to send a poll response. @@ -131,6 +133,14 @@ interface SendService { */ fun resendMediaMessage(localEcho: TimelineEvent): Cancelable + /** + * Send a location event to the room + * @param latitude required latitude of the location + * @param longitude required longitude of the location + * @param uncertainty Accuracy of the location in meters + */ + fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?): Cancelable + /** * Remove this failed message from the timeline * @param localEcho the unsent local echo diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt index 4d3f95233d..e9b0e4f676 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt @@ -68,8 +68,11 @@ interface StateService { /** * Send a state event to the room + * @param eventType The type of event to send. + * @param stateKey The state_key for the state to send. Can be an empty string. + * @param body The content object of the event; the fields in this object will vary depending on the type of event */ - suspend fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict) + suspend fun sendStateEvent(eventType: String, stateKey: String, body: JsonDict) /** * Get a state event of the room diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt index 45dc322420..3f7d2d1278 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt @@ -133,7 +133,7 @@ fun TimelineEvent.getEditedEventId(): String? { fun TimelineEvent.getLastMessageContent(): MessageContent? { return when (root.getClearType()) { EventType.STICKER -> root.getClearContent().toModel() - EventType.POLL_START -> root.getClearContent().toModel() + EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/statistics/StatisticEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/statistics/StatisticEvent.kt new file mode 100644 index 0000000000..946792d31e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/statistics/StatisticEvent.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.api.session.statistics + +/** + * Statistic Events. You can subscribe to received such events using [Session.Listener] + */ +sealed interface StatisticEvent { + /** + * Initial sync request, response downloading, and treatment (parsing and storage) of response + */ + data class InitialSyncRequest(val requestDurationMs: Int, + val downloadDurationMs: Int, + val treatmentDurationMs: Int, + val nbOfJoinedRooms: Int) : StatisticEvent + + /** + * Incremental sync event + */ + data class SyncTreatment(val durationMs: Int, + val afterPause: Boolean, + val nbOfJoinedRooms: Int) : StatisticEvent +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/statistics/StatisticsListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/statistics/StatisticsListener.kt new file mode 100644 index 0000000000..a2cb7910a7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/statistics/StatisticsListener.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.api.session.statistics + +import org.matrix.android.sdk.api.session.Session + +interface StatisticsListener { + fun onStatisticsEvent(session: Session, statisticEvent: StatisticEvent) = Unit +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt index ad34a4d8a6..0cbbe1210d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt @@ -22,6 +22,7 @@ import dagger.Module import dagger.Provides import okhttp3.ConnectionSpec import okhttp3.OkHttpClient +import okhttp3.Protocol import okhttp3.logging.HttpLoggingInterceptor import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.MatrixConfiguration @@ -71,6 +72,8 @@ internal object NetworkModule { val spec = ConnectionSpec.Builder(matrixConfiguration.connectionSpec).build() return OkHttpClient.Builder() + // workaround for #4669 + .protocols(listOf(Protocol.HTTP_1_1)) .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS) .writeTimeout(60, TimeUnit.SECONDS) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt index 927d9f7dd2..695e7525af 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt @@ -19,8 +19,9 @@ package org.matrix.android.sdk.internal.network import kotlinx.coroutines.CancellationException import kotlinx.coroutines.delay import org.matrix.android.sdk.api.failure.Failure -import org.matrix.android.sdk.api.failure.MatrixError +import org.matrix.android.sdk.api.failure.GlobalError import org.matrix.android.sdk.api.failure.getRetryDelay +import org.matrix.android.sdk.api.failure.isLimitExceededError import org.matrix.android.sdk.api.failure.shouldBeRetried import org.matrix.android.sdk.internal.network.ssl.CertUtil import retrofit2.HttpException @@ -33,7 +34,8 @@ import java.io.IOException * * @param globalErrorReceiver will be use to notify error such as invalid token error. See [GlobalError] * @param canRetry if set to true, the request will be executed again in case of error, after a delay - * @param maxDelayBeforeRetry the max delay to wait before a retry + * @param maxDelayBeforeRetry the max delay to wait before a retry. Note that in the case of a 429, if the provided delay exceeds this value, the error will + * be propagated as it does not make sense to retry it with a shorter delay. * @param maxRetriesCount the max number of retries * @param requestBlock a suspend lambda to perform the network request */ @@ -74,23 +76,26 @@ internal suspend inline fun executeRequest(globalErrorReceiver: GlobalErr currentRetryCount++ - if (exception is Failure.ServerError && - exception.httpCode == 429 && - exception.error.code == MatrixError.M_LIMIT_EXCEEDED && - currentRetryCount < maxRetriesCount) { + if (exception.isLimitExceededError() && currentRetryCount < maxRetriesCount) { // 429, we can retry - delay(exception.getRetryDelay(1_000)) + val retryDelay = exception.getRetryDelay(1_000) + if (retryDelay <= maxDelayBeforeRetry) { + delay(retryDelay) + } else { + // delay is too high to be retried, propagate the exception + throw exception + } } else if (canRetry && currentRetryCount < maxRetriesCount && exception.shouldBeRetried()) { delay(currentDelay) currentDelay = currentDelay.times(2L).coerceAtMost(maxDelayBeforeRetry) // Try again (loop) } else { throw when (exception) { - is IOException -> Failure.NetworkConnection(exception) + is IOException -> Failure.NetworkConnection(exception) is Failure.ServerError, is Failure.OtherServerError, - is CancellationException -> exception - else -> Failure.Unknown(exception) + is CancellationException -> exception + else -> Failure.Unknown(exception) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt index 1fe7503141..1c3d1971c2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt @@ -130,7 +130,7 @@ internal class DefaultRoom(override val roomId: String, else -> { val params = SendStateTask.Params( roomId = roomId, - stateKey = null, + stateKey = "", eventType = EventType.STATE_ROOM_ENCRYPTION, body = mapOf( "algorithm" to algorithm diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index 62b6d626f5..1577f3057f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -34,10 +34,13 @@ import org.matrix.android.sdk.api.session.room.model.VoteInfo import org.matrix.android.sdk.api.session.room.model.VoteSummary import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollContent +import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper +import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent +import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent import org.matrix.android.sdk.internal.crypto.verification.toState import org.matrix.android.sdk.internal.database.mapper.ContentMapper @@ -55,6 +58,7 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.query.create import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource @@ -63,7 +67,9 @@ import javax.inject.Inject internal class EventRelationsAggregationProcessor @Inject constructor( @UserId private val userId: String, - private val stateEventDataSource: StateEventDataSource + private val stateEventDataSource: StateEventDataSource, + @SessionId private val sessionId: String, + private val sessionManager: SessionManager ) : EventInsertLiveProcessor { private val allowedTypes = listOf( @@ -79,6 +85,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( // EventType.KEY_VERIFICATION_READY, EventType.KEY_VERIFICATION_KEY, EventType.ENCRYPTED, + EventType.POLL_START, EventType.POLL_RESPONSE, EventType.POLL_END ) @@ -208,6 +215,14 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } } } + EventType.POLL_START -> { + val content: MessagePollContent? = event.content.toModel() + if (content?.relatesTo?.type == RelationType.REPLACE) { + Timber.v("###REPLACE in room $roomId for event ${event.eventId}") + // A replace! + handleReplace(realm, event, content, roomId, isLocalEcho) + } + } EventType.POLL_RESPONSE -> { event.content.toModel(catchError = true)?.let { handleResponse(realm, event, it, roomId, isLocalEcho) @@ -274,6 +289,20 @@ internal class EventRelationsAggregationProcessor @Inject constructor( Timber.v("###REPLACE ignoring event for summary, it's known $eventId") return } + + ContentMapper + .map(eventAnnotationsSummaryEntity.pollResponseSummary?.aggregatedContent) + ?.toModel() + ?.apply { + totalVotes = 0 + winnerVoteCount = 0 + votes = emptyList() + votesSummary = emptyMap() + } + ?.apply { + eventAnnotationsSummaryEntity.pollResponseSummary?.aggregatedContent = ContentMapper.map(toContent()) + } + val txId = event.unsignedData?.transactionId // is it a remote echo? if (!isLocalEcho && existingSummary.editions.any { it.eventId == txId }) { @@ -315,6 +344,16 @@ internal class EventRelationsAggregationProcessor @Inject constructor( val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return val eventTimestamp = event.originServerTs ?: return + val session = sessionManager.getSessionComponent(sessionId)?.session() + + val targetPollEvent = session?.getRoom(roomId)?.getTimeLineEvent(targetEventId) ?: return Unit.also { + Timber.v("## POLL target poll event $targetEventId not found in room $roomId") + } + + val targetPollContent = targetPollEvent.getLastMessageContent() as? MessagePollContent ?: return Unit.also { + Timber.v("## POLL target poll event $targetEventId content is malformed") + } + // ok, this is a poll response var existing = EventAnnotationsSummaryEntity.where(realm, roomId, targetEventId).findFirst() if (existing == null) { @@ -355,6 +394,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor( Timber.d("## POLL Ignoring malformed response no option eventId:$eventId content: ${event.content}") } + // Check if this option is in available options + if (!targetPollContent.pollCreationInfo?.answers?.map { it.id }?.contains(option).orFalse()) { + Timber.v("## POLL $targetEventId doesn't contain option $option") + return + } + val votes = sumModel.votes?.toMutableList() ?: ArrayList() val existingVoteIndex = votes.indexOfFirst { it.userId == senderId } if (existingVoteIndex != -1) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt index 6cf82dde44..49b58aa765 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt @@ -125,7 +125,7 @@ internal class DefaultMembershipService @AssistedInject constructor( membershipAdminTask.execute(params) } - override suspend fun kick(userId: String, reason: String?) { + override suspend fun remove(userId: String, reason: String?) { val params = MembershipAdminTask.Params(MembershipAdminTask.Type.KICK, roomId, userId, reason) membershipAdminTask.execute(params) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt index 07927b1412..cbcc108ddd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt @@ -24,6 +24,7 @@ import dagger.assisted.AssistedInject import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary +import org.matrix.android.sdk.api.session.room.model.message.PollType import org.matrix.android.sdk.api.session.room.model.relation.RelationService import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.util.Cancelable @@ -112,6 +113,13 @@ internal class DefaultRelationService @AssistedInject constructor( .executeBy(taskExecutor) } + override fun editPoll(targetEvent: TimelineEvent, + pollType: PollType, + question: String, + options: List): Cancelable { + return eventEditor.editPoll(targetEvent, pollType, question, options) + } + override fun editTextMessage(targetEvent: TimelineEvent, msgType: String, newBodyText: CharSequence, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt index a666d40fc3..a40a8df443 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.relation import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.api.session.room.model.message.PollType import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.util.Cancelable @@ -46,13 +47,11 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor: val editedEvent = eventFactory.createTextEvent(roomId, msgType, newBodyText, newBodyAutoMarkdown).copy( eventId = targetEvent.eventId ) - updateFailedEchoWithEvent(roomId, targetEvent.eventId, editedEvent) - return eventSenderProcessor.postEvent(editedEvent, cryptoSessionInfoProvider.isRoomEncrypted(roomId)) + return sendFailedEvent(targetEvent, editedEvent) } else if (targetEvent.root.sendState.isSent()) { val event = eventFactory .createReplaceTextEvent(roomId, targetEvent.eventId, newBodyText, newBodyAutoMarkdown, msgType, compatibilityBodyText) - .also { localEchoRepository.createLocalEcho(it) } - return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId)) + return sendReplaceEvent(roomId, event) } else { // Should we throw? Timber.w("Can't edit a sending event") @@ -60,6 +59,37 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor: } } + fun editPoll(targetEvent: TimelineEvent, + pollType: PollType, + question: String, + options: List): Cancelable { + val roomId = targetEvent.roomId + if (targetEvent.root.sendState.hasFailed()) { + val editedEvent = eventFactory.createPollEvent(roomId, pollType, question, options).copy( + eventId = targetEvent.eventId + ) + return sendFailedEvent(targetEvent, editedEvent) + } else if (targetEvent.root.sendState.isSent()) { + val event = eventFactory + .createPollReplaceEvent(roomId, pollType, targetEvent.eventId, question, options) + return sendReplaceEvent(roomId, event) + } else { + Timber.w("Can't edit a sending event") + return NoOpCancellable + } + } + + private fun sendFailedEvent(targetEvent: TimelineEvent, editedEvent: Event): Cancelable { + val roomId = targetEvent.roomId + updateFailedEchoWithEvent(roomId, targetEvent.eventId, editedEvent) + return eventSenderProcessor.postEvent(editedEvent, cryptoSessionInfoProvider.isRoomEncrypted(roomId)) + } + + private fun sendReplaceEvent(roomId: String, editedEvent: Event): Cancelable { + localEchoRepository.createLocalEcho(editedEvent) + return eventSenderProcessor.postEvent(editedEvent, cryptoSessionInfoProvider.isRoomEncrypted(roomId)) + } + fun editReply(replyToEdit: TimelineEvent, originalTimelineEvent: TimelineEvent, newBodyText: String, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt index fb2fb3950a..5662a72cb8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -37,6 +37,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent +import org.matrix.android.sdk.api.session.room.model.message.PollType import org.matrix.android.sdk.api.session.room.model.message.getFileUrl import org.matrix.android.sdk.api.session.room.send.SendService import org.matrix.android.sdk.api.session.room.send.SendState @@ -103,8 +104,8 @@ internal class DefaultSendService @AssistedInject constructor( .let { sendEvent(it) } } - override fun sendPoll(question: String, options: List): Cancelable { - return localEchoEventFactory.createPollEvent(roomId, question, options) + override fun sendPoll(pollType: PollType, question: String, options: List): Cancelable { + return localEchoEventFactory.createPollEvent(roomId, pollType, question, options) .also { createLocalEcho(it) } .let { sendEvent(it) } } @@ -121,6 +122,12 @@ internal class DefaultSendService @AssistedInject constructor( .let { sendEvent(it) } } + override fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?): Cancelable { + return localEchoEventFactory.createLocationEvent(roomId, latitude, longitude, uncertainty) + .also { createLocalEcho(it) } + .let { sendEvent(it) } + } + override fun redactEvent(event: Event, reason: String?): Cancelable { // TODO manage media/attachements? val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index c4caedc407..1e46602411 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -32,6 +32,9 @@ import org.matrix.android.sdk.api.session.room.model.message.AudioInfo import org.matrix.android.sdk.api.session.room.model.message.AudioWaveformInfo import org.matrix.android.sdk.api.session.room.model.message.FileInfo import org.matrix.android.sdk.api.session.room.model.message.ImageInfo +import org.matrix.android.sdk.api.session.room.model.message.LocationAsset +import org.matrix.android.sdk.api.session.room.model.message.LocationAssetType +import org.matrix.android.sdk.api.session.room.model.message.LocationInfo import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody @@ -39,6 +42,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollConte import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent import org.matrix.android.sdk.api.session.room.model.message.MessageFormat import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent @@ -48,6 +52,7 @@ import org.matrix.android.sdk.api.session.room.model.message.PollAnswer import org.matrix.android.sdk.api.session.room.model.message.PollCreationInfo import org.matrix.android.sdk.api.session.room.model.message.PollQuestion import org.matrix.android.sdk.api.session.room.model.message.PollResponse +import org.matrix.android.sdk.api.session.room.model.message.PollType import org.matrix.android.sdk.api.session.room.model.message.ThumbnailInfo import org.matrix.android.sdk.api.session.room.model.message.VideoInfo import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent @@ -61,6 +66,8 @@ import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.content.ThumbnailExtractor import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory import org.matrix.android.sdk.internal.session.room.send.pills.TextPillsUtils +import java.util.UUID +import java.util.concurrent.TimeUnit import javax.inject.Inject /** @@ -124,6 +131,45 @@ internal class LocalEchoEventFactory @Inject constructor( )) } + private fun createPollContent(question: String, + options: List, + pollType: PollType): MessagePollContent { + return MessagePollContent( + pollCreationInfo = PollCreationInfo( + question = PollQuestion( + question = question + ), + kind = pollType, + answers = options.map { option -> + PollAnswer( + id = UUID.randomUUID().toString(), + answer = option + ) + } + ) + ) + } + + fun createPollReplaceEvent(roomId: String, + pollType: PollType, + targetEventId: String, + question: String, + options: List): Event { + val newContent = MessagePollContent( + relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId), + newContent = createPollContent(question, options, pollType).toContent() + ) + val localId = LocalEcho.createLocalEchoId() + return Event( + roomId = roomId, + originServerTs = dummyOriginServerTs(), + senderId = userId, + eventId = localId, + type = EventType.POLL_START, + content = newContent.toContent() + ) + } + fun createPollReplyEvent(roomId: String, pollEventId: String, answerId: String): Event { @@ -149,21 +195,10 @@ internal class LocalEchoEventFactory @Inject constructor( } fun createPollEvent(roomId: String, + pollType: PollType, question: String, options: List): Event { - val content = MessagePollContent( - pollCreationInfo = PollCreationInfo( - question = PollQuestion( - question = question - ), - answers = options.mapIndexed { index, option -> - PollAnswer( - id = "$index-$option", - answer = option - ) - } - ) - ) + val content = createPollContent(question, options, pollType) val localId = LocalEcho.createLocalEchoId() return Event( roomId = roomId, @@ -194,6 +229,27 @@ internal class LocalEchoEventFactory @Inject constructor( unsignedData = UnsignedData(age = null, transactionId = localId)) } + fun createLocationEvent(roomId: String, + latitude: Double, + longitude: Double, + uncertainty: Double?): Event { + val geoUri = buildGeoUri(latitude, longitude, uncertainty) + val content = MessageLocationContent( + geoUri = geoUri, + body = geoUri, + locationInfo = LocationInfo( + geoUri = geoUri, + description = geoUri + ), + locationAsset = LocationAsset( + type = LocationAssetType.SELF + ), + ts = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()), + text = geoUri + ) + return createMessageEvent(roomId, content) + } + fun createReplaceTextOfReply(roomId: String, eventReplaced: TimelineEvent, originalEvent: TimelineEvent, @@ -480,6 +536,23 @@ internal class LocalEchoEventFactory @Inject constructor( } } + /** + * Returns RFC5870 formatted geo uri 'geo:latitude,longitude;uncertainty' like 'geo:40.05,29.24;30' + * Uncertainty of the location is in meters and not required. + */ + private fun buildGeoUri(latitude: Double, longitude: Double, uncertainty: Double?): String { + return buildString { + append("geo:") + append(latitude) + append(",") + append(longitude) + uncertainty?.let { + append(";") + append(it) + } + } + } + /* * { "content": { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt index 3be01762e7..eb69161614 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt @@ -23,8 +23,8 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.auth.data.SessionParams import org.matrix.android.sdk.api.failure.Failure -import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.getRetryDelay +import org.matrix.android.sdk.api.failure.isLimitExceededError import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.events.model.Event @@ -145,17 +145,17 @@ internal class EventSenderProcessorCoroutine @Inject constructor( task.execute() } catch (exception: Throwable) { when { - exception is IOException || exception is Failure.NetworkConnection -> { + exception is IOException || exception is Failure.NetworkConnection -> { canReachServer.set(false) task.markAsFailedOrRetry(exception, 0) } - (exception is Failure.ServerError && exception.error.code == MatrixError.M_LIMIT_EXCEEDED) -> { + (exception.isLimitExceededError()) -> { task.markAsFailedOrRetry(exception, exception.getRetryDelay(3_000)) } - exception is CancellationException -> { + exception is CancellationException -> { Timber.v("## $task has been cancelled, try next task") } - else -> { + else -> { Timber.v("## un-retryable error for $task, try next task") // this task is in error, check next one? task.onTaskFailed() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorThread.kt index f32890f3fb..1ee3139194 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorThread.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorThread.kt @@ -23,7 +23,7 @@ import org.matrix.android.sdk.api.auth.data.SessionParams import org.matrix.android.sdk.api.auth.data.sessionId import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.failure.Failure -import org.matrix.android.sdk.api.failure.MatrixError +import org.matrix.android.sdk.api.failure.isLimitExceededError import org.matrix.android.sdk.api.failure.isTokenError import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.CryptoService @@ -171,7 +171,7 @@ internal class EventSenderProcessorThread @Inject constructor( break@retryLoop } catch (exception: Throwable) { when { - exception is IOException || exception is Failure.NetworkConnection -> { + exception is IOException || exception is Failure.NetworkConnection -> { canReachServer = false if (task.retryCount.getAndIncrement() >= 3) task.onTaskFailed() while (!canReachServer) { @@ -180,7 +180,7 @@ internal class EventSenderProcessorThread @Inject constructor( waitForNetwork() } } - (exception is Failure.ServerError && exception.error.code == MatrixError.M_LIMIT_EXCEEDED) -> { + (exception.isLimitExceededError()) -> { if (task.retryCount.getAndIncrement() >= 3) task.onTaskFailed() Timber.v("## SendThread retryLoop retryable error for $task reason: ${exception.localizedMessage}") // wait a bit @@ -188,17 +188,17 @@ internal class EventSenderProcessorThread @Inject constructor( sleep(3_000) continue@retryLoop } - exception.isTokenError() -> { + exception.isTokenError() -> { Timber.v("## SendThread retryLoop retryable TOKEN error, interrupt") // we can exit the loop task.onTaskFailed() throw InterruptedException() } - exception is CancellationException -> { + exception is CancellationException -> { Timber.v("## SendThread task has been cancelled") break@retryLoop } - else -> { + else -> { Timber.v("## SendThread retryLoop Un-Retryable error, try next task") // this task is in error, check next one? task.onTaskFailed() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt index 4ec27976a2..417417f439 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt @@ -68,7 +68,7 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private override suspend fun sendStateEvent( eventType: String, - stateKey: String?, + stateKey: String, body: JsonDict ) { val params = SendStateTask.Params( @@ -92,7 +92,7 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private sendStateEvent( eventType = EventType.STATE_ROOM_TOPIC, body = mapOf("topic" to topic), - stateKey = null + stateKey = "" ) } @@ -100,7 +100,7 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private sendStateEvent( eventType = EventType.STATE_ROOM_NAME, body = mapOf("name" to name), - stateKey = null + stateKey = "" ) } @@ -117,7 +117,7 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private // Sort for the cleanup .sorted() ).toContent(), - stateKey = null + stateKey = "" ) } @@ -125,7 +125,7 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private sendStateEvent( eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY, body = mapOf("history_visibility" to readability), - stateKey = null + stateKey = "" ) } @@ -142,14 +142,14 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private sendStateEvent( eventType = EventType.STATE_ROOM_JOIN_RULES, body = body, - stateKey = null + stateKey = "" ) } if (guestAccess != null) { sendStateEvent( eventType = EventType.STATE_ROOM_GUEST_ACCESS, body = mapOf("guest_access" to guestAccess), - stateKey = null + stateKey = "" ) } } @@ -159,7 +159,7 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private sendStateEvent( eventType = EventType.STATE_ROOM_AVATAR, body = mapOf("url" to response.contentUri), - stateKey = null + stateKey = "" ) } @@ -167,7 +167,7 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private sendStateEvent( eventType = EventType.STATE_ROOM_AVATAR, body = emptyMap(), - stateKey = null + stateKey = "" ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SendStateTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SendStateTask.kt index 998e116a0e..56c69a05a6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SendStateTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SendStateTask.kt @@ -26,7 +26,7 @@ import javax.inject.Inject internal interface SendStateTask : Task { data class Params( val roomId: String, - val stateKey: String?, + val stateKey: String, val eventType: String, val body: JsonDict ) @@ -39,7 +39,7 @@ internal class DefaultSendStateTask @Inject constructor( override suspend fun execute(params: SendStateTask.Params) { return executeRequest(globalErrorReceiver) { - if (params.stateKey == null) { + if (params.stateKey.isEmpty()) { roomAPI.sendStateEvent( roomId = params.roomId, stateEventType = params.eventType, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt index 621a08a414..1ee62ad774 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt @@ -16,10 +16,13 @@ package org.matrix.android.sdk.internal.session.sync +import android.os.SystemClock import okhttp3.ResponseBody import org.matrix.android.sdk.api.logger.LoggerTag +import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.initsync.InitSyncStep import org.matrix.android.sdk.api.session.initsync.SyncStatusService +import org.matrix.android.sdk.api.session.statistics.StatisticEvent import org.matrix.android.sdk.api.session.sync.model.LazyRoomSyncEphemeral import org.matrix.android.sdk.api.session.sync.model.SyncResponse import org.matrix.android.sdk.internal.di.SessionFilesDirectory @@ -28,6 +31,8 @@ import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.TimeOutInterceptor import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.toFailure +import org.matrix.android.sdk.internal.session.SessionListeners +import org.matrix.android.sdk.internal.session.dispatchTo import org.matrix.android.sdk.internal.session.filter.FilterRepository import org.matrix.android.sdk.internal.session.homeserver.GetHomeServerCapabilitiesTask import org.matrix.android.sdk.internal.session.initsync.DefaultSyncStatusService @@ -49,7 +54,8 @@ internal interface SyncTask : Task { data class Params( val timeout: Long, - val presence: SyncPresence? + val presence: SyncPresence?, + val afterPause: Boolean ) } @@ -62,6 +68,8 @@ internal class DefaultSyncTask @Inject constructor( private val syncTokenStore: SyncTokenStore, private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask, private val userStore: UserStore, + private val session: Session, + private val sessionListeners: SessionListeners, private val syncTaskSequencer: SyncTaskSequencer, private val globalErrorReceiver: GlobalErrorReceiver, @SessionFilesDirectory @@ -105,6 +113,7 @@ internal class DefaultSyncTask @Inject constructor( val readTimeOut = (params.timeout + TIMEOUT_MARGIN).coerceAtLeast(TimeOutInterceptor.DEFAULT_LONG_TIMEOUT) var syncResponseToReturn: SyncResponse? = null + val syncStatisticsData = SyncStatisticsData(isInitialSync, params.afterPause) if (isInitialSync) { Timber.tag(loggerTag.value).d("INIT_SYNC with filter: ${requestParams["filter"]}") val initSyncStrategy = initialSyncStrategy @@ -112,7 +121,7 @@ internal class DefaultSyncTask @Inject constructor( if (initSyncStrategy is InitialSyncStrategy.Optimized) { roomSyncEphemeralTemporaryStore.reset() workingDir.mkdirs() - val file = downloadInitSyncResponse(requestParams) + val file = downloadInitSyncResponse(requestParams, syncStatisticsData) syncResponseToReturn = reportSubtask(defaultSyncStatusService, InitSyncStep.ImportingAccount, 1, 0.7F) { handleSyncFile(file, initSyncStrategy) } @@ -127,6 +136,9 @@ internal class DefaultSyncTask @Inject constructor( ) } } + // We cannot distinguish request and download in this case. + syncStatisticsData.requestInitSyncTime = SystemClock.elapsedRealtime() + syncStatisticsData.downloadInitSyncTime = syncStatisticsData.requestInitSyncTime logDuration("INIT_SYNC Database insertion", loggerTag) { syncResponseHandler.handleResponse(syncResponse, token, defaultSyncStatusService) } @@ -161,12 +173,15 @@ internal class DefaultSyncTask @Inject constructor( Timber.tag(loggerTag.value).d("Incremental sync done") defaultSyncStatusService.setStatus(SyncStatusService.Status.IncrementalSyncDone) } + syncStatisticsData.treatmentSyncTime = SystemClock.elapsedRealtime() + syncStatisticsData.nbOfRooms = syncResponseToReturn?.rooms?.join?.size ?: 0 + sendStatistics(syncStatisticsData) Timber.tag(loggerTag.value).d("Sync task finished on Thread: ${Thread.currentThread().name}") // Should throw if null as it's a mandatory value. return syncResponseToReturn!! } - private suspend fun downloadInitSyncResponse(requestParams: Map): File { + private suspend fun downloadInitSyncResponse(requestParams: Map, syncStatisticsData: SyncStatisticsData): File { val workingFile = File(workingDir, "initSync.json") val status = initialSyncStatusRepository.getStep() if (workingFile.exists() && status >= InitialSyncStatus.STEP_DOWNLOADED) { @@ -181,7 +196,7 @@ internal class DefaultSyncTask @Inject constructor( getSyncResponse(requestParams, MAX_NUMBER_OF_RETRY_AFTER_TIMEOUT) } } - + syncStatisticsData.requestInitSyncTime = SystemClock.elapsedRealtime() if (syncResponse.isSuccessful) { logDuration("INIT_SYNC Download and save to file", loggerTag) { reportSubtask(defaultSyncStatusService, InitSyncStep.Downloading, 1, 0.1f) { @@ -196,6 +211,7 @@ internal class DefaultSyncTask @Inject constructor( throw syncResponse.toFailure(globalErrorReceiver) .also { Timber.tag(loggerTag.value).w("INIT_SYNC request failure: $this") } } + syncStatisticsData.downloadInitSyncTime = SystemClock.elapsedRealtime() initialSyncStatusRepository.setStep(InitialSyncStatus.STEP_DOWNLOADED) } return workingFile @@ -239,6 +255,45 @@ internal class DefaultSyncTask @Inject constructor( } } + /** + * Aggregator to send stat event. + */ + class SyncStatisticsData( + val isInitSync: Boolean, + val isAfterPause: Boolean + ) { + val startTime = SystemClock.elapsedRealtime() + var requestInitSyncTime = startTime + var downloadInitSyncTime = startTime + var treatmentSyncTime = startTime + var nbOfRooms: Int = 0 + } + + private fun sendStatistics(data: SyncStatisticsData) { + sendStatisticEvent( + if (data.isInitSync) { + (StatisticEvent.InitialSyncRequest( + requestDurationMs = (data.requestInitSyncTime - data.startTime).toInt(), + downloadDurationMs = (data.downloadInitSyncTime - data.requestInitSyncTime).toInt(), + treatmentDurationMs = (data.treatmentSyncTime - data.downloadInitSyncTime).toInt(), + nbOfJoinedRooms = data.nbOfRooms, + )) + } else { + StatisticEvent.SyncTreatment( + durationMs = (data.treatmentSyncTime - data.startTime).toInt(), + afterPause = data.isAfterPause, + nbOfJoinedRooms = data.nbOfRooms + ) + } + ) + } + + private fun sendStatisticEvent(statisticEvent: StatisticEvent) { + session.dispatchTo(sessionListeners) { session, listener -> + listener.onStatisticsEvent(session, statisticEvent) + } + } + companion object { private const val MAX_NUMBER_OF_RETRY_AFTER_TIMEOUT = 50 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt index 24722445be..a0d1ebec4d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt @@ -426,6 +426,10 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle } } } + + // Handle deletion of [stuck] local echos if needed + deleteLocalEchosIfNeeded(insertType, roomEntity, eventList) + // posting new events to timeline if any is registered timelineInput.onNewTimelineEvents(roomId = roomId, eventIds = eventIds) return chunkEntity @@ -478,4 +482,49 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle return result } + + /** + * There are multiple issues like #516 that report stuck local echo events + * at the bottom of each room timeline. + * + * That can happen when a message is SENT but not received back from the /sync. + * Until now we use unsignedData.transactionId to determine whether or not the local + * event should be deleted on every /sync. However, this is partially correct, lets have a look + * at the following scenario: + * + * [There is no Internet connection] --> [10 Messages are sent] --> [The 10 messages are in the queue] --> + * [Internet comes back for 1 second] --> [3 messages are sent] --> [Internet drops again] --> + * [No /sync response is triggered | home server can even replied with /sync but never arrived while we are offline] + * + * So the state until now is that we have 7 pending events to send and 3 sent but not received them back from /sync + * Subsequently, those 3 local messages will not be deleted while there is no transactionId from the /sync + * + * lets continue: + * [Now lets assume that in the same room another user sent 15 events] --> + * [We are finally back online!] --> + * [We will receive the 10 latest events for the room and of course sent the pending 7 messages] --> + * Now /sync response will NOT contain the 3 local messages so our events will stuck in the device. + * + * Someone can say, yes but it will come with the rooms/{roomId}/messages while paginating, + * so the problem will be solved. No that is not the case for two reasons: + * 1. rooms/{roomId}/messages response do not contain the unsignedData.transactionId so we cannot know which event + * to delete + * 2. even if transactionId was there, currently we are not deleting it from the pagination + * + * --------------------------------------------------------------------------------------------- + * While we cannot know when a specific event arrived from the pagination (no transactionId included), after each room /sync + * we clear all SENT events, and we are sure that we will receive it from /sync or pagination + */ + private fun deleteLocalEchosIfNeeded(insertType: EventInsertType, roomEntity: RoomEntity, eventList: List) { + // Skip deletion if we are on initial sync + if (insertType == EventInsertType.INITIAL_SYNC) return + // Skip deletion if there are no timeline events or there is no event received from the current user + if (eventList.firstOrNull { it.senderId == userId } == null) return + roomEntity.sendingTimelineEvents.filter { timelineEvent -> + timelineEvent.root?.sendState == SendState.SENT + }.forEach { + roomEntity.sendingTimelineEvents.remove(it) + it.deleteOnCascade(true) + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt index c17b31b910..0ecf91f6fa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt @@ -152,7 +152,7 @@ abstract class SyncService : Service() { private suspend fun doSync() { Timber.v("## Sync: Execute sync request with timeout $syncTimeoutSeconds seconds") - val params = SyncTask.Params(syncTimeoutSeconds * 1000L, SyncPresence.Offline) + val params = SyncTask.Params(syncTimeoutSeconds * 1000L, SyncPresence.Offline, afterPause = false) try { // never do that in foreground, let the syncThread work syncTask.execute(params) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt index b6ea7a68f7..2460720adc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt @@ -173,13 +173,14 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, if (state !is SyncState.Running) { updateStateTo(SyncState.Running(afterPause = true)) } + val afterPause = state.let { it is SyncState.Running && it.afterPause } val timeout = when { - previousSyncResponseHasToDevice -> 0L /* Force timeout to 0 */ - state.let { it is SyncState.Running && it.afterPause } -> 0L /* No timeout after a pause */ - else -> DEFAULT_LONG_POOL_TIMEOUT + previousSyncResponseHasToDevice -> 0L /* Force timeout to 0 */ + afterPause -> 0L /* No timeout after a pause */ + else -> DEFAULT_LONG_POOL_TIMEOUT } Timber.tag(loggerTag.value).d("Execute sync request with timeout $timeout") - val params = SyncTask.Params(timeout, SyncPresence.Online) + val params = SyncTask.Params(timeout, SyncPresence.Online, afterPause = afterPause) val sync = syncScope.launch { previousSyncResponseHasToDevice = doSync(params) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt index 2f1241f4d8..423a4e553f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt @@ -113,7 +113,7 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, * Will return true if the sync response contains some toDevice events. */ private suspend fun doSync(timeout: Long): Boolean { - val taskParams = SyncTask.Params(timeout * 1000, SyncPresence.Offline) + val taskParams = SyncTask.Params(timeout * 1000, SyncPresence.Offline, afterPause = false) val syncResponse = syncTask.execute(taskParams) return syncResponse.toDevice?.events?.isNotEmpty().orFalse() } @@ -151,6 +151,7 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, sessionId = sessionId, timeout = serverTimeoutInSeconds, delay = delayInSeconds, + periodic = true, forceImmediate = forceImmediate ) ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt index 313fb6319d..6205e3e4b1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt @@ -64,7 +64,7 @@ internal class DefaultTermsService @Inject constructor( */ override suspend fun getHomeserverTerms(baseUrl: String): TermsResponse { return try { - val request = baseUrl + NetworkConstants.URI_API_PREFIX_PATH_R0 + "register" + val request = baseUrl.ensureTrailingSlash() + NetworkConstants.URI_API_PREFIX_PATH_R0 + "register" executeRequest(null) { termsAPI.register(request) } diff --git a/settings.gradle b/settings.gradle index 7ba66c7cb1..d3b217c517 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,9 +1,9 @@ include ':vector' include ':matrix-sdk-android' -include ':matrix-sdk-android-rx' include ':diff-match-patch' include ':attachment-viewer' include ':multipicker' +include ':library:core-utils' include ':library:ui-styles' include ':library:jsonviewer' include ':matrix-sdk-android-flow' diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index 293e0b2a58..21ab0bab77 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -159,9 +159,6 @@ Formatter\.formatShortFileSize===1 # DISABLED # android\.text\.TextUtils -### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt -enum class===119 - ### Do not import temporary legacy classes import org.matrix.android.sdk.internal.legacy.riot===3 import org.matrix.androidsdk.crypto.data===2 diff --git a/tools/release/pushPlayStoreMetaData.sh b/tools/release/pushPlayStoreMetaData.sh index 276ea32fce..2d8fd9b36a 100755 --- a/tools/release/pushPlayStoreMetaData.sh +++ b/tools/release/pushPlayStoreMetaData.sh @@ -32,6 +32,15 @@ mv ./fastlane/metadata/android/nb ./fastlane_tmp # Fastlane / PlayStore require longDescription and shortDescription file to be set, so copy the default # one for languages where they are missing echo "Copying default description when missing" +if [[ -f "./fastlane/metadata/android/nl-NL/full_description.txt" ]]; then + echo "It appears that file ./fastlane/metadata/android/nl-NL/full_description.txt now exists. This can be removed." + removeFullDes_nl=0 +else + echo "Copy default full description to ./fastlane/metadata/android/nl-NL" + cp ./fastlane/metadata/android/en-US/full_description.txt ./fastlane/metadata/android/nl-NL + removeFullDes_nl=1 +fi + if [[ -f "./fastlane/metadata/android/ro/full_description.txt" ]]; then echo "It appears that file ./fastlane/metadata/android/ro/full_description.txt now exists. This can be removed." removeFullDes_ro=0 @@ -78,6 +87,10 @@ mv ./fastlane_tmp/* ./fastlane/metadata/android/ # Delete the tmp folder (should be empty) rmdir ./fastlane_tmp +if [[ ${removeFullDes_nl} -eq 1 ]]; then + rm ./fastlane/metadata/android/nl-NL/full_description.txt +fi + if [[ ${removeFullDes_ro} -eq 1 ]]; then rm ./fastlane/metadata/android/ro/full_description.txt fi diff --git a/towncrier.toml b/towncrier.toml index 486ef6f186..e4d569faa7 100644 --- a/towncrier.toml +++ b/towncrier.toml @@ -15,13 +15,18 @@ name = "Bugfixes 🐛" showcontent = true + [[tool.towncrier.type]] + directory = "wip" + name = "In development 🚧" + showcontent = true + [[tool.towncrier.type]] directory = "doc" name = "Improved Documentation 📚" showcontent = true [[tool.towncrier.type]] - directory = "removal" + directory = "sdk" name = "SDK API changes ⚠️" showcontent = true diff --git a/vector/build.gradle b/vector/build.gradle index 695c205002..0151950c84 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -18,7 +18,7 @@ ext.versionMinor = 3 // 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 = 15 +ext.versionPatch = 16 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' @@ -153,6 +153,9 @@ android { // 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 @@ -205,9 +208,8 @@ android { animationsDisabled = true // Comment to run on Android 12 - execution 'ANDROIDX_TEST_ORCHESTRATOR' +// execution 'ANDROIDX_TEST_ORCHESTRATOR' } - signingConfigs { debug { keyAlias 'androiddebugkey' @@ -215,6 +217,12 @@ android { storeFile file('./signature/debug.keystore') storePassword 'android' } + release { + keyAlias project.property("signing.element.keyId") + keyPassword project.property("signing.element.keyPassword") + storeFile file(project.property("signing.element.storePath")) + storePassword project.property("signing.element.storePassword") + } } buildTypes { @@ -245,6 +253,7 @@ android { optimizeCode true proguardFiles 'proguard-rules.pro' } + signingConfig signingConfigs.release } } @@ -328,6 +337,7 @@ dependencies { implementation project(":attachment-viewer") implementation project(":library:jsonviewer") implementation project(":library:ui-styles") + implementation project(":library:core-utils") implementation 'androidx.multidex:multidex:2.0.1' implementation libs.jetbrains.coroutinesCore @@ -363,7 +373,7 @@ dependencies { implementation 'com.facebook.stetho:stetho:1.6.0' // Phone number https://github.com/google/libphonenumber - implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.40' + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.41' // FlowBinding implementation libs.github.flowBinding @@ -491,6 +501,10 @@ dependencies { } implementation 'commons-codec:commons-codec:1.15' + // MapTiler + implementation 'org.maplibre.gl:android-sdk:9.5.2' + implementation 'org.maplibre.gl:android-plugin-annotation-v9:1.0.0' + // TESTS testImplementation libs.tests.junit diff --git a/vector/src/androidTest/java/im/vector/app/SecurityBootstrapTest.kt b/vector/src/androidTest/java/im/vector/app/SecurityBootstrapTest.kt index 0d0ec3dd2b..fb7b9dcb41 100644 --- a/vector/src/androidTest/java/im/vector/app/SecurityBootstrapTest.kt +++ b/vector/src/androidTest/java/im/vector/app/SecurityBootstrapTest.kt @@ -154,8 +154,6 @@ class SecurityBootstrapTest : VerificationTestBase() { onView(withId(R.id.recoveryCopy)) .perform(click()) - Thread.sleep(1000) - // Dismiss dialog onView(withText(R.string.ok)).inRoot(RootMatchers.isDialog()).perform(click()) diff --git a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt index 33e1e6f6b4..042e9ef3ee 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt @@ -55,6 +55,10 @@ class UiAllScreensSanityTest { fun allScreensTest() { IdlingPolicies.setMasterPolicyTimeout(120, TimeUnit.SECONDS) + elementRobot.onboarding { + crawl() + } + // Create an account val userId = "UiTest_" + UUID.randomUUID().toString() elementRobot.signUp(userId) diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt index 22a5a0790b..d7e99c63dd 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt @@ -40,6 +40,10 @@ import timber.log.Timber class ElementRobot { + fun onboarding(block: OnboardingRobot.() -> Unit) { + block(OnboardingRobot()) + } + fun signUp(userId: String) { val onboardingRobot = OnboardingRobot() onboardingRobot.createAccount(userId = userId) @@ -124,7 +128,7 @@ class ElementRobot { } waitUntilActivityVisible { - assertDisplayed(R.id.loginSplashLogo) + assertDisplayed(R.id.loginSplashSubmit) } } diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingRobot.kt index 633d3cceab..47bf31355c 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingRobot.kt @@ -18,6 +18,7 @@ package im.vector.app.ui.robot import androidx.test.espresso.Espresso.closeSoftKeyboard import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.Espresso.pressBack import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.espresso.matcher.ViewMatchers.withId import com.adevinta.android.barista.assertion.BaristaEnabledAssertions.assertDisabled @@ -31,6 +32,24 @@ import im.vector.app.waitForView class OnboardingRobot { + fun crawl() { + waitUntilViewVisible(withId(R.id.loginSplashSubmit)) + crawlGetStarted() + crawlAlreadyHaveAccount() + } + + private fun crawlGetStarted() { + clickOn(R.id.loginSplashSubmit) + OnboardingServersRobot().crawlSignUp() + pressBack() + } + + private fun crawlAlreadyHaveAccount() { + clickOn(R.id.loginSplashAlreadyHaveAccount) + OnboardingServersRobot().crawlSignIn() + pressBack() + } + fun createAccount(userId: String, password: String = "password", homeServerUrl: String = "http://10.0.2.2:8080") { initSession(true, userId, password, homeServerUrl) } @@ -44,7 +63,7 @@ class OnboardingRobot { password: String, homeServerUrl: String) { waitUntilViewVisible(withId(R.id.loginSplashSubmit)) - assertDisplayed(R.id.loginSplashSubmit, R.string.login_splash_submit) + assertDisplayed(R.id.loginSplashSubmit, R.string.login_splash_create_account) if (createAccount) { clickOn(R.id.loginSplashSubmit) } else { diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingServersRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingServersRobot.kt new file mode 100644 index 0000000000..1625b4580d --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingServersRobot.kt @@ -0,0 +1,97 @@ +/* + * 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.ui.robot + +import androidx.test.espresso.Espresso +import androidx.test.espresso.matcher.ViewMatchers +import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions +import com.adevinta.android.barista.interaction.BaristaClickInteractions +import com.adevinta.android.barista.interaction.BaristaEditTextInteractions +import im.vector.app.R +import im.vector.app.espresso.tools.waitUntilViewVisible + +class OnboardingServersRobot { + + fun crawlSignUp() { + BaristaVisibilityAssertions.assertDisplayed(R.id.loginServerTitle, R.string.login_server_title) + crawlMatrixServer(isSignUp = true) + crawlEmsServer() + crawlOtherServer(isSignUp = true) + crawlSignInWithMatrixId() + } + + fun crawlSignIn() { + BaristaVisibilityAssertions.assertDisplayed(R.id.loginServerTitle, R.string.login_server_title) + crawlMatrixServer(isSignUp = false) + crawlEmsServer() + crawlOtherServer(isSignUp = false) + crawlSignInWithMatrixId() + } + + private fun crawlOtherServer(isSignUp: Boolean) { + BaristaClickInteractions.clickOn(R.id.loginServerChoiceOther) + waitUntilViewVisible(ViewMatchers.withId(R.id.loginServerUrlFormTitle)) + BaristaEditTextInteractions.writeTo(R.id.loginServerUrlFormHomeServerUrl, "https://chat.mozilla.org") + BaristaClickInteractions.clickOn(R.id.loginServerUrlFormSubmit) + waitUntilViewVisible(ViewMatchers.withId(R.id.loginSignupSigninTitle)) + BaristaVisibilityAssertions.assertDisplayed(R.id.loginSignupSigninText, "Connect to chat.mozilla.org") + BaristaVisibilityAssertions.assertDisplayed(R.id.loginSignupSigninSubmit, R.string.login_signin_sso) + Espresso.pressBack() + + BaristaEditTextInteractions.writeTo(R.id.loginServerUrlFormHomeServerUrl, "https://matrix.org") + BaristaClickInteractions.clickOn(R.id.loginServerUrlFormSubmit) + assetMatrixSignInOptions(isSignUp) + Espresso.pressBack() + Espresso.pressBack() + } + + private fun crawlEmsServer() { + BaristaClickInteractions.clickOn(R.id.loginServerChoiceEms) + waitUntilViewVisible(ViewMatchers.withId(R.id.loginServerUrlFormTitle)) + BaristaVisibilityAssertions.assertDisplayed(R.id.loginServerUrlFormTitle, R.string.login_connect_to_modular) + + BaristaEditTextInteractions.writeTo(R.id.loginServerUrlFormHomeServerUrl, "https://one.ems.host") + BaristaClickInteractions.clickOn(R.id.loginServerUrlFormSubmit) + + waitUntilViewVisible(ViewMatchers.withId(R.id.loginSignupSigninTitle)) + BaristaVisibilityAssertions.assertDisplayed(R.id.loginSignupSigninText, "one.ems.host") + BaristaVisibilityAssertions.assertDisplayed(R.id.loginSignupSigninSubmit, R.string.login_signin_sso) + Espresso.pressBack() + Espresso.pressBack() + } + + private fun crawlMatrixServer(isSignUp: Boolean) { + BaristaClickInteractions.clickOn(R.id.loginServerChoiceMatrixOrg) + assetMatrixSignInOptions(isSignUp) + Espresso.pressBack() + } + + private fun assetMatrixSignInOptions(isSignUp: Boolean) { + waitUntilViewVisible(ViewMatchers.withId(R.id.loginTitle)) + when (isSignUp) { + true -> BaristaVisibilityAssertions.assertDisplayed(R.id.loginTitle, "Sign up to matrix.org") + false -> BaristaVisibilityAssertions.assertDisplayed(R.id.loginTitle, "Connect to matrix.org") + } + } + + private fun crawlSignInWithMatrixId() { + BaristaClickInteractions.clickOn(R.id.loginServerIKnowMyIdSubmit) + waitUntilViewVisible(ViewMatchers.withId(R.id.loginTitle)) + BaristaVisibilityAssertions.assertDisplayed(R.id.loginTitle, R.string.login_signin_matrix_id_title) + Espresso.pressBack() + } +} diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt index 53d6c16bb7..ebf5fdf23d 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt @@ -37,7 +37,6 @@ import im.vector.app.features.home.room.detail.timeline.action.MessageActionsBot import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet import im.vector.app.features.reactions.data.EmojiDataSource import im.vector.app.interactWithSheet -import im.vector.app.waitForView import im.vector.app.withRetry import java.lang.Thread.sleep @@ -127,7 +126,7 @@ class RoomDetailRobot { fun openSettings(block: RoomSettingsRobot.() -> Unit) { clickMenu(R.id.timeline_setting) - waitForView(withId(R.id.roomProfileAvatarView)) + waitUntilViewVisible(withId(R.id.roomProfileAvatarView)) sleep(1000) block(RoomSettingsRobot()) pressBack() diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt index 6ddbb53134..fb803162a7 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt @@ -36,13 +36,18 @@ class DebugFeaturesStateFactory @Inject constructor( ), createBooleanFeature( label = "FTUE Splash - I already have an account", - factory = VectorFeatures::isAlreadyHaveAccountSplashEnabled, - key = DebugFeatureKeys.alreadyHaveAnAccount + key = DebugFeatureKeys.onboardingAlreadyHaveAnAccount, + factory = VectorFeatures::isOnboardingAlreadyHaveAccountSplashEnabled ), createBooleanFeature( - label = "FTUE Splash - Carousel", - factory = VectorFeatures::isSplashCarouselEnabled, - key = DebugFeatureKeys.splashCarousel + label = "FTUE Splash - carousel", + key = DebugFeatureKeys.onboardingSplashCarousel, + factory = VectorFeatures::isOnboardingSplashCarouselEnabled + ), + createBooleanFeature( + label = "FTUE Use Case", + key = DebugFeatureKeys.onboardingUseCase, + factory = VectorFeatures::isOnboardingUseCaseEnabled ) )) } diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt index 2e11017ef3..6ca33ca968 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt @@ -43,10 +43,13 @@ class DebugVectorFeatures( return readPreferences().getEnum() ?: vectorFeatures.onboardingVariant() } - override fun isAlreadyHaveAccountSplashEnabled(): Boolean = read(DebugFeatureKeys.alreadyHaveAnAccount) - ?: vectorFeatures.isAlreadyHaveAccountSplashEnabled() + override fun isOnboardingAlreadyHaveAccountSplashEnabled(): Boolean = read(DebugFeatureKeys.onboardingAlreadyHaveAnAccount) + ?: vectorFeatures.isOnboardingAlreadyHaveAccountSplashEnabled() - override fun isSplashCarouselEnabled(): Boolean = read(DebugFeatureKeys.splashCarousel) ?: vectorFeatures.isSplashCarouselEnabled() + override fun isOnboardingSplashCarouselEnabled(): Boolean = read(DebugFeatureKeys.onboardingSplashCarousel) + ?: vectorFeatures.isOnboardingSplashCarouselEnabled() + + override fun isOnboardingUseCaseEnabled(): Boolean = read(DebugFeatureKeys.onboardingUseCase) ?: vectorFeatures.isOnboardingUseCaseEnabled() fun override(value: T?, key: Preferences.Key) = updatePreferences { if (value == null) { @@ -96,6 +99,7 @@ private inline fun > enumPreferencesKey() = enumPreferencesK private fun > enumPreferencesKey(type: KClass) = stringPreferencesKey("enum-${type.simpleName}") object DebugFeatureKeys { - val alreadyHaveAnAccount = booleanPreferencesKey("already-have-an-account") - val splashCarousel = booleanPreferencesKey("splash-carousel") + val onboardingAlreadyHaveAnAccount = booleanPreferencesKey("onboarding-already-have-an-account") + val onboardingSplashCarousel = booleanPreferencesKey("onboarding-splash-carousel") + val onboardingUseCase = booleanPreferencesKey("onbboarding-splash-carousel") } diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 14796f9d2e..fdec5337ba 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -42,6 +42,10 @@ android:name="android.permission.WRITE_CALENDAR" tools:node="remove" /> + + + + @@ -76,6 +80,7 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:networkSecurityConfig="@xml/network_security_config" + android:resizeableActivity="true" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.Vector.Light" @@ -332,6 +337,7 @@ + @@ -400,7 +406,8 @@ android:value="androidx.startup" tools:node="remove" /> - diff --git a/vector/src/main/assets/open_source_licenses.html b/vector/src/main/assets/open_source_licenses.html index 0eefa3b863..2c25606f57 100755 --- a/vector/src/main/assets/open_source_licenses.html +++ b/vector/src/main/assets/open_source_licenses.html @@ -254,6 +254,33 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +
    +
  • + org.maplibre.gl:android-sdk +
    + org.maplibre.gl:android-plugin-annotation-v9 +
    + BSD 2-Clause License + + Copyright (c) 2021 MapLibre contributors + + Copyright (c) 2018-2021 MapTiler.com + + Copyright (c) 2014-2020 Mapbox +
  • +
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
  • textdrawable diff --git a/vector/src/main/java/im/vector/app/AppStateHandler.kt b/vector/src/main/java/im/vector/app/AppStateHandler.kt index 9ed9dd5b23..1ff3d97576 100644 --- a/vector/src/main/java/im/vector/app/AppStateHandler.kt +++ b/vector/src/main/java/im/vector/app/AppStateHandler.kt @@ -61,7 +61,7 @@ class AppStateHandler @Inject constructor( private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) private val selectedSpaceDataSource = BehaviorDataSource>(Option.empty()) - val selectedRoomGroupingObservable = selectedSpaceDataSource.stream() + val selectedRoomGroupingFlow = selectedSpaceDataSource.stream() fun getCurrentRoomGroupingMethod(): RoomGroupingMethod? { // XXX we should somehow make it live :/ just a work around @@ -78,11 +78,17 @@ class AppStateHandler @Inject constructor( } } - fun setCurrentSpace(spaceId: String?, session: Session? = null) { + fun setCurrentSpace(spaceId: String?, session: Session? = null, persistNow: Boolean = false) { val uSession = session ?: activeSessionHolder.getSafeActiveSession() ?: return if (selectedSpaceDataSource.currentValue?.orNull() is RoomGroupingMethod.BySpace && spaceId == selectedSpaceDataSource.currentValue?.orNull()?.space()?.roomId) return val spaceSum = spaceId?.let { uSession.getRoomSummary(spaceId) } + + if (persistNow) { + uiStateRepository.storeGroupingMethod(true, uSession.sessionId) + uiStateRepository.storeSelectedSpace(spaceSum?.roomId, uSession.sessionId) + } + selectedSpaceDataSource.post(Option.just(RoomGroupingMethod.BySpace(spaceSum))) if (spaceId != null) { uSession.coroutineScope.launch(Dispatchers.IO) { diff --git a/vector/src/main/java/im/vector/app/AutoRageShaker.kt b/vector/src/main/java/im/vector/app/AutoRageShaker.kt index ca91f728cb..0238931e4c 100644 --- a/vector/src/main/java/im/vector/app/AutoRageShaker.kt +++ b/vector/src/main/java/im/vector/app/AutoRageShaker.kt @@ -67,6 +67,7 @@ class AutoRageShaker @Inject constructor( fun initialize() { observeActiveSession() + enable(vectorPreferences.labsAutoReportUISI()) // It's a singleton... vectorPreferences.subscribeToChanges(this) @@ -141,17 +142,19 @@ class AutoRageShaker @Inject constructor( withCrashLogs = true, withKeyRequestHistory = true, withScreenshot = false, - theBugDescription = "UISI detected", + theBugDescription = "Auto-reporting decryption error", serverVersion = "", canContact = false, - customFields = mapOf("auto-uisi" to buildString { - append("\neventId: ${target.eventId}") - append("\nroomId: ${target.roomId}") - append("\nsenderKey: ${target.senderKey}") - append("\nsource: ${target.source}") - append("\ndeviceId: ${target.senderDeviceId}") - append("\nuserId: ${target.senderUserId}") - append("\nsessionId: ${target.sessionId}") + customFields = mapOf("auto_uisi" to buildString { + append("{") + append("\"event_id\": \"${target.eventId}\",") + append("\"room_id\": \"${target.roomId}\",") + append("\"sender_key\": \"${target.senderKey}\",") + append("\"device_id\": \"${target.senderDeviceId}\",") + append("\"source\": \"${target.source}\",") + append("\"user_id\": \"${target.senderUserId}\",") + append("\"session_id\": \"${target.sessionId}\"") + append("}") }), listener = object : BugReporter.IMXBugReportListener { override fun onUploadCancelled() { @@ -221,17 +224,19 @@ class AutoRageShaker @Inject constructor( withCrashLogs = true, withKeyRequestHistory = true, withScreenshot = false, - theBugDescription = "UISI detected $matchingIssue", + theBugDescription = "Auto-reporting decryption error \nRecipient rageshake: $matchingIssue", serverVersion = "", canContact = false, customFields = mapOf( - "auto-uisi" to buildString { - append("\neventId: $eventId") - append("\nroomId: $roomId") - append("\nsenderKey: $senderKey") - append("\ndeviceId: $deviceId") - append("\nuserId: $userId") - append("\nsessionId: $sessionId") + "auto_uisi" to buildString { + append("{") + append("\"event_id\": \"$eventId\",") + append("\"room_id\": \"$roomId\",") + append("\"sender_key\": \"$senderKey\",") + append("\"device_id\": \"$deviceId\",") + append("\"user_id\": \"$userId\",") + append("\"session_id\": \"$sessionId\"") + append("}") }, "recipient_rageshake" to matchingIssue ), diff --git a/vector/src/main/java/im/vector/app/VectorApplication.kt b/vector/src/main/java/im/vector/app/VectorApplication.kt index 05d20662c7..d252b5d9bd 100644 --- a/vector/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector/src/main/java/im/vector/app/VectorApplication.kt @@ -36,6 +36,7 @@ import com.airbnb.epoxy.EpoxyController import com.airbnb.mvrx.Mavericks import com.facebook.stetho.Stetho import com.gabrielittner.threetenbp.LazyThreeTen +import com.mapbox.mapboxsdk.Mapbox import com.vanniktech.emoji.EmojiManager import com.vanniktech.emoji.google.GoogleEmojiProvider import dagger.hilt.android.HiltAndroidApp @@ -197,6 +198,9 @@ class VectorApplication : }) EmojiManager.install(GoogleEmojiProvider()) + + // Initialize Mapbox before inflating mapViews + Mapbox.getInstance(this) } private val startSyncOnFirstStart = object : DefaultLifecycleObserver { diff --git a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt index 5d8d5db3fe..4883676f87 100644 --- a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt +++ b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt @@ -31,7 +31,7 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class ActiveSessionHolder @Inject constructor(private val sessionObservableStore: ActiveSessionDataSource, +class ActiveSessionHolder @Inject constructor(private val activeSessionDataSource: ActiveSessionDataSource, private val keyRequestHandler: KeyRequestHandler, private val incomingVerificationRequestHandler: IncomingVerificationRequestHandler, private val callManager: WebRtcCallManager, @@ -46,7 +46,7 @@ class ActiveSessionHolder @Inject constructor(private val sessionObservableStore fun setActiveSession(session: Session) { Timber.w("setActiveSession of ${session.myUserId}") activeSession.set(session) - sessionObservableStore.post(Option.just(session)) + activeSessionDataSource.post(Option.just(session)) keyRequestHandler.start(session) incomingVerificationRequestHandler.start(session) @@ -66,7 +66,7 @@ class ActiveSessionHolder @Inject constructor(private val sessionObservableStore } activeSession.set(null) - sessionObservableStore.post(Option.empty()) + activeSessionDataSource.post(Option.empty()) keyRequestHandler.stop() incomingVerificationRequestHandler.stop() diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index c27309fad6..5d27909b25 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -61,6 +61,8 @@ import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsFragment import im.vector.app.features.home.room.detail.RoomDetailFragment import im.vector.app.features.home.room.detail.search.SearchFragment import im.vector.app.features.home.room.list.RoomListFragment +import im.vector.app.features.location.LocationPreviewFragment +import im.vector.app.features.location.LocationSharingFragment import im.vector.app.features.login.LoginCaptchaFragment import im.vector.app.features.login.LoginFragment import im.vector.app.features.login.LoginGenericTextInputFormFragment @@ -104,6 +106,7 @@ import im.vector.app.features.onboarding.ftueauth.FtueAuthServerSelectionFragmen import im.vector.app.features.onboarding.ftueauth.FtueAuthSignUpSignInSelectionFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthSplashCarouselFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthSplashFragment +import im.vector.app.features.onboarding.ftueauth.FtueAuthUseCaseFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthWaitForEmailFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthWebFragment import im.vector.app.features.onboarding.ftueauth.terms.FtueAuthTermsFragment @@ -449,6 +452,11 @@ interface FragmentModule { @FragmentKey(FtueAuthSplashCarouselFragment::class) fun bindFtueAuthSplashCarouselFragment(fragment: FtueAuthSplashCarouselFragment): Fragment + @Binds + @IntoMap + @FragmentKey(FtueAuthUseCaseFragment::class) + fun bindFtueAuthUseCaseFragment(fragment: FtueAuthUseCaseFragment): Fragment + @Binds @IntoMap @FragmentKey(FtueAuthWaitForEmailFragment::class) @@ -933,4 +941,14 @@ interface FragmentModule { @IntoMap @FragmentKey(CreatePollFragment::class) fun bindCreatePollFragment(fragment: CreatePollFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(LocationSharingFragment::class) + fun bindLocationSharingFragment(fragment: LocationSharingFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(LocationPreviewFragment::class) + fun bindLocationPreviewFragment(fragment: LocationPreviewFragment): Fragment } diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt index cc31a7dca6..9ad01cd3e4 100644 --- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt @@ -54,6 +54,7 @@ import im.vector.app.features.home.room.detail.upgrade.MigrateRoomViewModel import im.vector.app.features.home.room.list.RoomListViewModel 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.login.LoginViewModel import im.vector.app.features.login2.LoginViewModel2 import im.vector.app.features.login2.created.AccountCreatedViewModel @@ -588,4 +589,9 @@ interface MavericksViewModelModule { @IntoMap @MavericksViewModelKey(CreatePollViewModel::class) fun createPollViewModelFactory(factory: CreatePollViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(LocationSharingViewModel::class) + fun createLocationSharingViewModelFactory(factory: LocationSharingViewModel.Factory): MavericksAssistedViewModelFactory<*, *> } diff --git a/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt b/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt index 0b9855ef56..283437c679 100644 --- a/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt +++ b/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt @@ -21,7 +21,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import im.vector.app.core.dialogs.UnrecognizedCertificateDialog import im.vector.app.core.error.ErrorFormatter -import im.vector.app.features.analytics.VectorAnalytics +import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.navigation.Navigator @@ -56,7 +56,7 @@ interface SingletonEntryPoint { fun pinLocker(): PinLocker - fun analytics(): VectorAnalytics + fun analyticsTracker(): AnalyticsTracker fun webRtcCallManager(): WebRtcCallManager diff --git a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt index d83bb5cb57..0e19cd4388 100644 --- a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt @@ -33,6 +33,7 @@ import im.vector.app.core.error.DefaultErrorFormatter import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.time.Clock import im.vector.app.core.time.DefaultClock +import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.analytics.impl.DefaultVectorAnalytics import im.vector.app.features.invite.AutoAcceptInvites @@ -64,6 +65,9 @@ abstract class VectorBindModule { @Binds abstract fun bindVectorAnalytics(analytics: DefaultVectorAnalytics): VectorAnalytics + @Binds + abstract fun bindAnalyticsTracker(analytics: DefaultVectorAnalytics): AnalyticsTracker + @Binds abstract fun bindErrorFormatter(formatter: DefaultErrorFormatter): ErrorFormatter diff --git a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt index 35db585b79..cdecd2d6c6 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt @@ -26,14 +26,17 @@ import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider import im.vector.app.features.home.room.detail.timeline.item.BindingOptions import im.vector.app.features.home.room.detail.timeline.tools.findPillsAndProcess +import im.vector.app.features.location.LocationData +import im.vector.app.features.location.MapTilerMapView import im.vector.app.features.media.ImageContentRenderer +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence import org.matrix.android.sdk.api.util.MatrixItem /** @@ -66,6 +69,12 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel + holder.mapView.initialize { + if (holder.view.isAttachedToWindow) { + holder.mapView.zoomToLocation(location.latitude, location.longitude, 15.0) + locationPinProvider?.create(matrixItem.id) { pinDrawable -> + holder.mapView.addPinToMap(matrixItem.id, pinDrawable) + holder.mapView.updatePinLocation(matrixItem.id, location.latitude, location.longitude) + } + } + } + } } override fun unbind(holder: Holder) { @@ -101,5 +124,6 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel(R.id.bottom_sheet_message_preview_body_details) val timestamp by bind(R.id.bottom_sheet_message_preview_timestamp) val imagePreview by bind(R.id.bottom_sheet_message_preview_image) + val mapView by bind(R.id.bottom_sheet_message_preview_location) } } diff --git a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt index 8640fa6f05..2eb36d758e 100644 --- a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt @@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.MatrixIdFailure import org.matrix.android.sdk.api.failure.isInvalidPassword +import org.matrix.android.sdk.api.failure.isLimitExceededError import org.matrix.android.sdk.api.session.identity.IdentityServiceError import java.net.HttpURLConnection import java.net.SocketTimeoutException @@ -58,53 +59,53 @@ class DefaultErrorFormatter @Inject constructor( } is Failure.ServerError -> { when { - throwable.error.code == MatrixError.M_CONSENT_NOT_GIVEN -> { + throwable.error.code == MatrixError.M_CONSENT_NOT_GIVEN -> { // Special case for terms and conditions stringProvider.getString(R.string.error_terms_not_accepted) } - throwable.isInvalidPassword() -> { + throwable.isInvalidPassword() -> { stringProvider.getString(R.string.auth_invalid_login_param) } - throwable.error.code == MatrixError.M_USER_IN_USE -> { + throwable.error.code == MatrixError.M_USER_IN_USE -> { stringProvider.getString(R.string.login_signup_error_user_in_use) } - throwable.error.code == MatrixError.M_BAD_JSON -> { + throwable.error.code == MatrixError.M_BAD_JSON -> { stringProvider.getString(R.string.login_error_bad_json) } - throwable.error.code == MatrixError.M_NOT_JSON -> { + throwable.error.code == MatrixError.M_NOT_JSON -> { stringProvider.getString(R.string.login_error_not_json) } - throwable.error.code == MatrixError.M_THREEPID_DENIED -> { + throwable.error.code == MatrixError.M_THREEPID_DENIED -> { stringProvider.getString(R.string.login_error_threepid_denied) } - throwable.error.code == MatrixError.M_LIMIT_EXCEEDED -> { + throwable.isLimitExceededError() -> { limitExceededError(throwable.error) } - throwable.error.code == MatrixError.M_TOO_LARGE -> { + throwable.error.code == MatrixError.M_TOO_LARGE -> { stringProvider.getString(R.string.error_file_too_big_simple) } - throwable.error.code == MatrixError.M_THREEPID_NOT_FOUND -> { + throwable.error.code == MatrixError.M_THREEPID_NOT_FOUND -> { stringProvider.getString(R.string.login_reset_password_error_not_found) } - throwable.error.code == MatrixError.M_USER_DEACTIVATED -> { + throwable.error.code == MatrixError.M_USER_DEACTIVATED -> { stringProvider.getString(R.string.auth_invalid_login_deactivated_account) } throwable.error.code == MatrixError.M_THREEPID_IN_USE && - throwable.error.message == "Email is already in use" -> { + throwable.error.message == "Email is already in use" -> { stringProvider.getString(R.string.account_email_already_used_error) } throwable.error.code == MatrixError.M_THREEPID_IN_USE && - throwable.error.message == "MSISDN is already in use" -> { + throwable.error.message == "MSISDN is already in use" -> { stringProvider.getString(R.string.account_phone_number_already_used_error) } - throwable.error.code == MatrixError.M_THREEPID_AUTH_FAILED -> { + throwable.error.code == MatrixError.M_THREEPID_AUTH_FAILED -> { stringProvider.getString(R.string.error_threepid_auth_failed) } throwable.error.code == MatrixError.M_UNKNOWN && throwable.error.message == "Not allowed to join this room" -> { stringProvider.getString(R.string.room_error_access_unauthorized) } - else -> { + else -> { throwable.error.message.takeIf { it.isNotEmpty() } ?: throwable.error.code.takeIf { it.isNotEmpty() } } diff --git a/vector/src/main/java/im/vector/app/core/extensions/Context.kt b/vector/src/main/java/im/vector/app/core/extensions/Context.kt index 59847da7c9..1063d30a41 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/Context.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/Context.kt @@ -17,9 +17,36 @@ package im.vector.app.core.extensions import android.content.Context +import android.graphics.drawable.Drawable +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.annotation.FloatRange +import androidx.core.content.ContextCompat import dagger.hilt.EntryPoints import im.vector.app.core.di.SingletonEntryPoint +import kotlin.math.roundToInt fun Context.singletonEntryPoint(): SingletonEntryPoint { return EntryPoints.get(applicationContext, SingletonEntryPoint::class.java) } + +fun Context.getResTintedDrawable(@DrawableRes drawableRes: Int, @ColorRes tint: Int, @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1f): Drawable? { + return getTintedDrawable(drawableRes, ContextCompat.getColor(this, tint), alpha) +} + +fun Context.getTintedDrawable(@DrawableRes drawableRes: Int, + @ColorInt tint: Int, + @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1f +) = ContextCompat.getDrawable(this, drawableRes) + ?.mutate() + ?.also { drawable -> + drawable.setTint(tint) + alpha.let { + drawable.alpha = it.toAndroidAlpha() + } + } + +private fun Float.toAndroidAlpha(): Int { + return (this * 255).roundToInt() +} diff --git a/vector/src/main/java/im/vector/app/core/extensions/TextView.kt b/vector/src/main/java/im/vector/app/core/extensions/TextView.kt index adb655f169..cb34b95fa1 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/TextView.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/TextView.kt @@ -16,6 +16,7 @@ package im.vector.app.core.extensions +import android.graphics.drawable.Drawable import android.text.Spannable import android.text.SpannableString import android.text.TextPaint @@ -82,7 +83,7 @@ fun TextView.setTextWithColoredPart(@StringRes fullTextRes: Int, fun TextView.setTextWithColoredPart(fullText: String, coloredPart: String, @AttrRes colorAttribute: Int = R.attr.colorPrimary, - underline: Boolean = false, + underline: Boolean = true, onClick: (() -> Unit)? = null) { val color = ThemeUtils.getColor(context, colorAttribute) @@ -101,7 +102,6 @@ fun TextView.setTextWithColoredPart(fullText: String, override fun updateDrawState(ds: TextPaint) { ds.color = color - ds.isUnderlineText = !underline } } setSpan(clickableSpan, index, index + coloredPart.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) @@ -122,7 +122,11 @@ fun TextView.setLeftDrawable(@DrawableRes iconRes: Int, @AttrRes tintColor: Int? } else { ContextCompat.getDrawable(context, iconRes) } - setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null) + setLeftDrawable(icon) +} + +fun TextView.setLeftDrawable(drawable: Drawable?) { + setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null) } /** diff --git a/vector/src/main/java/im/vector/app/core/platform/ScreenOrientationLocker.kt b/vector/src/main/java/im/vector/app/core/platform/ScreenOrientationLocker.kt new file mode 100644 index 0000000000..4b62090d3f --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/platform/ScreenOrientationLocker.kt @@ -0,0 +1,42 @@ +/* + * 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.platform + +import android.annotation.SuppressLint +import android.content.pm.ActivityInfo +import android.content.res.Resources +import androidx.appcompat.app.AppCompatActivity +import im.vector.app.R +import javax.inject.Inject + +class ScreenOrientationLocker @Inject constructor( + private val resources: Resources +) { + + // Some screens do not provide enough value for us to provide phone landscape experiences + @SuppressLint("SourceLockedOrientationActivity") + fun lockPhonesToPortrait(activity: AppCompatActivity) { + when (resources.getBoolean(R.bool.is_tablet)) { + true -> { + // do nothing + } + false -> { + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/core/platform/SimpleFragmentActivity.kt b/vector/src/main/java/im/vector/app/core/platform/SimpleFragmentActivity.kt index a70b2d66e6..4cd7da2a4f 100644 --- a/vector/src/main/java/im/vector/app/core/platform/SimpleFragmentActivity.kt +++ b/vector/src/main/java/im/vector/app/core/platform/SimpleFragmentActivity.kt @@ -30,7 +30,8 @@ abstract class SimpleFragmentActivity : VectorBaseActivity() { final override fun getCoordinatorLayout() = views.coordinatorLayout override fun initUiAndData() { - configureToolbar(views.toolbar) + setupToolbar(views.toolbar) + .allowBack(true) waitingView = views.waitingView.waitingView } diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt index 21419d55cf..8164df9c55 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt @@ -62,10 +62,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.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.analytics.VectorAnalytics +import im.vector.app.features.analytics.AnalyticsTracker +import im.vector.app.features.analytics.plan.Screen +import im.vector.app.features.analytics.screen.ScreenEvent import im.vector.app.features.configuration.VectorConfiguration import im.vector.app.features.consent.ConsentNotGivenHelper import im.vector.app.features.navigation.Navigator @@ -90,6 +93,15 @@ import timber.log.Timber import javax.inject.Inject abstract class VectorBaseActivity : AppCompatActivity(), MavericksView { + /* ========================================================================================== + * Analytics + * ========================================================================================== */ + + protected var analyticsScreenName: Screen.ScreenName? = null + private var screenEvent: ScreenEvent? = null + + protected lateinit var analyticsTracker: AnalyticsTracker + /* ========================================================================================== * View * ========================================================================================== */ @@ -115,6 +127,8 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver .launchIn(lifecycleScope) } + var toolbar: ToolbarConfig? = null + /* ========================================================================================== * Views * ========================================================================================== */ @@ -133,7 +147,6 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver private lateinit var sessionListener: SessionListener protected lateinit var bugReporter: BugReporter private lateinit var pinLocker: PinLocker - protected lateinit var analytics: VectorAnalytics @Inject lateinit var rageShake: RageShake @@ -189,7 +202,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver configurationViewModel = viewModelProvider.get(ConfigurationViewModel::class.java) bugReporter = singletonEntryPoint.bugReporter() pinLocker = singletonEntryPoint.pinLocker() - analytics = singletonEntryPoint.analytics() + analyticsTracker = singletonEntryPoint.analyticsTracker() navigator = singletonEntryPoint.navigator() activeSessionHolder = singletonEntryPoint.activeSessionHolder() vectorPreferences = singletonEntryPoint.vectorPreferences() @@ -324,7 +337,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver override fun onResume() { super.onResume() Timber.i("onResume Activity ${javaClass.simpleName}") - + screenEvent = analyticsScreenName?.let { ScreenEvent(it) } configurationViewModel.onActivityResumed() if (this !is BugReportActivity && vectorPreferences.useRageshake()) { @@ -363,6 +376,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver override fun onPause() { super.onPause() + screenEvent?.send(analyticsTracker, analyticsScreenName) Timber.i("onPause Activity ${javaClass.simpleName}") rageShake.stop() @@ -497,18 +511,6 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver */ protected fun isFirstCreation() = savedInstanceState == null - /** - * Configure the Toolbar, with default back button. - */ - protected fun configureToolbar(toolbar: MaterialToolbar, displayBack: Boolean = true) { - setSupportActionBar(toolbar) - supportActionBar?.let { - it.setDisplayShowHomeEnabled(displayBack) - it.setDisplayHomeAsUpEnabled(displayBack) - it.title = null - } - } - // ============================================================================================== // Handle loading view (also called waiting view or spinner view) // ============================================================================================== @@ -618,4 +620,13 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver toast(getString(R.string.not_implemented)) } } + + /** + * Sets toolbar as actionBar + * + * @return Instance of [ToolbarConfig] with set of helper methods to configure toolbar + * */ + fun setupToolbar(toolbar: MaterialToolbar) = ToolbarConfig(this, toolbar).also { + this.toolbar = it.setup() + } } diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt index 69c525dbde..7e6a429274 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt @@ -37,7 +37,9 @@ import im.vector.app.core.di.ActivityEntryPoint import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.extensions.toMvRxBundle import im.vector.app.core.utils.DimensionConverter -import im.vector.app.features.analytics.VectorAnalytics +import im.vector.app.features.analytics.AnalyticsTracker +import im.vector.app.features.analytics.plan.Screen +import im.vector.app.features.analytics.screen.ScreenEvent import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import reactivecircus.flowbinding.android.view.clicks @@ -47,6 +49,14 @@ import timber.log.Timber * Add Mavericks capabilities, handle DI and bindings. */ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment(), MavericksView { + /* ========================================================================================== + * Analytics + * ========================================================================================== */ + + protected var analyticsScreenName: Screen.ScreenName? = null + private var screenEvent: ScreenEvent? = null + + protected lateinit var analyticsTracker: AnalyticsTracker /* ========================================================================================== * View @@ -84,8 +94,6 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomShe open val showExpanded = false - protected lateinit var analytics: VectorAnalytics - interface ResultListener { fun onBottomSheetResult(resultCode: Int, data: Any?) @@ -124,13 +132,19 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomShe val activityEntryPoint = EntryPointAccessors.fromActivity(vectorBaseActivity, ActivityEntryPoint::class.java) viewModelFactory = activityEntryPoint.viewModelFactory() val singletonEntryPoint = context.singletonEntryPoint() - analytics = singletonEntryPoint.analytics() + analyticsTracker = singletonEntryPoint.analyticsTracker() super.onAttach(context) } override fun onResume() { super.onResume() Timber.i("onResume BottomSheet ${javaClass.simpleName}") + screenEvent = analyticsScreenName?.let { ScreenEvent(it) } + } + + override fun onPause() { + super.onPause() + screenEvent?.send(analyticsTracker) } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt index 64443139f1..8a1b9051cc 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt @@ -42,7 +42,10 @@ import im.vector.app.core.dialogs.UnrecognizedCertificateDialog import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.extensions.toMvRxBundle -import im.vector.app.features.analytics.VectorAnalytics +import im.vector.app.core.utils.ToolbarConfig +import im.vector.app.features.analytics.AnalyticsTracker +import im.vector.app.features.analytics.plan.Screen +import im.vector.app.features.analytics.screen.ScreenEvent import im.vector.app.features.navigation.Navigator import im.vector.lib.ui.styles.dialogs.MaterialProgressDialog import kotlinx.coroutines.flow.launchIn @@ -51,6 +54,18 @@ import reactivecircus.flowbinding.android.view.clicks import timber.log.Timber abstract class VectorBaseFragment : Fragment(), MavericksView { + /* ========================================================================================== + * Analytics + * ========================================================================================== */ + + protected var analyticsScreenName: Screen.ScreenName? = null + private var screenEvent: ScreenEvent? = null + + protected lateinit var analyticsTracker: AnalyticsTracker + + /* ========================================================================================== + * Activity + * ========================================================================================== */ protected val vectorBaseActivity: VectorBaseActivity<*> by lazy { activity as VectorBaseActivity<*> @@ -61,12 +76,17 @@ abstract class VectorBaseFragment : Fragment(), MavericksView * ========================================================================================== */ protected lateinit var navigator: Navigator - protected lateinit var analytics: VectorAnalytics protected lateinit var errorFormatter: ErrorFormatter protected lateinit var unrecognizedCertificateDialog: UnrecognizedCertificateDialog private var progress: AlertDialog? = null + /** + * [ToolbarConfig] instance from host activity + * */ + protected var toolbar: ToolbarConfig? = null + get() = (activity as? VectorBaseActivity<*>)?.toolbar + private set /* ========================================================================================== * View model * ========================================================================================== */ @@ -98,7 +118,7 @@ abstract class VectorBaseFragment : Fragment(), MavericksView val activityEntryPoint = EntryPointAccessors.fromActivity(vectorBaseActivity, ActivityEntryPoint::class.java) navigator = singletonEntryPoint.navigator() errorFormatter = singletonEntryPoint.errorFormatter() - analytics = singletonEntryPoint.analytics() + analyticsTracker = singletonEntryPoint.analyticsTracker() unrecognizedCertificateDialog = singletonEntryPoint.unrecognizedCertificateDialog() viewModelFactory = activityEntryPoint.viewModelFactory() childFragmentManager.fragmentFactory = activityEntryPoint.fragmentFactory() @@ -125,12 +145,14 @@ abstract class VectorBaseFragment : Fragment(), MavericksView override fun onResume() { super.onResume() Timber.i("onResume Fragment ${javaClass.simpleName}") + screenEvent = analyticsScreenName?.let { ScreenEvent(it) } } @CallSuper override fun onPause() { super.onPause() Timber.i("onPause Fragment ${javaClass.simpleName}") + screenEvent?.send(analyticsTracker) } @CallSuper @@ -213,13 +235,12 @@ abstract class VectorBaseFragment : Fragment(), MavericksView * ========================================================================================== */ /** - * Configure the Toolbar. - */ - protected fun setupToolbar(toolbar: MaterialToolbar) { - val parentActivity = vectorBaseActivity - if (parentActivity is ToolbarConfigurable) { - parentActivity.configure(toolbar) - } + * Sets toolbar as actionBar for current activity + * + * @return Instance of [ToolbarConfig] with set of helper methods to configure toolbar + * */ + protected fun setupToolbar(toolbar: MaterialToolbar): ToolbarConfig { + return vectorBaseActivity.setupToolbar(toolbar) } /* ========================================================================================== diff --git a/vector/src/main/java/im/vector/app/core/resources/LocaleProvider.kt b/vector/src/main/java/im/vector/app/core/resources/LocaleProvider.kt index 4ddf24414f..6a9d434aea 100644 --- a/vector/src/main/java/im/vector/app/core/resources/LocaleProvider.kt +++ b/vector/src/main/java/im/vector/app/core/resources/LocaleProvider.kt @@ -27,3 +27,5 @@ class LocaleProvider @Inject constructor(private val resources: Resources) { return ConfigurationCompat.getLocales(resources.configuration)[0] } } + +fun LocaleProvider.isEnglishSpeaking() = current().language.startsWith("en") diff --git a/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt b/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt index e7cabd1540..9ab3b9bf45 100644 --- a/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt +++ b/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt @@ -48,8 +48,4 @@ class UserPreferencesProvider @Inject constructor(private val vectorPreferences: fun shouldShowAvatarDisplayNameChanges(): Boolean { return vectorPreferences.showAvatarDisplayNameChangeMessages() } - - fun shouldShowPolls(): Boolean { - return vectorPreferences.labsEnablePolls() - } } diff --git a/vector/src/main/java/im/vector/app/core/ui/list/GenericFooterItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/GenericFooterItem.kt index 5e3ea285a5..6d7763cd48 100644 --- a/vector/src/main/java/im/vector/app/core/ui/list/GenericFooterItem.kt +++ b/vector/src/main/java/im/vector/app/core/ui/list/GenericFooterItem.kt @@ -24,10 +24,10 @@ import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide import im.vector.app.features.themes.ThemeUtils +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence /** * A generic list item. diff --git a/vector/src/main/java/im/vector/app/core/ui/list/GenericItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/GenericItem.kt index 3a691ddfd5..7b00001e4c 100644 --- a/vector/src/main/java/im/vector/app/core/ui/list/GenericItem.kt +++ b/vector/src/main/java/im/vector/app/core/ui/list/GenericItem.kt @@ -28,9 +28,9 @@ import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence /** * A generic list item. diff --git a/vector/src/main/java/im/vector/app/core/ui/list/GenericPillItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/GenericPillItem.kt index e116561ecb..09fdcded6e 100644 --- a/vector/src/main/java/im/vector/app/core/ui/list/GenericPillItem.kt +++ b/vector/src/main/java/im/vector/app/core/ui/list/GenericPillItem.kt @@ -28,10 +28,10 @@ import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide import im.vector.app.features.themes.ThemeUtils +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence /** * A generic list item with a rounded corner background and an optional icon diff --git a/vector/src/main/java/im/vector/app/core/ui/list/GenericWithValueItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/GenericWithValueItem.kt index f95281eb75..e633b633a7 100644 --- a/vector/src/main/java/im/vector/app/core/ui/list/GenericWithValueItem.kt +++ b/vector/src/main/java/im/vector/app/core/ui/list/GenericWithValueItem.kt @@ -27,10 +27,10 @@ import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide import im.vector.app.features.themes.ThemeUtils +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence /** * A generic list item. diff --git a/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt b/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt index 478f4d882b..a9375b6545 100644 --- a/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt +++ b/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt @@ -183,6 +183,26 @@ fun openMedia(activity: Activity, savedMediaPath: String, mimeType: String) { activity.safeStartActivity(intent) } +/** + * Open external location + * @param activity the activity + * @param latitude latitude of the location + * @param longitude longitude of the location + */ +fun openLocation(activity: Activity, latitude: Double, longitude: Double) { + val locationUri = buildString { + append("geo:") + append(latitude) + append(",") + append(longitude) + append("?q=") // This is required to drop a pin to the location + append(latitude) + append(",") + append(longitude) + } + openUri(activity, locationUri) +} + fun shareMedia(context: Context, file: File, mediaMimeType: String?) { val mediaUri = try { FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileProvider", file) diff --git a/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt b/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt index 19dc341f12..dabf11b9d3 100644 --- a/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt +++ b/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt @@ -40,6 +40,7 @@ val PERMISSIONS_FOR_MEMBERS_SEARCH = listOf(Manifest.permission.READ_CONTACTS) val PERMISSIONS_FOR_ROOM_AVATAR = listOf(Manifest.permission.CAMERA) val PERMISSIONS_FOR_WRITING_FILES = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE) val PERMISSIONS_FOR_PICKING_CONTACT = listOf(Manifest.permission.READ_CONTACTS) +val PERMISSIONS_FOR_LOCATION_SHARING = listOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION) val PERMISSIONS_EMPTY = emptyList() diff --git a/vector/src/main/java/im/vector/app/core/utils/ToolbarConfig.kt b/vector/src/main/java/im/vector/app/core/utils/ToolbarConfig.kt new file mode 100644 index 0000000000..53bc60a4a6 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/utils/ToolbarConfig.kt @@ -0,0 +1,90 @@ +/* + * 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.utils + +import androidx.annotation.StringRes +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.content.res.AppCompatResources +import com.google.android.material.appbar.MaterialToolbar +import im.vector.app.R + +/** + * Helper class to configure toolbar. + * Wraps [MaterialToolbar] providing set of methods to configure it + */ +class ToolbarConfig(val activity: AppCompatActivity, val toolbar: MaterialToolbar) { + private var customBackResId: Int? = null + + fun setup() = apply { + activity.setSupportActionBar(toolbar) + } + + /** + * Delegating property for [activity.supportActionBar?.title] + */ + var title: CharSequence? + set(value) { + setTitle(value) + } + get() = activity.supportActionBar?.title + + /** + * Delegating property for [activity.supportActionBar?.subtitle] + */ + var subtitle: CharSequence? + set(value) { + setSubtitle(value) + } + get() = activity.supportActionBar?.subtitle + + /** + * Sets toolbar's title text + */ + fun setTitle(title: CharSequence?) = apply { activity.supportActionBar?.title = title } + + /** + * Sets toolbar's title text using provided string resource + */ + fun setTitle(@StringRes titleRes: Int) = apply { activity.supportActionBar?.setTitle(titleRes) } + + /** + * Sets toolbar's subtitle text + */ + fun setSubtitle(subtitle: CharSequence?) = apply { activity.supportActionBar?.subtitle = subtitle } + + /** + * Sets toolbar's title text using provided string resource + */ + fun setSubtitle(@StringRes subtitleRes: Int) = apply { activity.supportActionBar?.setSubtitle(subtitleRes) } + + /** + * Enables/disables navigate back button + * + * @param isAllowed defines if back button is enabled. Default [true] + * @param useCross defines if cross icon should be used instead of arrow. Default [false] + */ + fun allowBack(isAllowed: Boolean = true, useCross: Boolean = false) = apply { + activity.supportActionBar?.let { + it.setDisplayShowHomeEnabled(isAllowed) + it.setDisplayHomeAsUpEnabled(isAllowed) + if (isAllowed && useCross) { + val navResId = customBackResId ?: R.drawable.ic_x_18dp + toolbar.navigationIcon = AppCompatResources.getDrawable(activity, navResId) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt index c53ff0f433..03e9954b2c 100644 --- a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt +++ b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt @@ -21,10 +21,9 @@ import im.vector.app.BuildConfig interface VectorFeatures { fun onboardingVariant(): OnboardingVariant - - fun isAlreadyHaveAccountSplashEnabled(): Boolean - - fun isSplashCarouselEnabled(): Boolean + fun isOnboardingAlreadyHaveAccountSplashEnabled(): Boolean + fun isOnboardingSplashCarouselEnabled(): Boolean + fun isOnboardingUseCaseEnabled(): Boolean enum class OnboardingVariant { LEGACY, @@ -35,6 +34,7 @@ interface VectorFeatures { class DefaultVectorFeatures : VectorFeatures { override fun onboardingVariant(): VectorFeatures.OnboardingVariant = BuildConfig.ONBOARDING_VARIANT - override fun isAlreadyHaveAccountSplashEnabled() = true - override fun isSplashCarouselEnabled() = false + override fun isOnboardingAlreadyHaveAccountSplashEnabled() = true + override fun isOnboardingSplashCarouselEnabled() = true + override fun isOnboardingUseCaseEnabled() = false } diff --git a/vector/src/main/java/im/vector/app/features/analytics/AnalyticsTracker.kt b/vector/src/main/java/im/vector/app/features/analytics/AnalyticsTracker.kt new file mode 100644 index 0000000000..e1da0f4434 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/AnalyticsTracker.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2021 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.analytics + +import im.vector.app.features.analytics.itf.VectorAnalyticsEvent +import im.vector.app.features.analytics.itf.VectorAnalyticsScreen + +interface AnalyticsTracker { + /** + * Capture an Event + */ + fun capture(event: VectorAnalyticsEvent) + + /** + * Track a displayed screen + */ + fun screen(screen: VectorAnalyticsScreen) +} diff --git a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt index 18fec37c62..6b2ceb1444 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt @@ -16,10 +16,10 @@ package im.vector.app.features.analytics -import im.vector.app.core.flow.tickerFlow import im.vector.app.core.time.Clock -import im.vector.app.core.utils.compat.removeIfCompat import im.vector.app.features.analytics.plan.Error +import im.vector.lib.core.utils.compat.removeIfCompat +import im.vector.lib.core.utils.flow.tickerFlow import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -49,7 +49,7 @@ private const val CHECK_INTERVAL = 2_000L */ @Singleton class DecryptionFailureTracker @Inject constructor( - private val vectorAnalytics: VectorAnalytics, + private val analyticsTracker: AnalyticsTracker, private val clock: Clock ) { @@ -136,7 +136,7 @@ class DecryptionFailureTracker @Inject constructor( // for now we ignore events already reported even if displayed again? .filter { alreadyReported.contains(it).not() } .forEach { failedEventId -> - vectorAnalytics.capture(Error(failedEventId, Error.Domain.E2EE, aggregation.key)) + analyticsTracker.capture(Error(failedEventId, Error.Domain.E2EE, aggregation.key)) alreadyReported.add(failedEventId) } } diff --git a/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt b/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt index 476f5ade56..95322412bd 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt @@ -16,11 +16,9 @@ package im.vector.app.features.analytics -import im.vector.app.features.analytics.itf.VectorAnalyticsEvent -import im.vector.app.features.analytics.itf.VectorAnalyticsScreen import kotlinx.coroutines.flow.Flow -interface VectorAnalytics { +interface VectorAnalytics : AnalyticsTracker { /** * Return a Flow of Boolean, true if the user has given their consent */ @@ -60,14 +58,4 @@ interface VectorAnalytics { * To be called when application is started */ fun init() - - /** - * Capture an Event - */ - fun capture(event: VectorAnalyticsEvent) - - /** - * Track a displayed screen - */ - fun screen(screen: VectorAnalyticsScreen) } diff --git a/vector/src/main/java/im/vector/app/features/analytics/extensions/JoinedRoomExt.kt b/vector/src/main/java/im/vector/app/features/analytics/extensions/JoinedRoomExt.kt new file mode 100644 index 0000000000..ff23fd9a64 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/extensions/JoinedRoomExt.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2021 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.analytics.extensions + +import im.vector.app.features.analytics.plan.JoinedRoom +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom + +fun Int?.toAnalyticsRoomSize(): JoinedRoom.RoomSize { + return when (this) { + null, + 2 -> JoinedRoom.RoomSize.Two + in 3..10 -> JoinedRoom.RoomSize.ThreeToTen + in 11..100 -> JoinedRoom.RoomSize.ElevenToOneHundred + in 101..1000 -> JoinedRoom.RoomSize.OneHundredAndOneToAThousand + else -> JoinedRoom.RoomSize.MoreThanAThousand + } +} + +fun RoomSummary?.toAnalyticsJoinedRoom(): JoinedRoom { + return JoinedRoom( + isDM = this?.isDirect.orFalse(), + roomSize = this?.joinedMembersCount?.toAnalyticsRoomSize() ?: JoinedRoom.RoomSize.Two + ) +} + +fun PublicRoom.toAnalyticsJoinedRoom(): JoinedRoom { + return JoinedRoom( + isDM = false, + roomSize = numJoinedMembers.toAnalyticsRoomSize() + ) +} diff --git a/vector/src/main/java/im/vector/app/features/analytics/extensions/PerformanceTimerExt.kt b/vector/src/main/java/im/vector/app/features/analytics/extensions/PerformanceTimerExt.kt new file mode 100644 index 0000000000..11b4b670d3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/extensions/PerformanceTimerExt.kt @@ -0,0 +1,51 @@ +/* + * 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.analytics.extensions + +import im.vector.app.features.analytics.plan.PerformanceTimer +import org.matrix.android.sdk.api.session.statistics.StatisticEvent + +fun StatisticEvent.toListOfPerformanceTimer(): List { + return when (this) { + is StatisticEvent.InitialSyncRequest -> + listOf( + PerformanceTimer( + name = PerformanceTimer.Name.InitialSyncRequest, + timeMs = requestDurationMs + downloadDurationMs, + itemCount = nbOfJoinedRooms + ), + PerformanceTimer( + name = PerformanceTimer.Name.InitialSyncParsing, + timeMs = treatmentDurationMs, + itemCount = nbOfJoinedRooms + ) + ) + is StatisticEvent.SyncTreatment -> + if (afterPause) { + listOf( + PerformanceTimer( + name = PerformanceTimer.Name.StartupIncrementalSync, + timeMs = durationMs, + itemCount = nbOfJoinedRooms + ) + ) + } else { + // We do not report + emptyList() + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/CallEnded.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/CallEnded.kt index cd813325f1..3bf16a6c97 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/CallEnded.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/CallEnded.kt @@ -25,22 +25,22 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent * Triggered when a call has ended. */ data class CallEnded( - /** - * The duration of the call in milliseconds. - */ - val durationMs: Int, - /** - * Whether its a video call or not. - */ - val isVideo: Boolean, - /** - * Number of participants in the call. - */ - val numParticipants: Int, - /** - * Whether this user placed it. - */ - val placed: Boolean, + /** + * The duration of the call in milliseconds. + */ + val durationMs: Int, + /** + * Whether its a video call or not. + */ + val isVideo: Boolean, + /** + * Number of participants in the call. + */ + val numParticipants: Int, + /** + * Whether this user placed it. + */ + val placed: Boolean, ) : VectorAnalyticsEvent { override fun getName() = "CallEnded" diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/CallError.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/CallError.kt index 18e77f9f1c..1c3a57e971 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/CallError.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/CallError.kt @@ -25,18 +25,18 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent * Triggered when an error occurred in a call. */ data class CallError( - /** - * Whether its a video call or not. - */ - val isVideo: Boolean, - /** - * Number of participants in the call. - */ - val numParticipants: Int, - /** - * Whether this user placed it. - */ - val placed: Boolean, + /** + * Whether its a video call or not. + */ + val isVideo: Boolean, + /** + * Number of participants in the call. + */ + val numParticipants: Int, + /** + * Whether this user placed it. + */ + val placed: Boolean, ) : VectorAnalyticsEvent { override fun getName() = "CallError" diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/CallStarted.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/CallStarted.kt index 81f4b6c194..e74d07d38c 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/CallStarted.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/CallStarted.kt @@ -25,18 +25,18 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent * Triggered when a call is started. */ data class CallStarted( - /** - * Whether its a video call or not. - */ - val isVideo: Boolean, - /** - * Number of participants in the call. - */ - val numParticipants: Int, - /** - * Whether this user placed it. - */ - val placed: Boolean, + /** + * Whether its a video call or not. + */ + val isVideo: Boolean, + /** + * Number of participants in the call. + */ + val numParticipants: Int, + /** + * Whether this user placed it. + */ + val placed: Boolean, ) : VectorAnalyticsEvent { override fun getName() = "CallStarted" diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/Click.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/Click.kt index fbc36a1195..fbc847165d 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/Click.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/Click.kt @@ -25,14 +25,14 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent * Triggered when the user clicks/taps on a UI element. */ data class Click( - /** - * The index of the element, if its in a list of elements. - */ - val index: Int? = null, - /** - * The unique name of this element. - */ - val name: Name, + /** + * The index of the element, if its in a list of elements. + */ + val index: Int? = null, + /** + * The unique name of this element. + */ + val name: Name, ) : VectorAnalyticsEvent { enum class Name { diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/CreatedRoom.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/CreatedRoom.kt index 9562a6e735..598cc6ac28 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/CreatedRoom.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/CreatedRoom.kt @@ -25,10 +25,10 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent * Triggered when the user creates a room. */ data class CreatedRoom( - /** - * Whether the room is a DM. - */ - val isDM: Boolean, + /** + * Whether the room is a DM. + */ + val isDM: Boolean, ) : VectorAnalyticsEvent { override fun getName() = "CreatedRoom" diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/Error.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/Error.kt index 988ad309b9..a926776680 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/Error.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/Error.kt @@ -25,12 +25,12 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent * Triggered when an error occurred */ data class Error( - /** - * Context - client defined, can be used for debugging - */ - val context: String? = null, - val domain: Domain, - val name: Name, + /** + * Context - client defined, can be used for debugging + */ + val context: String? = null, + val domain: Domain, + val name: Name, ) : VectorAnalyticsEvent { enum class Domain { diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/Identity.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/Identity.kt new file mode 100644 index 0000000000..1cc433aa7e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/Identity.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2021 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.analytics.plan + +import im.vector.app.features.analytics.itf.VectorAnalyticsEvent + +// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT +// https://github.com/matrix-org/matrix-analytics-events/ + +/** + * The user properties to apply when identifying + */ +data class Identity( + /** + * The selected messaging use case during the onboarding flow. + */ + val ftueUseCaseSelection: FtueUseCaseSelection? = null, +) : VectorAnalyticsEvent { + + enum class FtueUseCaseSelection { + /** + * The third option, Communities. + */ + CommunityMessaging, + + /** + * The first option, Friends and family. + */ + PersonalMessaging, + + /** + * The footer option to skip the question. + */ + Skip, + + /** + * The second option, Teams. + */ + WorkMessaging, + } + + override fun getName() = "Identity" + + override fun getProperties(): Map? { + return mutableMapOf().apply { + ftueUseCaseSelection?.let { put("ftueUseCaseSelection", it.name) } + }.takeIf { it.isNotEmpty() } + } +} diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/JoinedRoom.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/JoinedRoom.kt index fc5f29bff1..97ac19ec93 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/JoinedRoom.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/JoinedRoom.kt @@ -25,14 +25,14 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent * Triggered when the user joins a room. */ data class JoinedRoom( - /** - * Whether the room is a DM. - */ - val isDM: Boolean, - /** - * The size of the room. - */ - val roomSize: RoomSize, + /** + * Whether the room is a DM. + */ + val isDM: Boolean, + /** + * The size of the room. + */ + val roomSize: RoomSize, ) : VectorAnalyticsEvent { enum class RoomSize { diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/PerformanceTimer.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/PerformanceTimer.kt index 34d0297f2d..2cfc366cd3 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/PerformanceTimer.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/PerformanceTimer.kt @@ -25,22 +25,23 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent * Triggered after timing an operation in the app. */ data class PerformanceTimer( - /** - * Client defined, can be used for debugging. - */ - val context: String? = null, - /** - * Client defined, an optional value to indicate how many items were handled during the operation. - */ - val itemCount: Int? = null, - /** - * The timer that is being reported. - */ - val name: Name, - /** - * The time reported by the timer in milliseconds. - */ - val timeMs: Int, + /** + * Client defined, can be used for debugging. + */ + val context: String? = null, + /** + * Client defined, an optional value to indicate how many items were + * handled during the operation. + */ + val itemCount: Int? = null, + /** + * The timer that is being reported. + */ + val name: Name, + /** + * The time reported by the timer in milliseconds. + */ + val timeMs: Int, ) : VectorAnalyticsEvent { enum class Name { @@ -55,7 +56,8 @@ data class PerformanceTimer( InitialSyncRequest, /** - * The time taken to display an event in the timeline that was opened from a notification. + * The time taken to display an event in the timeline that was opened + * from a notification. */ NotificationsOpenEvent, @@ -65,7 +67,8 @@ data class PerformanceTimer( StartupIncrementalSync, /** - * The duration of an initial /sync request during startup (if the store has been wiped). + * The duration of an initial /sync request during startup (if the store + * has been wiped). */ StartupInitialSync, @@ -80,7 +83,8 @@ data class PerformanceTimer( StartupStorePreload, /** - * The time to load all data from the store (including StartupStorePreload time). + * The time to load all data from the store (including + * StartupStorePreload time). */ StartupStoreReady, } diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/Screen.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/Screen.kt index 1f18ceee00..db4dcd0fac 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/Screen.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/Screen.kt @@ -25,28 +25,221 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsScreen * Triggered when the user changed screen */ data class Screen( - /** - * How long the screen was displayed for in milliseconds. - */ - val durationMs: Int? = null, - val screenName: ScreenName, + /** + * How long the screen was displayed for in milliseconds. + */ + val durationMs: Int? = null, + val screenName: ScreenName, ) : VectorAnalyticsScreen { enum class ScreenName { + /** + * The screen shown to create a new (non-direct) room. + */ + CreateRoom, + + /** + * The confirmation screen shown before deactivating an account. + */ + DeactivateAccount, + + /** + * The form for the forgot password use case + */ + ForgotPassword, + + /** + * Legacy: The screen that shows information about a specific group. + */ Group, + + /** + * The Home tab on iOS | possibly the same on Android? | The Home space + * on Web? + */ Home, + + /** + * The screen that displays the login flow (when the user already has an + * account). + */ + Login, + + /** + * The screen that displays the user's breadcrumbs. + */ + MobileBreadcrumbs, + + /** + * The tab on mobile that displays the dialpad. + */ + MobileDialpad, + + /** + * The Favourites tab on mobile that lists your favourite people/rooms. + */ + MobileFavourites, + + /** + * The screen shown to share a link to download the app. + */ + MobileInviteFriends, + + /** + * The People tab on mobile that lists all the DM rooms you have joined. + */ + MobilePeople, + + /** + * The Rooms tab on mobile that lists all the (non-direct) rooms you've + * joined. + */ + MobileRooms, + + /** + * The Files tab shown in the global search screen on Mobile. + */ + MobileSearchFiles, + + /** + * The Messages tab shown in the global search screen on Mobile. + */ + MobileSearchMessages, + + /** + * The People tab shown in the global search screen on Mobile. + */ + MobileSearchPeople, + + /** + * The Rooms tab shown in the global search screen on Mobile. + */ + MobileSearchRooms, + + /** + * The sidebar shown on mobile with spaces, settings etc. + */ + MobileSidebar, + + /** + * The screen shown to select which room directory you'd like to use. + */ + MobileSwitchDirectory, + + /** + * Legacy: The screen that shows all groups/communities you have joined. + */ MyGroups, + + /** + * The screen that displays the registration flow (when the user wants + * to create an account) + */ + Register, + + /** + * The screen that displays the messages and events received in a room. + */ Room, + + /** + * The screen shown when tapping the name of a room from the Room + * screen. + */ + RoomDetails, + + /** + * The screen that lists public rooms for you to discover. + */ RoomDirectory, + + /** + * The screen that lists all the user's rooms and let them filter the + * rooms. + */ + RoomFilter, + + /** + * The screen that displays the list of members that are part of a room. + */ + RoomMembers, + + /** + * The notifications settings screen shown from the Room Details screen. + */ + RoomNotifications, + + /** + * The screen that allows you to search for messages/files in a specific + * room. + */ + RoomSearch, + + /** + * The settings screen shown from the Room Details screen. + */ + RoomSettings, + + /** + * The screen that allows you to see all of the files sent in a specific + * room. + */ + RoomUploads, + + /** + * The global settings screen shown in the app. + */ + Settings, + + /** + * The settings screen to change the default notification options. + */ + SettingsDefaultNotifications, + + /** + * The settings screen to manage notification mentions and keywords. + */ + SettingsMentionsAndKeywords, + + /** + * The global security settings screen. + */ + SettingsSecurity, + + /** + * The screen shown to create a new direct room. + */ + StartChat, + + /** + * A screen that shows information about a room member. + */ User, + + /** + * ? + */ WebCompleteSecurity, + + /** + * ? + */ WebE2ESetup, - WebForgotPassword, + + /** + * ? + */ WebLoading, - WebLogin, - WebRegister, + + /** + * ? + */ WebSoftLogout, - WebWelcome, + + /** + * The splash screen. + */ + Welcome, } override fun getName() = screenName.name diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/UnauthenticatedError.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/UnauthenticatedError.kt new file mode 100644 index 0000000000..56ef4af4be --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/UnauthenticatedError.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2021 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.analytics.plan + +import im.vector.app.features.analytics.itf.VectorAnalyticsEvent + +// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT +// https://github.com/matrix-org/matrix-analytics-events/ + +/** + * Triggered when the user becomes unauthenticated without actually clicking + * sign out(E.g. Due to expiry of an access token without a way to refresh). + */ +data class UnauthenticatedError( + /** + * The error code as defined in matrix spec. The source of this error is + * from the homeserver. + */ + val errorCode: ErrorCode, + /** + * The reason for the error. The source of this error is from the + * homeserver, the reason can vary and is subject to change so there is + * no enum of possible values. + */ + val errorReason: String, + /** + * Whether the auth mechanism is refresh-token-based. + */ + val refreshTokenAuth: Boolean, + /** + * Whether a soft logout or hard logout was triggered. + */ + val softLogout: Boolean, +) : VectorAnalyticsEvent { + + enum class ErrorCode { + M_FORBIDDEN, + M_UNKNOWN, + M_UNKNOWN_TOKEN, + } + + override fun getName() = "UnauthenticatedError" + + override fun getProperties(): Map? { + return mutableMapOf().apply { + put("errorCode", errorCode.name) + put("errorReason", errorReason) + put("refreshTokenAuth", refreshTokenAuth) + put("softLogout", softLogout) + }.takeIf { it.isNotEmpty() } + } +} diff --git a/vector/src/main/java/im/vector/app/features/analytics/screen/ScreenEvent.kt b/vector/src/main/java/im/vector/app/features/analytics/screen/ScreenEvent.kt new file mode 100644 index 0000000000..8e0513f25a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/screen/ScreenEvent.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2021 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.analytics.screen + +import android.os.SystemClock +import im.vector.app.features.analytics.AnalyticsTracker +import im.vector.app.features.analytics.plan.Screen +import timber.log.Timber + +/** + * Track a screen display. Unique usage. + */ +class ScreenEvent(val screenName: Screen.ScreenName) { + private val startTime = SystemClock.elapsedRealtime() + + // Protection to avoid multiple sending + private var isSent = false + + /** + * @param screenNameOverride can be used to override the screen name passed in constructor parameter + */ + fun send(analyticsTracker: AnalyticsTracker, + screenNameOverride: Screen.ScreenName? = null) { + if (isSent) { + Timber.w("Event $screenName Already sent!") + return + } + isSent = true + analyticsTracker.screen( + Screen( + screenName = screenNameOverride ?: screenName, + durationMs = (SystemClock.elapsedRealtime() - startTime).toInt() + ) + ) + } +} diff --git a/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsOptInActivity.kt b/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsOptInActivity.kt index f6a06ebdb7..c84031d2fd 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsOptInActivity.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsOptInActivity.kt @@ -20,8 +20,10 @@ import com.airbnb.mvrx.viewModel import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.platform.ScreenOrientationLocker import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleBinding +import javax.inject.Inject /** * Simple container for AnalyticsOptInFragment @@ -29,6 +31,8 @@ import im.vector.app.databinding.ActivitySimpleBinding @AndroidEntryPoint class AnalyticsOptInActivity : VectorBaseActivity() { + @Inject lateinit var orientationLocker: ScreenOrientationLocker + private val viewModel: AnalyticsConsentViewModel by viewModel() override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater) @@ -36,6 +40,7 @@ class AnalyticsOptInActivity : VectorBaseActivity() { override fun getCoordinatorLayout() = views.coordinatorLayout override fun initUiAndData() { + orientationLocker.lockPhonesToPortrait(this) if (isFirstCreation()) { addFragment(views.simpleFragmentContainer, AnalyticsOptInFragment::class.java) } diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt index 683c5908ba..a15bd52174 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt @@ -37,6 +37,7 @@ import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.core.epoxy.onClick import im.vector.app.core.utils.PERMISSIONS_EMPTY +import im.vector.app.core.utils.PERMISSIONS_FOR_LOCATION_SHARING import im.vector.app.core.utils.PERMISSIONS_FOR_PICKING_CONTACT import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.app.databinding.ViewAttachmentTypeSelectorBinding @@ -71,6 +72,7 @@ class AttachmentTypeSelectorView(context: Context, views.attachmentStickersButton.configure(Type.STICKER) views.attachmentContactButton.configure(Type.CONTACT) views.attachmentPollButton.configure(Type.POLL) + views.attachmentLocationButton.configure(Type.LOCATION) width = LinearLayout.LayoutParams.MATCH_PARENT height = LinearLayout.LayoutParams.WRAP_CONTENT animationStyle = 0 @@ -123,12 +125,13 @@ class AttachmentTypeSelectorView(context: Context, fun setAttachmentVisibility(type: Type, isVisible: Boolean) { when (type) { - Type.CAMERA -> views.attachmentCameraButton - Type.GALLERY -> views.attachmentGalleryButton - Type.FILE -> views.attachmentFileButton - Type.STICKER -> views.attachmentStickersButton - Type.CONTACT -> views.attachmentContactButton - Type.POLL -> views.attachmentPollButton + Type.CAMERA -> views.attachmentCameraButton + Type.GALLERY -> views.attachmentGalleryButton + Type.FILE -> views.attachmentFileButton + Type.STICKER -> views.attachmentStickersButton + Type.CONTACT -> views.attachmentContactButton + Type.POLL -> views.attachmentPollButton + Type.LOCATION -> views.attachmentLocationButton }.let { it.isVisible = isVisible } @@ -211,6 +214,7 @@ class AttachmentTypeSelectorView(context: Context, FILE(PERMISSIONS_EMPTY, R.string.tooltip_attachment_file), STICKER(PERMISSIONS_EMPTY, R.string.tooltip_attachment_sticker), CONTACT(PERMISSIONS_FOR_PICKING_CONTACT, R.string.tooltip_attachment_contact), - POLL(PERMISSIONS_EMPTY, R.string.tooltip_attachment_poll) + POLL(PERMISSIONS_EMPTY, R.string.tooltip_attachment_poll), + LOCATION(PERMISSIONS_FOR_LOCATION_SHARING, R.string.tooltip_attachment_location) } } diff --git a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewActivity.kt b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewActivity.kt index a52036011f..7ddba0d229 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewActivity.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewActivity.kt @@ -19,17 +19,15 @@ package im.vector.app.features.attachments.preview import android.content.Context import android.content.Intent -import com.google.android.material.appbar.MaterialToolbar import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.addFragment -import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleBinding import im.vector.app.features.themes.ActivityOtherThemes import org.matrix.android.sdk.api.session.content.ContentAttachmentData @AndroidEntryPoint -class AttachmentsPreviewActivity : VectorBaseActivity(), ToolbarConfigurable { +class AttachmentsPreviewActivity : VectorBaseActivity() { companion object { private const val EXTRA_FRAGMENT_ARGS = "EXTRA_FRAGMENT_ARGS" @@ -72,8 +70,4 @@ class AttachmentsPreviewActivity : VectorBaseActivity(), setResult(RESULT_OK, resultIntent) finish() } - - override fun configure(toolbar: MaterialToolbar) { - configureToolbar(toolbar) - } } diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandPresenter.kt b/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandPresenter.kt index 5ad31aeaa6..9888f1e35e 100644 --- a/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandPresenter.kt +++ b/vector/src/main/java/im/vector/app/features/autocomplete/command/AutocompleteCommandPresenter.kt @@ -50,7 +50,7 @@ class AutocompleteCommandPresenter @Inject constructor(context: Context, if (query.isNullOrEmpty()) { true } else { - it.command.startsWith(query, 1, true) + it.startsWith(query) } } controller.setData(data) diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index 995dc3d5e8..22f1fc40a2 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt @@ -126,7 +126,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro if (savedInstanceState != null) { (supportFragmentManager.findFragmentByTag(FRAGMENT_DIAL_PAD_TAG) as? CallDialPadBottomSheet)?.callback = dialPadCallback } - setSupportActionBar(views.callToolbar) + setupToolbar(views.callToolbar) configureCallViews() callViewModel.onEach { @@ -257,18 +257,18 @@ class VectorCallActivity : VectorBaseActivity(), CallContro views.fullscreenRenderer.isVisible = false views.pipRendererWrapper.isVisible = false views.callInfoGroup.isVisible = true - views.callToolbar.setSubtitle(R.string.call_ringing) + toolbar?.setSubtitle(R.string.call_ringing) configureCallInfo(state) } is CallState.Answering -> { views.fullscreenRenderer.isVisible = false views.pipRendererWrapper.isVisible = false views.callInfoGroup.isVisible = true - views.callToolbar.setSubtitle(R.string.call_connecting) + toolbar?.setSubtitle(R.string.call_connecting) configureCallInfo(state) } is CallState.Connected -> { - views.callToolbar.subtitle = state.formattedDuration + toolbar?.subtitle = state.formattedDuration if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { if (state.isLocalOnHold || state.isRemoteOnHold) { views.smallIsHeldIcon.isVisible = true @@ -280,11 +280,11 @@ class VectorCallActivity : VectorBaseActivity(), CallContro views.callActionText.setText(R.string.call_resume_action) views.callActionText.isVisible = true views.callActionText.setOnClickListener { callViewModel.handle(VectorCallViewActions.ToggleHoldResume) } - views.callToolbar.setSubtitle(R.string.call_held_by_you) + toolbar?.setSubtitle(R.string.call_held_by_you) } else { views.callActionText.isInvisible = true state.callInfo?.opponentUserItem?.let { - views.callToolbar.subtitle = getString(R.string.call_held_by_user, it.getBestName()) + toolbar?.subtitle = getString(R.string.call_held_by_user, it.getBestName()) } } } else if (state.transferee !is VectorCallViewState.TransfereeState.NoTransferee) { @@ -316,14 +316,14 @@ class VectorCallActivity : VectorBaseActivity(), CallContro views.pipRendererWrapper.isVisible = false views.callInfoGroup.isVisible = true configureCallInfo(state) - views.callToolbar.setSubtitle(R.string.call_connecting) + toolbar?.setSubtitle(R.string.call_connecting) } } is CallState.Ended -> { views.fullscreenRenderer.isVisible = false views.pipRendererWrapper.isVisible = false views.callInfoGroup.isVisible = true - views.callToolbar.setSubtitle(R.string.call_ended) + toolbar?.setSubtitle(R.string.call_ended) configureCallInfo(state) } else -> { @@ -410,7 +410,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro avatarRenderer.renderBlur(it, views.bgCallView, sampling = 20, rounded = false, colorFilter = colorFilter, addPlaceholder = false) if (state.transferee is VectorCallViewState.TransfereeState.NoTransferee) { views.participantNameText.setTextOrHide(null) - views.callToolbar.title = if (state.isVideoCall) { + toolbar?.title = if (state.isVideoCall) { getString(R.string.video_call_with_participant, it.getBestName()) } else { getString(R.string.audio_call_with_participant, it.getBestName()) diff --git a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadFragment.kt b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadFragment.kt index 16e7c01b5c..5fc866a4dd 100644 --- a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadFragment.kt +++ b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadFragment.kt @@ -17,6 +17,7 @@ package im.vector.app.features.call.dialpad import android.content.ClipboardManager +import android.content.Context import android.content.res.ColorStateList import android.os.Bundle import android.telephony.PhoneNumberFormattingTextWatcher @@ -37,6 +38,10 @@ import androidx.fragment.app.Fragment import com.android.dialer.dialpadview.DialpadView import com.android.dialer.dialpadview.DigitsEditText import im.vector.app.R +import im.vector.app.core.extensions.singletonEntryPoint +import im.vector.app.features.analytics.AnalyticsTracker +import im.vector.app.features.analytics.plan.Screen +import im.vector.app.features.analytics.screen.ScreenEvent import im.vector.app.features.themes.ThemeUtils class DialPadFragment : Fragment(), TextWatcher { @@ -53,6 +58,25 @@ class DialPadFragment : Fragment(), TextWatcher { private var enableDelete = true private var enableFabOk = true + private lateinit var analyticsTracker: AnalyticsTracker + + override fun onAttach(context: Context) { + super.onAttach(context) + val singletonEntryPoint = context.singletonEntryPoint() + analyticsTracker = singletonEntryPoint.analyticsTracker() + } + + private var screenEvent: ScreenEvent? = null + override fun onResume() { + super.onResume() + screenEvent = ScreenEvent(Screen.ScreenName.MobileDialpad) + } + + override fun onPause() { + super.onPause() + screenEvent?.send(analyticsTracker) + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt index c03b526f8c..959e96cc4c 100644 --- a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt @@ -70,7 +70,8 @@ class CallTransferActivity : VectorBaseActivity() { CallTransferPagerAdapter.DIAL_PAD_INDEX -> tab.text = getString(R.string.call_dial_pad_title) } }.attach() - configureToolbar(views.callTransferToolbar) + setupToolbar(views.callTransferToolbar) + .allowBack() views.callTransferToolbar.title = getString(R.string.call_transfer_title) setupConnectAction() } diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index bbb158f6e4..90088c8475 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -19,9 +19,7 @@ package im.vector.app.features.call.webrtc import android.content.Context import android.hardware.camera2.CameraManager import androidx.core.content.getSystemService -import im.vector.app.core.flow.chunk import im.vector.app.core.services.CallService -import im.vector.app.core.utils.CountUpTimer import im.vector.app.core.utils.PublishDataSource import im.vector.app.core.utils.TextUtils.formatDuration import im.vector.app.features.call.CameraEventsHandlerAdapter @@ -37,6 +35,8 @@ import im.vector.app.features.call.utils.awaitSetLocalDescription import im.vector.app.features.call.utils.awaitSetRemoteDescription import im.vector.app.features.call.utils.mapToCallCandidate import im.vector.app.features.session.coroutineScope +import im.vector.lib.core.utils.flow.chunk +import im.vector.lib.core.utils.timer.CountUpTimer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers @@ -270,6 +270,10 @@ class WebRtcCall( } } + fun durationMillis(): Int { + return timer.elapsedTime().toInt() + } + fun formattedDuration(): String { return formatDuration( Duration.ofMillis(timer.elapsedTime()) diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt index 80390a7dfb..fe12bf1ec7 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt @@ -22,6 +22,9 @@ import androidx.lifecycle.LifecycleOwner import im.vector.app.ActiveSessionDataSource import im.vector.app.BuildConfig import im.vector.app.core.services.CallService +import im.vector.app.features.analytics.AnalyticsTracker +import im.vector.app.features.analytics.plan.CallEnded +import im.vector.app.features.analytics.plan.CallStarted import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.audio.CallAudioManager import im.vector.app.features.call.lookup.CallProtocolsChecker @@ -68,7 +71,8 @@ private val loggerTag = LoggerTag("WebRtcCallManager", LoggerTag.VOIP) @Singleton class WebRtcCallManager @Inject constructor( private val context: Context, - private val activeSessionDataSource: ActiveSessionDataSource + private val activeSessionDataSource: ActiveSessionDataSource, + private val analyticsTracker: AnalyticsTracker ) : CallListener, DefaultLifecycleObserver { @@ -237,6 +241,7 @@ class WebRtcCallManager @Inject constructor( val currentCall = getCurrentCall().takeIf { it != call } currentCall?.updateRemoteOnHold(onHold = true) audioManager.setMode(if (call.mxCall.isVideoCall) CallAudioManager.Mode.VIDEO_CALL else CallAudioManager.Mode.AUDIO_CALL) + call.trackCallStarted() this.currentCall.setAndNotify(call) } @@ -245,6 +250,7 @@ class WebRtcCallManager @Inject constructor( val webRtcCall = callsByCallId.remove(callId) ?: return Unit.also { Timber.tag(loggerTag.value).v("On call ended for unknown call $callId") } + webRtcCall.trackCallEnded() CallService.onCallTerminated(context, callId, endCallReason, rejected) callsByRoomId[webRtcCall.signalingRoomId]?.remove(webRtcCall) callsByRoomId[webRtcCall.nativeRoomId]?.remove(webRtcCall) @@ -443,4 +449,28 @@ class WebRtcCallManager @Inject constructor( } call.onCallAssertedIdentityReceived(callAssertedIdentityContent) } + + /** + * Analytics + */ + private fun WebRtcCall.trackCallStarted() { + analyticsTracker.capture( + CallStarted( + isVideo = mxCall.isVideoCall, + numParticipants = 2, + placed = mxCall.isOutgoing + ) + ) + } + + private fun WebRtcCall.trackCallEnded() { + analyticsTracker.capture( + CallEnded( + durationMs = durationMillis(), + isVideo = mxCall.isVideoCall, + numParticipants = 2, + placed = mxCall.isOutgoing + ) + ) + } } diff --git a/vector/src/main/java/im/vector/app/features/command/Command.kt b/vector/src/main/java/im/vector/app/features/command/Command.kt index 1950038691..01f8cf234b 100644 --- a/vector/src/main/java/im/vector/app/features/command/Command.kt +++ b/vector/src/main/java/im/vector/app/features/command/Command.kt @@ -24,42 +24,50 @@ import im.vector.app.R * the user can write theses messages to perform some actions * the list will be displayed in this order */ -enum class Command(val command: String, val parameters: String, @StringRes val description: Int, val isDevCommand: Boolean) { - EMOTE("/me", "", R.string.command_description_emote, false), - BAN_USER("/ban", " [reason]", R.string.command_description_ban_user, false), - UNBAN_USER("/unban", " [reason]", R.string.command_description_unban_user, false), - IGNORE_USER("/ignore", " [reason]", R.string.command_description_ignore_user, false), - UNIGNORE_USER("/unignore", "", R.string.command_description_unignore_user, false), - SET_USER_POWER_LEVEL("/op", " []", R.string.command_description_op_user, false), - RESET_USER_POWER_LEVEL("/deop", "", R.string.command_description_deop_user, false), - ROOM_NAME("/roomname", "", R.string.command_description_room_name, false), - INVITE("/invite", " [reason]", R.string.command_description_invite_user, false), - JOIN_ROOM("/join", " [reason]", R.string.command_description_join_room, false), - PART("/part", "[]", R.string.command_description_part_room, false), - TOPIC("/topic", "", R.string.command_description_topic, false), - KICK_USER("/kick", " [reason]", R.string.command_description_kick_user, false), - CHANGE_DISPLAY_NAME("/nick", "", R.string.command_description_nick, false), - CHANGE_DISPLAY_NAME_FOR_ROOM("/myroomnick", "", R.string.command_description_nick_for_room, false), - ROOM_AVATAR("/roomavatar", "", R.string.command_description_room_avatar, true /* Since user has to know the mxc url */), - CHANGE_AVATAR_FOR_ROOM("/myroomavatar", "", R.string.command_description_avatar_for_room, true /* Since user has to know the mxc url */), - MARKDOWN("/markdown", "", R.string.command_description_markdown, false), - RAINBOW("/rainbow", "", R.string.command_description_rainbow, false), - RAINBOW_EMOTE("/rainbowme", "", R.string.command_description_rainbow_emote, false), - CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token, false), - SPOILER("/spoiler", "", R.string.command_description_spoiler, false), - SHRUG("/shrug", "", R.string.command_description_shrug, false), - LENNY("/lenny", "", R.string.command_description_lenny, false), - PLAIN("/plain", "", R.string.command_description_plain, false), - WHOIS("/whois", "", R.string.command_description_whois, false), - DISCARD_SESSION("/discardsession", "", R.string.command_description_discard_session, false), - CONFETTI("/confetti", "", R.string.command_confetti, false), - SNOWFALL("/snowfall", "", R.string.command_snow, false), - CREATE_SPACE("/createspace", " *", R.string.command_description_create_space, true), - ADD_TO_SPACE("/addToSpace", "spaceId", R.string.command_description_add_to_space, true), - JOIN_SPACE("/joinSpace", "spaceId", R.string.command_description_join_space, true), - LEAVE_ROOM("/leave", "", R.string.command_description_leave_room, true), - UPGRADE_ROOM("/upgraderoom", "newVersion", R.string.command_description_upgrade_room, true); +enum class Command(val command: String, + val aliases: Array?, + val parameters: String, + @StringRes val description: Int, + val isDevCommand: Boolean) { + EMOTE("/me", null, "", R.string.command_description_emote, false), + BAN_USER("/ban", null, " [reason]", R.string.command_description_ban_user, false), + UNBAN_USER("/unban", null, " [reason]", R.string.command_description_unban_user, false), + IGNORE_USER("/ignore", null, " [reason]", R.string.command_description_ignore_user, false), + UNIGNORE_USER("/unignore", null, "", R.string.command_description_unignore_user, false), + SET_USER_POWER_LEVEL("/op", null, " []", R.string.command_description_op_user, false), + RESET_USER_POWER_LEVEL("/deop", null, "", R.string.command_description_deop_user, false), + ROOM_NAME("/roomname", null, "", R.string.command_description_room_name, false), + INVITE("/invite", null, " [reason]", R.string.command_description_invite_user, false), + JOIN_ROOM("/join", arrayOf("/j", "/goto"), " [reason]", R.string.command_description_join_room, false), + PART("/part", null, "[]", R.string.command_description_part_room, false), + TOPIC("/topic", null, "", R.string.command_description_topic, false), + REMOVE_USER("/remove", arrayOf("/kick"), " [reason]", R.string.command_description_remove_user, false), + CHANGE_DISPLAY_NAME("/nick", null, "", R.string.command_description_nick, false), + CHANGE_DISPLAY_NAME_FOR_ROOM("/myroomnick", arrayOf("/roomnick"), "", R.string.command_description_nick_for_room, false), + ROOM_AVATAR("/roomavatar", null, "", R.string.command_description_room_avatar, true /* Since user has to know the mxc url */), + CHANGE_AVATAR_FOR_ROOM("/myroomavatar", null, "", R.string.command_description_avatar_for_room, true /* Since user has to know the mxc url */), + MARKDOWN("/markdown", null, "", R.string.command_description_markdown, false), + RAINBOW("/rainbow", null, "", R.string.command_description_rainbow, false), + RAINBOW_EMOTE("/rainbowme", null, "", R.string.command_description_rainbow_emote, false), + CLEAR_SCALAR_TOKEN("/clear_scalar_token", null, "", R.string.command_description_clear_scalar_token, false), + SPOILER("/spoiler", null, "", R.string.command_description_spoiler, false), + SHRUG("/shrug", null, "", R.string.command_description_shrug, false), + LENNY("/lenny", null, "", R.string.command_description_lenny, false), + PLAIN("/plain", null, "", R.string.command_description_plain, false), + WHOIS("/whois", null, "", R.string.command_description_whois, false), + DISCARD_SESSION("/discardsession", null, "", R.string.command_description_discard_session, false), + CONFETTI("/confetti", null, "", R.string.command_confetti, false), + SNOWFALL("/snowfall", null, "", R.string.command_snow, false), + CREATE_SPACE("/createspace", null, " *", R.string.command_description_create_space, true), + ADD_TO_SPACE("/addToSpace", null, "spaceId", R.string.command_description_add_to_space, true), + JOIN_SPACE("/joinSpace", null, "spaceId", R.string.command_description_join_space, true), + LEAVE_ROOM("/leave", null, "", R.string.command_description_leave_room, true), + UPGRADE_ROOM("/upgraderoom", null, "newVersion", R.string.command_description_upgrade_room, true); - val length - get() = command.length + 1 + val allAliases = arrayOf(command, *aliases.orEmpty()) + + fun matches(inputCommand: CharSequence) = allAliases.any { it.contentEquals(inputCommand, true) } + + fun startsWith(input: CharSequence) = + allAliases.any { it.startsWith(input, 1, true) } } diff --git a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt index 4b2a4aa28c..9d854fdbee 100644 --- a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt @@ -23,8 +23,9 @@ import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl import org.matrix.android.sdk.api.session.identity.ThreePid import timber.log.Timber +import javax.inject.Inject -object CommandParser { +class CommandParser @Inject constructor() { /** * Convert the text message into a Slash command. @@ -32,13 +33,11 @@ object CommandParser { * @param textMessage the text message * @return a parsed slash command (ok or error) */ - fun parseSplashCommand(textMessage: CharSequence): ParsedCommand { + fun parseSlashCommand(textMessage: CharSequence): ParsedCommand { // check if it has the Slash marker - if (!textMessage.startsWith("/")) { - return ParsedCommand.ErrorNotACommand + return if (!textMessage.startsWith("/")) { + ParsedCommand.ErrorNotACommand } else { - Timber.v("parseSplashCommand") - // "/" only if (textMessage.length == 1) { return ParsedCommand.ErrorEmptySlashCommand @@ -52,7 +51,7 @@ object CommandParser { val messageParts = try { textMessage.split("\\s+".toRegex()).dropLastWhile { it.isEmpty() } } catch (e: Exception) { - Timber.e(e, "## manageSplashCommand() : split failed") + Timber.e(e, "## parseSlashCommand() : split failed") null } @@ -61,35 +60,32 @@ object CommandParser { return ParsedCommand.ErrorEmptySlashCommand } - return when (val slashCommand = messageParts.first()) { - Command.PLAIN.command -> { - val text = textMessage.substring(Command.PLAIN.command.length).trim() + val slashCommand = messageParts.first() + val message = textMessage.substring(slashCommand.length).trim() - if (text.isNotEmpty()) { - ParsedCommand.SendPlainText(text) + when { + Command.PLAIN.matches(slashCommand) -> { + if (message.isNotEmpty()) { + ParsedCommand.SendPlainText(message = message) } else { ParsedCommand.ErrorSyntax(Command.PLAIN) } } - Command.CHANGE_DISPLAY_NAME.command -> { - val newDisplayName = textMessage.substring(Command.CHANGE_DISPLAY_NAME.command.length).trim() - - if (newDisplayName.isNotEmpty()) { - ParsedCommand.ChangeDisplayName(newDisplayName) + Command.CHANGE_DISPLAY_NAME.matches(slashCommand) -> { + if (message.isNotEmpty()) { + ParsedCommand.ChangeDisplayName(displayName = message) } else { ParsedCommand.ErrorSyntax(Command.CHANGE_DISPLAY_NAME) } } - Command.CHANGE_DISPLAY_NAME_FOR_ROOM.command -> { - val newDisplayName = textMessage.substring(Command.CHANGE_DISPLAY_NAME_FOR_ROOM.command.length).trim() - - if (newDisplayName.isNotEmpty()) { - ParsedCommand.ChangeDisplayNameForRoom(newDisplayName) + Command.CHANGE_DISPLAY_NAME_FOR_ROOM.matches(slashCommand) -> { + if (message.isNotEmpty()) { + ParsedCommand.ChangeDisplayNameForRoom(displayName = message) } else { ParsedCommand.ErrorSyntax(Command.CHANGE_DISPLAY_NAME_FOR_ROOM) } } - Command.ROOM_AVATAR.command -> { + Command.ROOM_AVATAR.matches(slashCommand) -> { if (messageParts.size == 2) { val url = messageParts[1] @@ -102,7 +98,7 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.ROOM_AVATAR) } } - Command.CHANGE_AVATAR_FOR_ROOM.command -> { + Command.CHANGE_AVATAR_FOR_ROOM.matches(slashCommand) -> { if (messageParts.size == 2) { val url = messageParts[1] @@ -115,40 +111,42 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.CHANGE_AVATAR_FOR_ROOM) } } - Command.TOPIC.command -> { - val newTopic = textMessage.substring(Command.TOPIC.command.length).trim() - - if (newTopic.isNotEmpty()) { - ParsedCommand.ChangeTopic(newTopic) + Command.TOPIC.matches(slashCommand) -> { + if (message.isNotEmpty()) { + ParsedCommand.ChangeTopic(topic = message) } else { ParsedCommand.ErrorSyntax(Command.TOPIC) } } - Command.EMOTE.command -> { - val message = textMessage.subSequence(Command.EMOTE.command.length, textMessage.length).trim() - - ParsedCommand.SendEmote(message) + Command.EMOTE.matches(slashCommand) -> { + if (message.isNotEmpty()) { + ParsedCommand.SendEmote(message) + } else { + ParsedCommand.ErrorSyntax(Command.EMOTE) + } } - Command.RAINBOW.command -> { - val message = textMessage.subSequence(Command.RAINBOW.command.length, textMessage.length).trim() - - ParsedCommand.SendRainbow(message) + Command.RAINBOW.matches(slashCommand) -> { + if (message.isNotEmpty()) { + ParsedCommand.SendRainbow(message) + } else { + ParsedCommand.ErrorSyntax(Command.RAINBOW) + } } - Command.RAINBOW_EMOTE.command -> { - val message = textMessage.subSequence(Command.RAINBOW_EMOTE.command.length, textMessage.length).trim() - - ParsedCommand.SendRainbowEmote(message) + Command.RAINBOW_EMOTE.matches(slashCommand) -> { + if (message.isNotEmpty()) { + ParsedCommand.SendRainbowEmote(message) + } else { + ParsedCommand.ErrorSyntax(Command.RAINBOW_EMOTE) + } } - Command.JOIN_ROOM.command -> { + Command.JOIN_ROOM.matches(slashCommand) -> { if (messageParts.size >= 2) { val roomAlias = messageParts[1] if (roomAlias.isNotEmpty()) { ParsedCommand.JoinRoom( roomAlias, - textMessage.substring(Command.JOIN_ROOM.length + roomAlias.length) - .trim() - .takeIf { it.isNotBlank() } + trimParts(textMessage, messageParts.take(2)) ) } else { ParsedCommand.ErrorSyntax(Command.JOIN_ROOM) @@ -157,23 +155,21 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.JOIN_ROOM) } } - Command.PART.command -> { + Command.PART.matches(slashCommand) -> { when (messageParts.size) { 1 -> ParsedCommand.PartRoom(null) 2 -> ParsedCommand.PartRoom(messageParts[1]) else -> ParsedCommand.ErrorSyntax(Command.PART) } } - Command.ROOM_NAME.command -> { - val newRoomName = textMessage.substring(Command.ROOM_NAME.command.length).trim() - - if (newRoomName.isNotEmpty()) { - ParsedCommand.ChangeRoomName(newRoomName) + Command.ROOM_NAME.matches(slashCommand) -> { + if (message.isNotEmpty()) { + ParsedCommand.ChangeRoomName(name = message) } else { ParsedCommand.ErrorSyntax(Command.ROOM_NAME) } } - Command.INVITE.command -> { + Command.INVITE.matches(slashCommand) -> { if (messageParts.size >= 2) { val userId = messageParts[1] @@ -181,9 +177,7 @@ object CommandParser { MatrixPatterns.isUserId(userId) -> { ParsedCommand.Invite( userId, - textMessage.substring(Command.INVITE.length + userId.length) - .trim() - .takeIf { it.isNotBlank() } + trimParts(textMessage, messageParts.take(2)) ) } userId.isEmail() -> { @@ -200,34 +194,30 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.INVITE) } } - Command.KICK_USER.command -> { + Command.REMOVE_USER.matches(slashCommand) -> { if (messageParts.size >= 2) { val userId = messageParts[1] if (MatrixPatterns.isUserId(userId)) { - ParsedCommand.KickUser( + ParsedCommand.RemoveUser( userId, - textMessage.substring(Command.KICK_USER.length + userId.length) - .trim() - .takeIf { it.isNotBlank() } + trimParts(textMessage, messageParts.take(2)) ) } else { - ParsedCommand.ErrorSyntax(Command.KICK_USER) + ParsedCommand.ErrorSyntax(Command.REMOVE_USER) } } else { - ParsedCommand.ErrorSyntax(Command.KICK_USER) + ParsedCommand.ErrorSyntax(Command.REMOVE_USER) } } - Command.BAN_USER.command -> { + Command.BAN_USER.matches(slashCommand) -> { if (messageParts.size >= 2) { val userId = messageParts[1] if (MatrixPatterns.isUserId(userId)) { ParsedCommand.BanUser( userId, - textMessage.substring(Command.BAN_USER.length + userId.length) - .trim() - .takeIf { it.isNotBlank() } + trimParts(textMessage, messageParts.take(2)) ) } else { ParsedCommand.ErrorSyntax(Command.BAN_USER) @@ -236,16 +226,14 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.BAN_USER) } } - Command.UNBAN_USER.command -> { + Command.UNBAN_USER.matches(slashCommand) -> { if (messageParts.size >= 2) { val userId = messageParts[1] if (MatrixPatterns.isUserId(userId)) { ParsedCommand.UnbanUser( userId, - textMessage.substring(Command.UNBAN_USER.length + userId.length) - .trim() - .takeIf { it.isNotBlank() } + trimParts(textMessage, messageParts.take(2)) ) } else { ParsedCommand.ErrorSyntax(Command.UNBAN_USER) @@ -254,7 +242,7 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.UNBAN_USER) } } - Command.IGNORE_USER.command -> { + Command.IGNORE_USER.matches(slashCommand) -> { if (messageParts.size == 2) { val userId = messageParts[1] @@ -267,7 +255,7 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.IGNORE_USER) } } - Command.UNIGNORE_USER.command -> { + Command.UNIGNORE_USER.matches(slashCommand) -> { if (messageParts.size == 2) { val userId = messageParts[1] @@ -280,7 +268,7 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.UNIGNORE_USER) } } - Command.SET_USER_POWER_LEVEL.command -> { + Command.SET_USER_POWER_LEVEL.matches(slashCommand) -> { if (messageParts.size == 3) { val userId = messageParts[1] if (MatrixPatterns.isUserId(userId)) { @@ -300,7 +288,7 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.SET_USER_POWER_LEVEL) } } - Command.RESET_USER_POWER_LEVEL.command -> { + Command.RESET_USER_POWER_LEVEL.matches(slashCommand) -> { if (messageParts.size == 2) { val userId = messageParts[1] @@ -313,7 +301,7 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.SET_USER_POWER_LEVEL) } } - Command.MARKDOWN.command -> { + Command.MARKDOWN.matches(slashCommand) -> { if (messageParts.size == 2) { when { "on".equals(messageParts[1], true) -> ParsedCommand.SetMarkdown(true) @@ -324,31 +312,34 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.MARKDOWN) } } - Command.CLEAR_SCALAR_TOKEN.command -> { + Command.CLEAR_SCALAR_TOKEN.matches(slashCommand) -> { if (messageParts.size == 1) { ParsedCommand.ClearScalarToken } else { ParsedCommand.ErrorSyntax(Command.CLEAR_SCALAR_TOKEN) } } - Command.SPOILER.command -> { - val message = textMessage.substring(Command.SPOILER.command.length).trim() - ParsedCommand.SendSpoiler(message) + Command.SPOILER.matches(slashCommand) -> { + if (message.isNotEmpty()) { + ParsedCommand.SendSpoiler(message) + } else { + ParsedCommand.ErrorSyntax(Command.SPOILER) + } } - Command.SHRUG.command -> { - val message = textMessage.substring(Command.SHRUG.command.length).trim() - + Command.SHRUG.matches(slashCommand) -> { ParsedCommand.SendShrug(message) } - Command.LENNY.command -> { - val message = textMessage.substring(Command.LENNY.command.length).trim() - + Command.LENNY.matches(slashCommand) -> { ParsedCommand.SendLenny(message) } - Command.DISCARD_SESSION.command -> { - ParsedCommand.DiscardSession + Command.DISCARD_SESSION.matches(slashCommand) -> { + if (messageParts.size == 1) { + ParsedCommand.DiscardSession + } else { + ParsedCommand.ErrorSyntax(Command.DISCARD_SESSION) + } } - Command.WHOIS.command -> { + Command.WHOIS.matches(slashCommand) -> { if (messageParts.size == 2) { val userId = messageParts[1] @@ -361,57 +352,57 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.WHOIS) } } - Command.CONFETTI.command -> { - val message = textMessage.substring(Command.CONFETTI.command.length).trim() + Command.CONFETTI.matches(slashCommand) -> { ParsedCommand.SendChatEffect(ChatEffect.CONFETTI, message) } - Command.SNOWFALL.command -> { - val message = textMessage.substring(Command.SNOWFALL.command.length).trim() + Command.SNOWFALL.matches(slashCommand) -> { ParsedCommand.SendChatEffect(ChatEffect.SNOWFALL, message) } - Command.CREATE_SPACE.command -> { - val rawCommand = textMessage.substring(Command.CREATE_SPACE.command.length).trim() - val split = rawCommand.split(" ").map { it.trim() } - if (split.isEmpty()) { - ParsedCommand.ErrorSyntax(Command.CREATE_SPACE) - } else { + Command.CREATE_SPACE.matches(slashCommand) -> { + if (messageParts.size >= 2) { ParsedCommand.CreateSpace( - split[0], - split.subList(1, split.size) + messageParts[1], + messageParts.drop(2) ) - } - } - Command.ADD_TO_SPACE.command -> { - val rawCommand = textMessage.substring(Command.ADD_TO_SPACE.command.length).trim() - ParsedCommand.AddToSpace( - rawCommand - ) - } - Command.JOIN_SPACE.command -> { - val spaceIdOrAlias = textMessage.substring(Command.JOIN_SPACE.command.length).trim() - ParsedCommand.JoinSpace( - spaceIdOrAlias - ) - } - Command.LEAVE_ROOM.command -> { - val spaceIdOrAlias = textMessage.substring(Command.LEAVE_ROOM.command.length).trim() - ParsedCommand.LeaveRoom( - spaceIdOrAlias - ) - } - Command.UPGRADE_ROOM.command -> { - val newVersion = textMessage.substring(Command.UPGRADE_ROOM.command.length).trim() - if (newVersion.isEmpty()) { - ParsedCommand.ErrorSyntax(Command.UPGRADE_ROOM) } else { - ParsedCommand.UpgradeRoom(newVersion) + ParsedCommand.ErrorSyntax(Command.CREATE_SPACE) } } - else -> { + Command.ADD_TO_SPACE.matches(slashCommand) -> { + if (messageParts.size == 1) { + ParsedCommand.AddToSpace(spaceId = message) + } else { + ParsedCommand.ErrorSyntax(Command.ADD_TO_SPACE) + } + } + Command.JOIN_SPACE.matches(slashCommand) -> { + if (messageParts.size == 1) { + ParsedCommand.JoinSpace(spaceIdOrAlias = message) + } else { + ParsedCommand.ErrorSyntax(Command.JOIN_SPACE) + } + } + Command.LEAVE_ROOM.matches(slashCommand) -> { + ParsedCommand.LeaveRoom(roomId = message) + } + Command.UPGRADE_ROOM.matches(slashCommand) -> { + if (message.isNotEmpty()) { + ParsedCommand.UpgradeRoom(newVersion = message) + } else { + ParsedCommand.ErrorSyntax(Command.UPGRADE_ROOM) + } + } + else -> { // Unknown command ParsedCommand.ErrorUnknownSlashCommand(slashCommand) } } } } + + private fun trimParts(message: CharSequence, messageParts: List): String? { + val partsSize = messageParts.sumOf { it.length } + val gapsNumber = messageParts.size - 1 + return message.substring(partsSize + gapsNumber).trim().takeIf { it.isNotEmpty() } + } } diff --git a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt index 4f8d19abb6..5f2e7f56a5 100644 --- a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt +++ b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt @@ -22,51 +22,51 @@ import org.matrix.android.sdk.api.session.identity.ThreePid /** * Represent a parsed command */ -sealed class ParsedCommand { +sealed interface ParsedCommand { // This is not a Slash command - object ErrorNotACommand : ParsedCommand() + object ErrorNotACommand : ParsedCommand - object ErrorEmptySlashCommand : ParsedCommand() + object ErrorEmptySlashCommand : ParsedCommand // Unknown/Unsupported slash command - class ErrorUnknownSlashCommand(val slashCommand: String) : ParsedCommand() + data class ErrorUnknownSlashCommand(val slashCommand: String) : ParsedCommand // A slash command is detected, but there is an error - class ErrorSyntax(val command: Command) : ParsedCommand() + data class ErrorSyntax(val command: Command) : ParsedCommand // Valid commands: - class SendPlainText(val message: CharSequence) : ParsedCommand() - class SendEmote(val message: CharSequence) : ParsedCommand() - class SendRainbow(val message: CharSequence) : ParsedCommand() - class SendRainbowEmote(val message: CharSequence) : ParsedCommand() - class BanUser(val userId: String, val reason: String?) : ParsedCommand() - class UnbanUser(val userId: String, val reason: String?) : ParsedCommand() - class IgnoreUser(val userId: String) : ParsedCommand() - class UnignoreUser(val userId: String) : ParsedCommand() - class SetUserPowerLevel(val userId: String, val powerLevel: Int?) : ParsedCommand() - class ChangeRoomName(val name: String) : ParsedCommand() - class Invite(val userId: String, val reason: String?) : ParsedCommand() - class Invite3Pid(val threePid: ThreePid) : ParsedCommand() - class JoinRoom(val roomAlias: String, val reason: String?) : ParsedCommand() - class PartRoom(val roomAlias: String?) : ParsedCommand() - class ChangeTopic(val topic: String) : ParsedCommand() - class KickUser(val userId: String, val reason: String?) : ParsedCommand() - class ChangeDisplayName(val displayName: String) : ParsedCommand() - class ChangeDisplayNameForRoom(val displayName: String) : ParsedCommand() - class ChangeRoomAvatar(val url: String) : ParsedCommand() - class ChangeAvatarForRoom(val url: String) : ParsedCommand() - class SetMarkdown(val enable: Boolean) : ParsedCommand() - object ClearScalarToken : ParsedCommand() - class SendSpoiler(val message: String) : ParsedCommand() - class SendShrug(val message: CharSequence) : ParsedCommand() - class SendLenny(val message: CharSequence) : ParsedCommand() - object DiscardSession : ParsedCommand() - class ShowUser(val userId: String) : ParsedCommand() - class SendChatEffect(val chatEffect: ChatEffect, val message: String) : ParsedCommand() - class CreateSpace(val name: String, val invitees: List) : ParsedCommand() - class AddToSpace(val spaceId: String) : ParsedCommand() - class JoinSpace(val spaceIdOrAlias: String) : ParsedCommand() - class LeaveRoom(val roomId: String) : ParsedCommand() - class UpgradeRoom(val newVersion: String) : ParsedCommand() + data class SendPlainText(val message: CharSequence) : ParsedCommand + data class SendEmote(val message: CharSequence) : ParsedCommand + data class SendRainbow(val message: CharSequence) : ParsedCommand + data class SendRainbowEmote(val message: CharSequence) : ParsedCommand + data class BanUser(val userId: String, val reason: String?) : ParsedCommand + data class UnbanUser(val userId: String, val reason: String?) : ParsedCommand + data class IgnoreUser(val userId: String) : ParsedCommand + data class UnignoreUser(val userId: String) : ParsedCommand + data class SetUserPowerLevel(val userId: String, val powerLevel: Int?) : ParsedCommand + data class ChangeRoomName(val name: String) : ParsedCommand + data class Invite(val userId: String, val reason: String?) : ParsedCommand + data class Invite3Pid(val threePid: ThreePid) : ParsedCommand + data class JoinRoom(val roomAlias: String, val reason: String?) : ParsedCommand + data class PartRoom(val roomAlias: String?) : ParsedCommand + data class ChangeTopic(val topic: String) : ParsedCommand + data class RemoveUser(val userId: String, val reason: String?) : ParsedCommand + data class ChangeDisplayName(val displayName: String) : ParsedCommand + data class ChangeDisplayNameForRoom(val displayName: String) : ParsedCommand + data class ChangeRoomAvatar(val url: String) : ParsedCommand + data class ChangeAvatarForRoom(val url: String) : ParsedCommand + data class SetMarkdown(val enable: Boolean) : ParsedCommand + object ClearScalarToken : ParsedCommand + data class SendSpoiler(val message: String) : ParsedCommand + data class SendShrug(val message: CharSequence) : ParsedCommand + data class SendLenny(val message: CharSequence) : ParsedCommand + object DiscardSession : ParsedCommand + data class ShowUser(val userId: String) : ParsedCommand + data class SendChatEffect(val chatEffect: ChatEffect, val message: String) : ParsedCommand + data class CreateSpace(val name: String, val invitees: List) : ParsedCommand + data class AddToSpace(val spaceId: String) : ParsedCommand + data class JoinSpace(val spaceIdOrAlias: String) : ParsedCommand + data class LeaveRoom(val roomId: String) : ParsedCommand + data class UpgradeRoom(val newVersion: String) : ParsedCommand } diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt index 5310fccb3a..ebd0089736 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt @@ -67,7 +67,8 @@ class ContactsBookFragment @Inject constructor( setupFilterView() setupConsentView() setupOnlyBoundContactsView() - setupCloseView() + setupToolbar(views.phoneBookToolbar) + .allowBack(useCross = true) contactsBookViewModel.observeViewEvents { when (it) { is ContactsBookViewEvents.Failure -> showFailure(it.throwable) @@ -119,12 +120,6 @@ class ContactsBookFragment @Inject constructor( views.phoneBookRecyclerView.configureWith(contactsBookController) } - private fun setupCloseView() { - views.phoneBookClose.debouncedClicks { - sharedActionViewModel.post(UserListSharedAction.GoBack) - } - } - override fun invalidate() = withState(contactsBookViewModel) { state -> views.phoneBookSearchForMatrixContacts.isVisible = state.filteredMappedContacts.isNotEmpty() && state.identityServerUrl != null && !state.userConsent views.phoneBookOnlyBoundContacts.isVisible = state.isBoundRetrieved diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt index f73799d0e9..0df9426852 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt @@ -42,6 +42,7 @@ import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.onPermissionDeniedSnackbar import im.vector.app.core.utils.registerForPermissionsResult +import im.vector.app.features.analytics.plan.Screen import im.vector.app.features.contactsbook.ContactsBookFragment import im.vector.app.features.userdirectory.UserListFragment import im.vector.app.features.userdirectory.UserListFragmentArgs @@ -63,6 +64,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + analyticsScreenName = Screen.ScreenName.StartChat views.toolbar.visibility = View.GONE sharedActionViewModel = viewModelProvider.get(UserListSharedActionViewModel::class.java) diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomByQrCodeFragment.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomByQrCodeFragment.kt index 96401604f3..766a6f5156 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomByQrCodeFragment.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomByQrCodeFragment.kt @@ -64,11 +64,8 @@ class CreateDirectRoomByQrCodeFragment @Inject constructor() : VectorBaseFragmen super.onViewCreated(view, savedInstanceState) setupToolbar(views.qrScannerToolbar) - - views.qrScannerClose.debouncedClicks { - requireActivity().onBackPressed() - } - views.qrScannerTitle.text = getString(R.string.add_by_qr_code) + .setTitle(R.string.add_by_qr_code) + .allowBack(useCross = true) } override fun onResume() { diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt index 010255256e..577572ef14 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt @@ -22,13 +22,13 @@ import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.epoxy.errorWithRetryItem import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.ItemStyle import im.vector.app.core.ui.list.genericItem import im.vector.app.features.settings.VectorPreferences +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState import org.matrix.android.sdk.internal.crypto.keysbackup.model.KeysBackupVersionTrust diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageKeyFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageKeyFragment.kt index c49291d6a2..8e7f11f0f5 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageKeyFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageKeyFragment.kt @@ -26,10 +26,10 @@ import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.activityViewModel import im.vector.app.R import im.vector.app.core.extensions.registerStartForActivityResult -import im.vector.app.core.flow.throttleFirst import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.startImportTextFromFileIntent import im.vector.app.databinding.FragmentSsssAccessFromKeyBinding +import im.vector.lib.core.utils.flow.throttleFirst import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.extensions.tryOrNull diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt index c93e562d77..70c1003773 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt @@ -25,10 +25,10 @@ import androidx.core.text.toSpannable import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.activityViewModel import im.vector.app.R -import im.vector.app.core.flow.throttleFirst import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider import im.vector.app.databinding.FragmentSsssAccessFromPassphraseBinding +import im.vector.lib.core.utils.flow.throttleFirst import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import reactivecircus.flowbinding.android.widget.editorActionEvents diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt index 940a4d9af3..8a211388ed 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt @@ -27,9 +27,9 @@ import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard -import im.vector.app.core.flow.throttleFirst import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentBootstrapEnterPassphraseBinding +import im.vector.lib.core.utils.flow.throttleFirst import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import reactivecircus.flowbinding.android.widget.editorActionEvents diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapEnterPassphraseFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapEnterPassphraseFragment.kt index 77fb5ab3a6..51430ba12e 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapEnterPassphraseFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapEnterPassphraseFragment.kt @@ -25,10 +25,10 @@ import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState import im.vector.app.R -import im.vector.app.core.flow.throttleFirst import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentBootstrapEnterPassphraseBinding import im.vector.app.features.settings.VectorLocale +import im.vector.lib.core.utils.flow.throttleFirst import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import reactivecircus.flowbinding.android.widget.editorActionEvents diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapMigrateBackupFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapMigrateBackupFragment.kt index 5d0f3bbeae..429d51857c 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapMigrateBackupFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapMigrateBackupFragment.kt @@ -33,12 +33,12 @@ import com.airbnb.mvrx.withState import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.registerStartForActivityResult -import im.vector.app.core.flow.throttleFirst import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider import im.vector.app.core.utils.colorizeMatchingText import im.vector.app.core.utils.startImportTextFromFileIntent import im.vector.app.databinding.FragmentBootstrapMigrateBackupBinding +import im.vector.lib.core.utils.flow.throttleFirst import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.extensions.tryOrNull diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationCancelController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationCancelController.kt index f0909dbc26..1adafe2760 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationCancelController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationCancelController.kt @@ -20,14 +20,14 @@ import androidx.core.text.toSpannable import com.airbnb.epoxy.EpoxyController import im.vector.app.R import im.vector.app.core.epoxy.bottomSheetDividerItem -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.colorizeMatchingText import im.vector.app.features.crypto.verification.VerificationBottomSheetViewState import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationActionItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import javax.inject.Inject class VerificationCancelController @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationNotMeController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationNotMeController.kt index 34f97d3cb2..a7c987f97e 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationNotMeController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationNotMeController.kt @@ -19,13 +19,13 @@ package im.vector.app.features.crypto.verification.cancel import com.airbnb.epoxy.EpoxyController import im.vector.app.R import im.vector.app.core.epoxy.bottomSheetDividerItem -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.features.crypto.verification.VerificationBottomSheetViewState import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationActionItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem import im.vector.app.features.html.EventHtmlRenderer +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import javax.inject.Inject class VerificationNotMeController @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodController.kt index 352c21a156..acc8cf61b9 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodController.kt @@ -19,12 +19,12 @@ package im.vector.app.features.crypto.verification.choose import com.airbnb.epoxy.EpoxyController import im.vector.app.R import im.vector.app.core.epoxy.bottomSheetDividerItem -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationActionItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationQrCodeItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import javax.inject.Inject class VerificationChooseMethodController @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionController.kt index 1314fd6fec..1d6dfbd947 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionController.kt @@ -19,13 +19,13 @@ package im.vector.app.features.crypto.verification.conclusion import com.airbnb.epoxy.EpoxyController import im.vector.app.R import im.vector.app.core.epoxy.bottomSheetDividerItem -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationActionItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationBigImageItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem import im.vector.app.features.html.EventHtmlRenderer +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeController.kt index 838f25ddfa..9f3e8ff690 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeController.kt @@ -21,7 +21,6 @@ import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Success import im.vector.app.R import im.vector.app.core.epoxy.bottomSheetDividerItem -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.epoxy.errorWithRetryItem import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.resources.ColorProvider @@ -32,6 +31,7 @@ import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationE import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationWaitingItem import im.vector.app.features.displayname.getBestName +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import javax.inject.Inject class VerificationEmojiCodeController @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationNoticeItem.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationNoticeItem.kt index f63459991b..ecd9989cdc 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationNoticeItem.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationNoticeItem.kt @@ -22,7 +22,7 @@ import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence /** * A action for bottom sheet. diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQRWaitingController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQRWaitingController.kt index cef5994c38..8de5f94ec9 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQRWaitingController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQRWaitingController.kt @@ -18,12 +18,12 @@ package im.vector.app.features.crypto.verification.qrconfirmation import com.airbnb.epoxy.EpoxyController import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationBigImageItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationWaitingItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherController.kt index bb21a6ccef..38f29622d0 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherController.kt @@ -19,7 +19,6 @@ package im.vector.app.features.crypto.verification.qrconfirmation import com.airbnb.epoxy.EpoxyController import im.vector.app.R import im.vector.app.core.epoxy.bottomSheetDividerItem -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.features.crypto.verification.VerificationBottomSheetViewState @@ -27,6 +26,7 @@ import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationA import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationBigImageItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem import im.vector.app.features.displayname.getBestName +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt index d40ee1f5c9..90997830a0 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt @@ -23,7 +23,6 @@ import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import im.vector.app.R import im.vector.app.core.epoxy.bottomSheetDividerItem -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.colorizeMatchingText @@ -33,6 +32,7 @@ import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationA import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationWaitingItem import im.vector.app.features.displayname.getBestName +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import javax.inject.Inject class VerificationRequestController @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolSendFormController.kt b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolSendFormController.kt index f0a6f40208..573ec0c085 100644 --- a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolSendFormController.kt +++ b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolSendFormController.kt @@ -18,11 +18,11 @@ package im.vector.app.features.devtools import com.airbnb.epoxy.TypedEpoxyController import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.features.form.formEditTextItem import im.vector.app.features.form.formMultiLineEditTextItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import javax.inject.Inject class RoomDevToolSendFormController @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolViewModel.kt b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolViewModel.kt index 04d90a63e7..c3524e2cdf 100644 --- a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolViewModel.kt @@ -174,8 +174,8 @@ class RoomDevToolViewModel @AssistedInject constructor( ?: throw IllegalArgumentException(stringProvider.getString(R.string.dev_tools_error_no_content)) room.sendStateEvent( - state.selectedEvent?.type ?: "", - state.selectedEvent?.stateKey, + state.selectedEvent?.type.orEmpty(), + state.selectedEvent?.stateKey.orEmpty(), json ) @@ -213,7 +213,7 @@ class RoomDevToolViewModel @AssistedInject constructor( if (isState) { room.sendStateEvent( eventType, - state.sendEventDraft.stateKey, + state.sendEventDraft.stateKey.orEmpty(), json ) } else { diff --git a/vector/src/main/java/im/vector/app/features/devtools/RoomStateListController.kt b/vector/src/main/java/im/vector/app/features/devtools/RoomStateListController.kt index ee5ae600ff..3f05db8ed5 100644 --- a/vector/src/main/java/im/vector/app/features/devtools/RoomStateListController.kt +++ b/vector/src/main/java/im/vector/app/features/devtools/RoomStateListController.kt @@ -18,11 +18,11 @@ package im.vector.app.features.devtools import com.airbnb.epoxy.TypedEpoxyController import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.epoxy.noResultItem import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.span import org.json.JSONObject import javax.inject.Inject @@ -37,7 +37,7 @@ class RoomStateListController @Inject constructor( override fun buildModels(data: RoomDevToolViewState?) { val host = this when (data?.displayMode) { - RoomDevToolViewState.Mode.StateEventList -> { + RoomDevToolViewState.Mode.StateEventList -> { val stateEventsGroups = data.stateEvents.invoke().orEmpty().groupBy { it.getClearType() } if (stateEventsGroups.isEmpty()) { diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 031802995d..d9719cb28f 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -24,6 +24,7 @@ import android.os.Bundle import android.os.Parcelable import android.view.Menu import android.view.MenuItem +import android.view.View import androidx.core.view.GravityCompat import androidx.core.view.isVisible import androidx.drawerlayout.widget.DrawerLayout @@ -32,7 +33,6 @@ import androidx.fragment.app.FragmentManager import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel -import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import im.vector.app.AppStateHandler @@ -42,13 +42,14 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.replaceFragment -import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.pushers.PushersManager import im.vector.app.databinding.ActivityHomeBinding import im.vector.app.features.MainActivity import im.vector.app.features.MainActivityArgs import im.vector.app.features.analytics.accountdata.AnalyticsAccountDataViewModel +import im.vector.app.features.analytics.plan.Screen +import im.vector.app.features.analytics.screen.ScreenEvent import im.vector.app.features.disclaimer.showDisclaimerDialog import im.vector.app.features.matrixto.MatrixToBottomSheet import im.vector.app.features.navigation.Navigator @@ -96,7 +97,6 @@ data class HomeActivityArgs( @AndroidEntryPoint class HomeActivity : VectorBaseActivity(), - ToolbarConfigurable, NavigationInterceptor, SpaceInviteBottomSheet.InteractionListener, MatrixToBottomSheet.InteractionListener { @@ -104,6 +104,7 @@ class HomeActivity : private lateinit var sharedActionViewModel: HomeSharedActionViewModel private val homeActivityViewModel: HomeActivityViewModel by viewModel() + @Suppress("UNUSED") private val analyticsAccountDataViewModel: AnalyticsAccountDataViewModel by viewModel() @Suppress("UNUSED") @@ -164,6 +165,16 @@ class HomeActivity : } private val drawerListener = object : DrawerLayout.SimpleDrawerListener() { + private var drawerScreenEvent: ScreenEvent? = null + override fun onDrawerOpened(drawerView: View) { + drawerScreenEvent = ScreenEvent(Screen.ScreenName.MobileSidebar) + } + + override fun onDrawerClosed(drawerView: View) { + drawerScreenEvent?.send(analyticsTracker) + drawerScreenEvent = null + } + override fun onDrawerStateChanged(newState: Int) { hideKeyboard() } @@ -175,6 +186,7 @@ class HomeActivity : override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + analyticsScreenName = Screen.ScreenName.Home supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false) FcmHelper.ensureFcmTokenIsRetrieved(this, pushManager, vectorPreferences.areNotificationEnabledForDevice()) sharedActionViewModel = viewModelProvider.get(HomeSharedActionViewModel::class.java) @@ -478,10 +490,6 @@ class HomeActivity : serverBackupStatusViewModel.refreshRemoteStateIfNeeded() } - override fun configure(toolbar: MaterialToolbar) { - configureToolbar(toolbar, false) - } - override fun getMenuRes() = R.menu.home override fun onPrepareOptionsMenu(menu: Menu): Boolean { diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt index 712055435f..a07409d063 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt @@ -33,7 +33,6 @@ import im.vector.app.R import im.vector.app.RoomGroupingMethod import im.vector.app.core.extensions.commitTransaction import im.vector.app.core.extensions.toMvRxBundle -import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider @@ -314,11 +313,9 @@ class HomeDetailFragment @Inject constructor( } private fun setupToolbar() { - val parentActivity = vectorBaseActivity - if (parentActivity is ToolbarConfigurable) { - parentActivity.configure(views.groupToolbar) - } - views.groupToolbar.title = "" + setupToolbar(views.groupToolbar) + .setTitle(null) + views.groupToolbarAvatarImageView.debouncedClicks { sharedActionViewModel.post(HomeActivitySharedAction.OpenDrawer) } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt index 5260330475..d7239373bd 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt @@ -27,7 +27,6 @@ import im.vector.app.RoomGroupingMethod import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.singletonEntryPoint -import im.vector.app.core.flow.throttleFirst import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.call.dialpad.DialPadLookup import im.vector.app.features.call.lookup.CallProtocolsChecker @@ -37,6 +36,7 @@ import im.vector.app.features.invite.AutoAcceptInvites import im.vector.app.features.invite.showInvites import im.vector.app.features.settings.VectorDataStore import im.vector.app.features.ui.UiStateRepository +import im.vector.lib.core.utils.flow.throttleFirst import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterIsInstance @@ -197,7 +197,7 @@ class HomeDetailViewModel @AssistedInject constructor( } private fun observeRoomGroupingMethod() { - appStateHandler.selectedRoomGroupingObservable + appStateHandler.selectedRoomGroupingFlow .setOnEach { copy( roomGroupingMethod = it.orNull() ?: RoomGroupingMethod.BySpace(null) @@ -206,7 +206,7 @@ class HomeDetailViewModel @AssistedInject constructor( } private fun observeRoomSummaries() { - appStateHandler.selectedRoomGroupingObservable.distinctUntilChanged().flatMapLatest { + appStateHandler.selectedRoomGroupingFlow.distinctUntilChanged().flatMapLatest { // we use it as a trigger to all changes in room, but do not really load // the actual models session.getPagedRoomSummariesLive( diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt index 3accc24740..9af06ef801 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt @@ -30,6 +30,7 @@ import im.vector.app.core.extensions.replaceChildFragment import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.startSharePlainTextIntent import im.vector.app.databinding.FragmentHomeDrawerBinding +import im.vector.app.features.analytics.plan.Screen import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorSettingsActivity import im.vector.app.features.spaces.SpaceListFragment @@ -97,6 +98,7 @@ class HomeDrawerFragment @Inject constructor( views.homeDrawerInviteFriendButton.debouncedClicks { session.permalinkService().createPermalink(sharedActionViewModel.session.myUserId)?.let { permalink -> + analyticsTracker.screen(Screen(screenName = Screen.ScreenName.MobileInviteFriends)) val text = getString(R.string.invite_friends_text, permalink) startSharePlainTextIntent( diff --git a/vector/src/main/java/im/vector/app/features/home/PromoteRestrictedViewModel.kt b/vector/src/main/java/im/vector/app/features/home/PromoteRestrictedViewModel.kt index 77ee23f732..5c66e7c52d 100644 --- a/vector/src/main/java/im/vector/app/features/home/PromoteRestrictedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/PromoteRestrictedViewModel.kt @@ -50,7 +50,7 @@ class PromoteRestrictedViewModel @AssistedInject constructor( ) : VectorViewModel(initialState) { init { - appStateHandler.selectedRoomGroupingObservable.distinctUntilChanged().execute { state -> + appStateHandler.selectedRoomGroupingFlow.distinctUntilChanged().execute { state -> val groupingMethod = state.invoke()?.orNull() val isSpaceMode = groupingMethod is RoomGroupingMethod.BySpace val currentSpace = (groupingMethod as? RoomGroupingMethod.BySpace)?.spaceSummary diff --git a/vector/src/main/java/im/vector/app/features/home/UnreadMessagesSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/home/UnreadMessagesSharedViewModel.kt index 6c0ae71cfa..409eb0b845 100644 --- a/vector/src/main/java/im/vector/app/features/home/UnreadMessagesSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/UnreadMessagesSharedViewModel.kt @@ -26,12 +26,12 @@ import im.vector.app.AppStateHandler import im.vector.app.RoomGroupingMethod import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.flow.throttleFirst import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.invite.AutoAcceptInvites import im.vector.app.features.settings.VectorPreferences +import im.vector.lib.core.utils.flow.throttleFirst import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -107,8 +107,8 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initia } combine( - appStateHandler.selectedRoomGroupingObservable.distinctUntilChanged(), - appStateHandler.selectedRoomGroupingObservable.flatMapLatest { + appStateHandler.selectedRoomGroupingFlow.distinctUntilChanged(), + appStateHandler.selectedRoomGroupingFlow.flatMapLatest { session.getPagedRoomSummariesLive( roomSummaryQueryParams { this.memberships = Membership.activeMemberships() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index f866bb328d..58e36d2303 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -20,6 +20,7 @@ import android.net.Uri import android.view.View import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.features.call.conference.ConferenceEvent +import im.vector.app.features.location.LocationData import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent @@ -111,4 +112,7 @@ sealed class RoomDetailAction : VectorViewModelAction { // Poll data class EndPoll(val eventId: String) : RoomDetailAction() + + // Location + data class ShowLocation(val locationData: LocationData, val userId: String) : RoomDetailAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt index 05dbd724d1..9a7b8e64f7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt @@ -19,6 +19,7 @@ package im.vector.app.features.home.room.detail import android.content.Context import android.content.Intent import android.os.Bundle +import android.view.View import android.widget.Toast import androidx.core.view.GravityCompat import androidx.drawerlayout.widget.DrawerLayout @@ -27,15 +28,15 @@ import androidx.fragment.app.FragmentManager import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel -import com.google.android.material.appbar.MaterialToolbar import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.endKeepScreenOn import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.keepScreenOn import im.vector.app.core.extensions.replaceFragment -import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityRoomDetailBinding +import im.vector.app.features.analytics.plan.Screen +import im.vector.app.features.analytics.screen.ScreenEvent import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsFragment import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker import im.vector.app.features.matrixto.MatrixToBottomSheet @@ -50,7 +51,6 @@ import javax.inject.Inject @AndroidEntryPoint class RoomDetailActivity : VectorBaseActivity(), - ToolbarConfigurable, MatrixToBottomSheet.InteractionListener { override fun getBinding(): ActivityRoomDetailBinding { @@ -156,11 +156,17 @@ class RoomDetailActivity : super.onDestroy() } - override fun configure(toolbar: MaterialToolbar) { - configureToolbar(toolbar) - } - private val drawerListener = object : DrawerLayout.SimpleDrawerListener() { + private var drawerScreenEvent: ScreenEvent? = null + override fun onDrawerOpened(drawerView: View) { + drawerScreenEvent = ScreenEvent(Screen.ScreenName.MobileBreadcrumbs) + } + + override fun onDrawerClosed(drawerView: View) { + drawerScreenEvent?.send(analyticsTracker) + drawerScreenEvent = null + } + override fun onDrawerStateChanged(newState: Int) { hideKeyboard() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 5731b36ce0..9926ecad24 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -106,6 +106,7 @@ import im.vector.app.core.utils.createUIHandler import im.vector.app.core.utils.isValidUrl import im.vector.app.core.utils.onPermissionDeniedDialog import im.vector.app.core.utils.onPermissionDeniedSnackbar +import im.vector.app.core.utils.openLocation import im.vector.app.core.utils.openUrlInExternalBrowser import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.core.utils.safeStartActivity @@ -116,6 +117,8 @@ 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.FragmentRoomDetailBinding +import im.vector.app.features.analytics.plan.Click +import im.vector.app.features.analytics.plan.Screen import im.vector.app.features.attachments.AttachmentTypeSelectorView import im.vector.app.features.attachments.AttachmentsHelper import im.vector.app.features.attachments.ContactAttachment @@ -168,12 +171,15 @@ import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.PillImageSpan import im.vector.app.features.html.PillsPostProcessor import im.vector.app.features.invite.VectorInviteView +import im.vector.app.features.location.LocationData +import im.vector.app.features.location.LocationSharingMode import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.VideoContentRenderer import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.permalink.NavigationInterceptor import im.vector.app.features.permalink.PermalinkHandler +import im.vector.app.features.poll.create.PollMode import im.vector.app.features.reactions.EmojiReactionPickerActivity import im.vector.app.features.roomprofile.RoomProfileActivity import im.vector.app.features.session.coroutineScope @@ -201,6 +207,7 @@ import org.billcarsonfr.jsonviewer.JSonViewerDialog import org.commonmark.parser.Parser import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.content.ContentAttachmentData +import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -208,6 +215,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageFormat import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent +import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent @@ -237,7 +245,8 @@ data class RoomDetailArgs( val roomId: String, val eventId: String? = null, val sharedData: SharedData? = null, - val openShareSpaceForId: String? = null + val openShareSpaceForId: String? = null, + val switchToParentSpace: Boolean = false ) : Parcelable class RoomDetailFragment @Inject constructor( @@ -335,6 +344,7 @@ class RoomDetailFragment @Inject constructor( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + analyticsScreenName = Screen.ScreenName.Room setFragmentResultListener(MigrateRoomBottomSheet.REQUEST_KEY) { _, bundle -> bundle.getString(MigrateRoomBottomSheet.BUNDLE_KEY_REPLACEMENT_ROOM)?.let { replacementRoomId -> roomDetailViewModel.handle(RoomDetailAction.RoomUpgradeSuccess(replacementRoomId)) @@ -361,6 +371,7 @@ class RoomDetailFragment @Inject constructor( keyboardStateUtils = KeyboardStateUtils(requireActivity()) lazyLoadedViews.bind(views) setupToolbar(views.roomToolbar) + .allowBack() setupRecyclerView() setupComposer() setupNotificationView() @@ -470,6 +481,7 @@ class RoomDetailFragment @Inject constructor( RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects() is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it) RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement() + is RoomDetailViewEvents.ShowLocation -> handleShowLocationPreview(it) }.exhaustive } @@ -601,6 +613,17 @@ class RoomDetailFragment @Inject constructor( } } + private fun handleShowLocationPreview(viewEvent: RoomDetailViewEvents.ShowLocation) { + navigator + .openLocationSharing( + context = requireContext(), + roomId = roomDetailArgs.roomId, + mode = LocationSharingMode.PREVIEW, + initialLocationData = viewEvent.locationData, + locationOwnerId = viewEvent.userId + ) + } + private fun requestNativeWidgetPermission(it: RoomDetailViewEvents.RequestNativeWidgetPermission) { val tag = RoomWidgetPermissionBottomSheet::class.java.name val dFrag = childFragmentManager.findFragmentByTag(tag) as? RoomWidgetPermissionBottomSheet @@ -675,7 +698,7 @@ class RoomDetailFragment @Inject constructor( */ private fun EmojiPopup.Builder.setOnEmojiPopupDismissListenerLifecycleAware(action: () -> Unit): EmojiPopup.Builder { return setOnEmojiPopupDismissListener { - if (lifecycle.currentState == Lifecycle.State.STARTED) { + if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { action() } } @@ -1366,7 +1389,7 @@ class RoomDetailFragment @Inject constructor( override fun onAddAttachment() { if (!::attachmentTypeSelector.isInitialized) { attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@RoomDetailFragment) - attachmentTypeSelector.setAttachmentVisibility(AttachmentTypeSelectorView.Type.POLL, vectorPreferences.labsEnablePolls()) + attachmentTypeSelector.setAttachmentVisibility(AttachmentTypeSelectorView.Type.LOCATION, vectorPreferences.isLocationSharingEnabled()) } attachmentTypeSelector.show(views.composerLayout.views.attachmentButton) } @@ -1395,6 +1418,7 @@ class RoomDetailFragment @Inject constructor( return } if (text.isNotBlank()) { + analyticsTracker.capture(Click(name = Click.Name.SendMessageButton)) // We collapse ASAP, if not there will be a slight annoying delay views.composerLayout.collapse(true) lockSendButton = true @@ -1509,7 +1533,7 @@ class RoomDetailFragment @Inject constructor( views.roomToolbarSubtitleView.apply { setTextOrHide(subtitle) if (typingMessage.isNullOrBlank()) { - setTextColor(colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) + setTextColor(colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary)) setTypeface(null, Typeface.NORMAL) } else { setTextColor(colorProvider.getColorFromAttribute(R.attr.colorPrimary)) @@ -1913,16 +1937,22 @@ class RoomDetailFragment @Inject constructor( } private fun onShareActionClicked(action: EventSharedAction.Share) { - if (action.messageContent is MessageTextContent) { - shareText(requireContext(), action.messageContent.body) - } else if (action.messageContent is MessageWithAttachmentContent) { - lifecycleScope.launch { - val result = runCatching { session.fileService().downloadFile(messageContent = action.messageContent) } - if (!isAdded) return@launch - result.fold( - { shareMedia(requireContext(), it, getMimeTypeFromUri(requireContext(), it.toUri())) }, - { showErrorInSnackbar(it) } - ) + when (action.messageContent) { + is MessageTextContent -> shareText(requireContext(), action.messageContent.body) + is MessageLocationContent -> { + LocationData.create(action.messageContent.getUri())?.let { + openLocation(requireActivity(), it.latitude, it.longitude) + } + } + is MessageWithAttachmentContent -> { + lifecycleScope.launch { + val result = runCatching { session.fileService().downloadFile(messageContent = action.messageContent) } + if (!isAdded) return@launch + result.fold( + { shareMedia(requireContext(), it, getMimeTypeFromUri(requireContext(), it.toUri())) }, + { showErrorInSnackbar(it) } + ) + } } } } @@ -2014,7 +2044,9 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.handle(RoomDetailAction.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add)) } is EventSharedAction.Edit -> { - if (withState(messageComposerViewModel) { it.isVoiceMessageIdle }) { + if (action.eventType == EventType.POLL_START) { + navigator.openCreatePoll(requireContext(), roomDetailArgs.roomId, action.eventId, PollMode.EDIT) + } else if (withState(messageComposerViewModel) { it.isVoiceMessageIdle }) { messageComposerViewModel.handle(MessageComposerAction.EnterEditMode(action.eventId, views.composerLayout.text.toString())) } else { requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit) @@ -2113,7 +2145,7 @@ class RoomDetailFragment @Inject constructor( userId == session.myUserId) { // Empty composer, current user: start an emote views.composerLayout.views.composerEditText.setText(Command.EMOTE.command + " ") - views.composerLayout.views.composerEditText.setSelection(Command.EMOTE.length) + views.composerLayout.views.composerEditText.setSelection(Command.EMOTE.command.length + 1) } else { val roomMember = roomDetailViewModel.getMember(userId) // TODO move logic outside of fragment @@ -2216,17 +2248,27 @@ class RoomDetailFragment @Inject constructor( private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) { when (type) { - AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera( + AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera( activity = requireActivity(), vectorPreferences = vectorPreferences, cameraActivityResultLauncher = attachmentCameraActivityResultLauncher, cameraVideoActivityResultLauncher = attachmentCameraVideoActivityResultLauncher ) - AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher) - AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(attachmentMediaActivityResultLauncher) - AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact(attachmentContactActivityResultLauncher) - AttachmentTypeSelectorView.Type.STICKER -> roomDetailViewModel.handle(RoomDetailAction.SelectStickerAttachment) - AttachmentTypeSelectorView.Type.POLL -> navigator.openCreatePoll(requireContext(), roomDetailArgs.roomId) + AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher) + AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(attachmentMediaActivityResultLauncher) + AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact(attachmentContactActivityResultLauncher) + AttachmentTypeSelectorView.Type.STICKER -> roomDetailViewModel.handle(RoomDetailAction.SelectStickerAttachment) + AttachmentTypeSelectorView.Type.POLL -> navigator.openCreatePoll(requireContext(), roomDetailArgs.roomId, null, PollMode.CREATE) + AttachmentTypeSelectorView.Type.LOCATION -> { + navigator + .openLocationSharing( + context = requireContext(), + roomId = roomDetailArgs.roomId, + mode = LocationSharingMode.STATIC_SHARING, + initialLocationData = null, + locationOwnerId = session.myUserId + ) + } }.exhaustive } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt index 86240a5ffe..b0921e01f9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt @@ -20,6 +20,7 @@ import android.net.Uri import android.view.View import im.vector.app.core.platform.VectorViewEvents import im.vector.app.features.call.webrtc.WebRtcCall +import im.vector.app.features.location.LocationData import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode @@ -82,4 +83,6 @@ sealed class RoomDetailViewEvents : VectorViewEvents { data class StartChatEffect(val type: ChatEffect) : RoomDetailViewEvents() object StopChatEffects : RoomDetailViewEvents() object RoomReplacementStarted : RoomDetailViewEvents() + + data class ShowLocation(val locationData: LocationData, val userId: String) : RoomDetailViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index aba636309f..6e14b0fc76 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -28,17 +28,19 @@ 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.BuildConfig import im.vector.app.R import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive -import im.vector.app.core.flow.chunk import im.vector.app.core.mvrx.runCatchingToAsync import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.BehaviorDataSource +import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.DecryptionFailureTracker +import im.vector.app.features.analytics.extensions.toAnalyticsJoinedRoom import im.vector.app.features.call.conference.ConferenceEvent import im.vector.app.features.call.conference.JitsiActiveConferenceHolder import im.vector.app.features.call.conference.JitsiService @@ -51,10 +53,13 @@ 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.LocationData import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorDataStore import im.vector.app.features.settings.VectorPreferences +import im.vector.app.space +import im.vector.lib.core.utils.flow.chunk import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.combine @@ -112,9 +117,11 @@ class RoomDetailViewModel @AssistedInject constructor( private val chatEffectManager: ChatEffectManager, private val directRoomHelper: DirectRoomHelper, private val jitsiService: JitsiService, + private val analyticsTracker: AnalyticsTracker, private val activeConferenceHolder: JitsiActiveConferenceHolder, private val decryptionFailureTracker: DecryptionFailureTracker, - timelineFactory: TimelineFactory + timelineFactory: TimelineFactory, + appStateHandler: AppStateHandler ) : VectorViewModel(initialState), Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener { @@ -179,6 +186,24 @@ class RoomDetailViewModel @AssistedInject constructor( if (OutboundSessionKeySharingStrategy.WhenEnteringRoom == BuildConfig.outboundSessionKeySharingStrategy && room.isEncrypted()) { prepareForEncryption() } + + 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.getCurrentRoomGroupingMethod()?.space().let { currentSpace -> + val currentRoomSummary = room.roomSummary() ?: return@let + // nothing we are good + if (currentSpace == null || !currentRoomSummary.flattenParentIds.contains(currentSpace.roomId)) { + // take first one or switch to home + appStateHandler.setCurrentSpace( + currentRoomSummary + .flattenParentIds.firstOrNull { it.isNotBlank() }, + // force persist, because if not on resume the AppStateHandler will resume + // the current space from what was persisted on enter background + persistNow = true) + } + } + } } private fun observeDataStore() { @@ -360,9 +385,14 @@ class RoomDetailViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.OpenRoom(action.replacementRoomId, closeCurrentRoom = true)) } is RoomDetailAction.EndPoll -> handleEndPoll(action.eventId) + is RoomDetailAction.ShowLocation -> handleShowLocation(action.locationData, action.userId) }.exhaustive } + private fun handleShowLocation(locationData: LocationData, userId: String) { + _viewEvents.post(RoomDetailViewEvents.ShowLocation(locationData, userId)) + } + private fun handleJitsiCallJoinStatus(action: RoomDetailAction.UpdateJoinJitsiCallStatus) = withState { state -> if (state.jitsiState.confId == null) { // If jitsi widget is removed while on the call @@ -709,7 +739,10 @@ class RoomDetailViewModel @AssistedInject constructor( private fun handleAcceptInvite() { viewModelScope.launch { - tryOrNull { room.join() } + tryOrNull { + room.join() + analyticsTracker.capture(room.roomSummary().toAnalyticsJoinedRoom()) + } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt index e35d601887..d963030013 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt @@ -66,14 +66,16 @@ data class RoomDetailViewState( val isAllowedToStartWebRTCCall: Boolean = true, val isAllowedToSetupEncryption: Boolean = true, val hasFailedSending: Boolean = false, - val jitsiState: JitsiState = JitsiState() + val jitsiState: JitsiState = JitsiState(), + val switchToParentSpace: Boolean = false ) : MavericksState { constructor(args: RoomDetailArgs) : this( roomId = args.roomId, eventId = args.eventId, // Also highlight the target event, if any - highlightedEventId = args.eventId + highlightedEventId = args.eventId, + switchToParentSpace = args.switchToParentSpace ) fun isWebRTCCallOptionAvailable() = (asyncRoomSummary.invoke()?.joinedMembersCount ?: 0) <= 2 diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt index 4dc4e638ba..a2d9b50edd 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt @@ -26,6 +26,8 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider +import im.vector.app.features.analytics.AnalyticsTracker +import im.vector.app.features.analytics.extensions.toAnalyticsJoinedRoom import im.vector.app.features.attachments.toContentAttachmentData import im.vector.app.features.command.CommandParser import im.vector.app.features.command.ParsedCommand @@ -66,8 +68,10 @@ class MessageComposerViewModel @AssistedInject constructor( private val session: Session, private val stringProvider: StringProvider, private val vectorPreferences: VectorPreferences, + private val commandParser: CommandParser, private val rainbowGenerator: RainbowGenerator, private val voiceMessageHelper: VoiceMessageHelper, + private val analyticsTracker: AnalyticsTracker, private val voicePlayerHelper: VoicePlayerHelper ) : VectorViewModel(initialState) { @@ -183,7 +187,7 @@ class MessageComposerViewModel @AssistedInject constructor( withState { state -> when (state.sendMode) { is SendMode.Regular -> { - when (val slashCommandResult = CommandParser.parseSplashCommand(action.text)) { + when (val slashCommandResult = commandParser.parseSlashCommand(action.text)) { is ParsedCommand.ErrorNotACommand -> { // Send the text message to the room room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown) @@ -239,8 +243,8 @@ class MessageComposerViewModel @AssistedInject constructor( is ParsedCommand.UnignoreUser -> { handleUnignoreSlashCommand(slashCommandResult) } - is ParsedCommand.KickUser -> { - handleKickSlashCommand(slashCommandResult) + is ParsedCommand.RemoveUser -> { + handleRemoveSlashCommand(slashCommandResult) } is ParsedCommand.JoinRoom -> { handleJoinToAnotherRoomSlashCommand(slashCommandResult) @@ -520,6 +524,7 @@ class MessageComposerViewModel @AssistedInject constructor( return@launch } session.getRoomSummary(command.roomAlias) + ?.also { analyticsTracker.capture(it.toAnalyticsJoinedRoom()) } ?.roomId ?.let { _viewEvents.post(MessageComposerViewEvents.JoinRoomCommandSuccess(it)) @@ -574,7 +579,7 @@ class MessageComposerViewModel @AssistedInject constructor( ?: return launchSlashCommandFlowSuspendable { - room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, null, newPowerLevelsContent) + room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, stateKey = "", newPowerLevelsContent) } } @@ -598,9 +603,9 @@ class MessageComposerViewModel @AssistedInject constructor( } } - private fun handleKickSlashCommand(kick: ParsedCommand.KickUser) { + private fun handleRemoveSlashCommand(removeUser: ParsedCommand.RemoveUser) { launchSlashCommandFlowSuspendable { - room.kick(kick.userId, kick.reason) + room.remove(removeUser.userId, removeUser.reason) } } @@ -641,7 +646,7 @@ class MessageComposerViewModel @AssistedInject constructor( private fun handleChangeRoomAvatarSlashCommand(changeAvatar: ParsedCommand.ChangeRoomAvatar) { launchSlashCommandFlowSuspendable { - room.sendStateEvent(EventType.STATE_ROOM_AVATAR, null, RoomAvatarContent(changeAvatar.url).toContent()) + room.sendStateEvent(EventType.STATE_ROOM_AVATAR, stateKey = "", RoomAvatarContent(changeAvatar.url).toContent()) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt index b7e584b4c0..735d356476 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt @@ -21,11 +21,11 @@ import android.media.AudioAttributes import android.media.MediaPlayer import androidx.core.content.FileProvider import im.vector.app.BuildConfig -import im.vector.app.core.utils.CountUpTimer import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker import im.vector.app.features.voice.VoiceFailure import im.vector.app.features.voice.VoiceRecorder import im.vector.app.features.voice.VoiceRecorderProvider +import im.vector.lib.core.utils.timer.CountUpTimer import im.vector.lib.multipicker.entity.MultiPickerAudioType import im.vector.lib.multipicker.utils.toMultiPickerAudioType import org.matrix.android.sdk.api.extensions.orFalse diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt index 312963771d..9a643796a9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt @@ -26,10 +26,10 @@ import im.vector.app.R import im.vector.app.core.extensions.exhaustive import im.vector.app.core.hardware.vibrate import im.vector.app.core.time.Clock -import im.vector.app.core.utils.CountUpTimer import im.vector.app.core.utils.DimensionConverter import im.vector.app.databinding.ViewVoiceMessageRecorderBinding import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker +import im.vector.lib.core.utils.timer.CountUpTimer import javax.inject.Inject import kotlin.math.floor diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchActivity.kt index bc1dc088df..eed596cda0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchActivity.kt @@ -40,7 +40,8 @@ class SearchActivity : VectorBaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - configureToolbar(views.searchToolbar) + setupToolbar(views.searchToolbar) + .allowBack() } override fun initUiAndData() { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt index 4c5a52864d..ccf83011a8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt @@ -26,12 +26,12 @@ import com.airbnb.epoxy.VisibilityState import im.vector.app.R import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.VectorDateFormatter -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.epoxy.noResultItem import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.GenericHeaderItem_ import im.vector.app.features.home.AvatarRenderer +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt index 9d146792d9..95dea2b8d2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt @@ -24,11 +24,11 @@ import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence import org.matrix.android.sdk.api.util.MatrixItem @EpoxyModelClass(layout = R.layout.item_search_result) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt index 39e04e8ae4..d7a57e6577 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt @@ -39,7 +39,7 @@ sealed class EventSharedAction(@StringRes val titleRes: Int, data class Copy(val content: String) : EventSharedAction(R.string.action_copy, R.drawable.ic_copy) - data class Edit(val eventId: String) : + data class Edit(val eventId: String, val eventType: String) : EventSharedAction(R.string.edit, R.drawable.ic_edit) data class Quote(val eventId: String) : diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt index 15f33db6ff..1ff9679479 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt @@ -27,21 +27,25 @@ import im.vector.app.core.epoxy.bottomsheet.bottomSheetActionItem import im.vector.app.core.epoxy.bottomsheet.bottomSheetMessagePreviewItem import im.vector.app.core.epoxy.bottomsheet.bottomSheetQuickReactionsItem import im.vector.app.core.epoxy.bottomsheet.bottomSheetSendStateItem -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.DimensionConverter import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.format.EventDetailsFormatter +import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider import im.vector.app.features.home.room.detail.timeline.image.buildImageContentRendererData import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod import im.vector.app.features.home.room.detail.timeline.tools.linkify import im.vector.app.features.html.SpanUtils +import im.vector.app.features.location.LocationData import im.vector.app.features.media.ImageContentRenderer +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent import org.matrix.android.sdk.api.session.room.send.SendState import javax.inject.Inject @@ -57,7 +61,8 @@ class MessageActionsEpoxyController @Inject constructor( private val errorFormatter: ErrorFormatter, private val spanUtils: SpanUtils, private val eventDetailsFormatter: EventDetailsFormatter, - private val dateFormatter: VectorDateFormatter + private val dateFormatter: VectorDateFormatter, + private val locationPinProvider: LocationPinProvider ) : TypedEpoxyController() { var listener: MessageActionsEpoxyControllerListener? = null @@ -69,6 +74,9 @@ class MessageActionsEpoxyController @Inject constructor( val formattedDate = dateFormatter.format(date, DateFormatKind.MESSAGE_DETAIL) val body = state.messageBody.linkify(host.listener) val bindingOptions = spanUtils.getBindingOptions(body) + val locationData = state.timelineEvent()?.root?.getClearContent()?.toModel(catchError = true)?.let { + LocationData.create(it.getUri()) + } bottomSheetMessagePreviewItem { id("preview") avatarRenderer(host.avatarRenderer) @@ -81,6 +89,8 @@ class MessageActionsEpoxyController @Inject constructor( body(body.toEpoxyCharSequence()) bodyDetails(host.eventDetailsFormatter.format(state.timelineEvent()?.root)?.toEpoxyCharSequence()) time(formattedDate) + locationData(locationData) + locationPinProvider(host.locationPinProvider) } // Send state diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index ff7d555ee3..ea54d91a78 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -284,7 +284,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } add(EventSharedAction.Remove(eventId)) if (canEdit(timelineEvent, session.myUserId, actionPermissions)) { - add(EventSharedAction.Edit(eventId)) + add(EventSharedAction.Edit(eventId, timelineEvent.root.getClearType())) } if (canCopy(msgType)) { // TODO copy images? html? see ClipBoard @@ -329,7 +329,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } if (canEdit(timelineEvent, session.myUserId, actionPermissions)) { - add(EventSharedAction.Edit(eventId)) + add(EventSharedAction.Edit(eventId, timelineEvent.root.getClearType())) } if (canRedact(timelineEvent, actionPermissions)) { @@ -424,8 +424,9 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted MessageType.MSGTYPE_VIDEO, MessageType.MSGTYPE_AUDIO, MessageType.MSGTYPE_FILE, - MessageType.MSGTYPE_POLL_START -> true - else -> false + MessageType.MSGTYPE_POLL_START, + MessageType.MSGTYPE_LOCATION -> true + else -> false } } @@ -466,14 +467,15 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } private fun canEdit(event: TimelineEvent, myUserId: String, actionPermissions: ActionPermissions): Boolean { - // Only event of type EventType.MESSAGE are supported for the moment - if (event.root.getClearType() != EventType.MESSAGE) return false + // Only event of type EventType.MESSAGE and EventType.POLL_START are supported for the moment + if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.POLL_START)) return false if (!actionPermissions.canSendMessage) return false // TODO if user is admin or moderator val messageContent = event.root.getClearContent().toModel() return event.root.senderId == myUserId && ( messageContent?.msgType == MessageType.MSGTYPE_TEXT || - messageContent?.msgType == MessageType.MSGTYPE_EMOTE + messageContent?.msgType == MessageType.MSGTYPE_EMOTE || + canEditPoll(event) ) } @@ -516,4 +518,10 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted canRedact(event, actionPermissions) && event.annotations?.pollResponseSummary?.closedTime == null } + + private fun canEditPoll(event: TimelineEvent): Boolean { + return event.root.getClearType() == EventType.POLL_START && + event.annotations?.pollResponseSummary?.closedTime == null && + event.annotations?.pollResponseSummary?.aggregatedContent?.totalVotes ?: 0 == 0 + } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt index 19b6b8c71a..1dad6cc4a7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt @@ -23,7 +23,6 @@ import com.airbnb.mvrx.Success import im.vector.app.R import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.VectorDateFormatter -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem @@ -31,6 +30,7 @@ import im.vector.app.core.ui.list.genericHeaderItem import im.vector.app.core.ui.list.genericItem import im.vector.app.core.ui.list.genericLoaderItem import im.vector.app.features.html.EventHtmlRenderer +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.span import name.fraser.neil.plaintext.diff_match_patch import org.matrix.android.sdk.api.session.events.model.Event diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt index 89c9c51f0c..4f8a36e234 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt @@ -18,7 +18,6 @@ package im.vector.app.features.home.room.detail.timeline.factory import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider import im.vector.app.core.resources.StringProvider @@ -28,6 +27,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttrib import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem_ import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod import im.vector.app.features.settings.VectorPreferences +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.image import me.gujun.android.span.span import org.matrix.android.sdk.api.session.crypto.MXCryptoError diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 21af8b82cb..eab7621d14 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -28,16 +28,17 @@ import dagger.Lazy import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.files.LocalFilesHelper import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.containsOnlyEmojis +import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder +import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider @@ -50,6 +51,8 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData +import im.vector.app.features.home.room.detail.timeline.item.MessageLocationItem +import im.vector.app.features.home.room.detail.timeline.item.MessageLocationItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem @@ -68,8 +71,11 @@ import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.PillsPostProcessor import im.vector.app.features.html.SpanUtils import im.vector.app.features.html.VectorHtmlCompressor +import im.vector.app.features.location.LocationData import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.VideoContentRenderer +import im.vector.app.features.settings.VectorPreferences +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.span import org.commonmark.node.Document import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl @@ -83,12 +89,14 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithF import org.matrix.android.sdk.api.session.room.model.message.MessageEmoteContent import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent +import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent import org.matrix.android.sdk.api.session.room.model.message.MessageNoticeContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent +import org.matrix.android.sdk.api.session.room.model.message.PollType import org.matrix.android.sdk.api.session.room.model.message.getFileName import org.matrix.android.sdk.api.session.room.model.message.getFileUrl import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl @@ -117,7 +125,9 @@ class MessageItemFactory @Inject constructor( private val pillsPostProcessorFactory: PillsPostProcessor.Factory, private val spanUtils: SpanUtils, private val session: Session, - private val voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker) { + private val voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker, + private val locationPinProvider: LocationPinProvider, + private val vectorPreferences: VectorPreferences) { // TODO inject this properly? private var roomId: String = "" @@ -169,16 +179,49 @@ class MessageItemFactory @Inject constructor( } } is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessagePollContent -> buildPollContent(messageContent, informationData, highlight, callback, attributes) + is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes) + is MessageLocationContent -> { + if (vectorPreferences.labsRenderLocationsInTimeline()) { + buildLocationItem(messageContent, informationData, highlight, callback, attributes) + } else { + buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes) + } + } else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) } } - private fun buildPollContent(pollContent: MessagePollContent, - informationData: MessageInformationData, - highlight: Boolean, - callback: TimelineEventController.Callback?, - attributes: AbsMessageItem.Attributes): PollItem? { + private fun buildLocationItem(locationContent: MessageLocationContent, + informationData: MessageInformationData, + highlight: Boolean, + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes): MessageLocationItem? { + val geoUri = locationContent.getUri() + val locationData = LocationData.create(geoUri) + + val mapCallback: MessageLocationItem.Callback = object : MessageLocationItem.Callback { + override fun onMapClicked() { + locationData?.let { + callback?.onTimelineItemAction(RoomDetailAction.ShowLocation(it, informationData.senderId)) + } + } + } + + return MessageLocationItem_() + .attributes(attributes) + .locationData(locationData) + .userId(informationData.senderId) + .locationPinProvider(locationPinProvider) + .highlighted(highlight) + .leftGuideline(avatarSizeProvider.leftGuideline) + .callback(mapCallback) + } + + private fun buildPollItem(pollContent: MessagePollContent, + informationData: MessageInformationData, + highlight: Boolean, + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes): PollItem? { val optionViewStates = mutableListOf() val pollResponseSummary = informationData.pollResponseAggregatedSummary @@ -186,11 +229,18 @@ class MessageItemFactory @Inject constructor( val didUserVoted = pollResponseSummary?.myVote?.isNotEmpty().orFalse() val winnerVoteCount = pollResponseSummary?.winnerVoteCount val isPollSent = informationData.sendState.isSent() + val isPollUndisclosed = pollContent.pollCreationInfo?.kind == PollType.UNDISCLOSED + val totalVotesText = (pollResponseSummary?.totalVotes ?: 0).let { when { - isEnded -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, it, it) - didUserVoted -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, it, it) - else -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_not_voted, it, it) + isEnded -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, it, it) + isPollUndisclosed -> "" + didUserVoted -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, it, it) + else -> if (it == 0) { + stringProvider.getString(R.string.poll_no_votes_cast) + } else { + stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_not_voted, it, it) + } } } @@ -210,6 +260,9 @@ class MessageItemFactory @Inject constructor( // Poll is ended. Disable option, show votes and mark the winner. val isWinner = winnerVoteCount != 0 && voteCount == winnerVoteCount PollOptionViewState.PollEnded(optionId, optionAnswer, voteCount, votePercentage, isWinner) + } else if (isPollUndisclosed) { + // Poll is closed. Enable option, hide votes and mark the user's selection. + PollOptionViewState.PollUndisclosed(optionId, optionAnswer, isMyVote) } else if (didUserVoted) { // User voted to the poll, but poll is not ended. Enable option, show votes and mark the user's selection. PollOptionViewState.PollVoted(optionId, optionAnswer, voteCount, votePercentage, isMyVote) @@ -220,13 +273,22 @@ class MessageItemFactory @Inject constructor( ) } + val question = pollContent.pollCreationInfo?.question?.question ?: "" + return PollItem_() .attributes(attributes) .eventId(informationData.eventId) - .pollQuestion(pollContent.pollCreationInfo?.question?.question ?: "") + .pollQuestion( + if (informationData.hasBeenEdited) { + annotateWithEdited(question, callback, informationData) + } else { + question + }.toEpoxyCharSequence() + ) .pollSent(isPollSent) .totalVotesText(totalVotesText) .optionViewStates(optionViewStates) + .edited(informationData.hasBeenEdited) .highlighted(highlight) .leftGuideline(avatarSizeProvider.leftGuideline) .callback(callback) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt index 70c8fa7574..6951c3c316 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt @@ -16,13 +16,13 @@ package im.vector.app.features.home.room.detail.timeline.factory -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.format.NoticeEventFormatter import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.app.features.home.room.detail.timeline.item.NoticeItem import im.vector.app.features.home.room.detail.timeline.item.NoticeItem_ +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence import org.matrix.android.sdk.api.extensions.orFalse import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt index 3a3d269058..fff709f346 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt @@ -18,10 +18,10 @@ package im.vector.app.features.home.room.detail.timeline.factory import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.features.home.room.detail.timeline.item.RoomCreateItem_ +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.span import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.toModel diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt index 3892bfff85..d5f3a74e4e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt @@ -89,6 +89,9 @@ class DisplayableEventFormatter @Inject constructor( MessageType.MSGTYPE_FILE -> { simpleFormat(senderName, stringProvider.getString(R.string.sent_a_file), appendAuthor) } + MessageType.MSGTYPE_LOCATION -> { + simpleFormat(senderName, stringProvider.getString(R.string.sent_location), appendAuthor) + } else -> { simpleFormat(senderName, messageContent.body, appendAuthor) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index d39b8aec5e..ae541217bf 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -770,12 +770,12 @@ class NoticeEventFormatter @Inject constructor( Membership.JOIN -> if (event.isSentByCurrentUser()) { eventContent.safeReason?.let { reason -> - sp.getString(R.string.notice_room_kick_with_reason_by_you, targetDisplayName, reason) - } ?: sp.getString(R.string.notice_room_kick_by_you, targetDisplayName) + sp.getString(R.string.notice_room_remove_with_reason_by_you, targetDisplayName, reason) + } ?: sp.getString(R.string.notice_room_remove_by_you, targetDisplayName) } else { eventContent.safeReason?.let { reason -> - sp.getString(R.string.notice_room_kick_with_reason, senderDisplayName, targetDisplayName, reason) - } ?: sp.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName) + sp.getString(R.string.notice_room_remove_with_reason, senderDisplayName, targetDisplayName, reason) + } ?: sp.getString(R.string.notice_room_remove, senderDisplayName, targetDisplayName) } Membership.BAN -> if (event.isSentByCurrentUser()) { @@ -803,12 +803,12 @@ class NoticeEventFormatter @Inject constructor( Membership.KNOCK -> if (event.isSentByCurrentUser()) { eventContent.safeReason?.let { reason -> - sp.getString(R.string.notice_room_kick_with_reason_by_you, targetDisplayName, reason) - } ?: sp.getString(R.string.notice_room_kick_by_you, targetDisplayName) + sp.getString(R.string.notice_room_remove_with_reason_by_you, targetDisplayName, reason) + } ?: sp.getString(R.string.notice_room_remove_by_you, targetDisplayName) } else { eventContent.safeReason?.let { reason -> - sp.getString(R.string.notice_room_kick_with_reason, senderDisplayName, targetDisplayName, reason) - } ?: sp.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName) + sp.getString(R.string.notice_room_remove_with_reason, senderDisplayName, targetDisplayName, reason) + } ?: sp.getString(R.string.notice_room_remove, senderDisplayName, targetDisplayName) } else -> null } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/LocationPinProvider.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/LocationPinProvider.kt new file mode 100644 index 0000000000..fe3a7d9007 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/LocationPinProvider.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2021 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.timeline.helper + +import android.content.Context +import android.graphics.drawable.Drawable +import android.graphics.drawable.LayerDrawable +import androidx.core.content.ContextCompat +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.transition.Transition +import im.vector.app.R +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.glide.GlideApp +import im.vector.app.core.utils.DimensionConverter +import im.vector.app.features.home.AvatarRenderer +import org.matrix.android.sdk.api.util.toMatrixItem +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class LocationPinProvider @Inject constructor( + private val context: Context, + private val activeSessionHolder: ActiveSessionHolder, + private val dimensionConverter: DimensionConverter, + private val avatarRenderer: AvatarRenderer +) { + private val cache = mutableMapOf() + + private val glideRequests by lazy { + GlideApp.with(context) + } + + fun create(userId: String, callback: (Drawable) -> Unit) { + if (cache.contains(userId)) { + callback(cache[userId]!!) + return + } + + activeSessionHolder.getActiveSession().getUser(userId)?.toMatrixItem()?.let { + val size = dimensionConverter.dpToPx(44) + avatarRenderer.render(glideRequests, it, object : CustomTarget(size, size) { + override fun onResourceReady(resource: Drawable, transition: Transition?) { + val bgUserPin = ContextCompat.getDrawable(context, R.drawable.bg_map_user_pin)!! + val layerDrawable = LayerDrawable(arrayOf(bgUserPin, resource)) + val horizontalInset = dimensionConverter.dpToPx(4) + val topInset = dimensionConverter.dpToPx(4) + val bottomInset = dimensionConverter.dpToPx(8) + layerDrawable.setLayerInset(1, horizontalInset, topInset, horizontalInset, bottomInset) + + cache[userId] = layerDrawable + + callback(layerDrawable) + } + + override fun onLoadCleared(placeholder: Drawable?) { + // Is it possible? Put placeholder instead? + } + }) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt index 01a92decc9..580d7d18cf 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt @@ -119,8 +119,6 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen val diff = computeMembershipDiff() if ((diff.isJoin || diff.isPart) && !userPreferencesProvider.shouldShowJoinLeaves()) return true if ((diff.isAvatarChange || diff.isDisplaynameChange) && !userPreferencesProvider.shouldShowAvatarDisplayNameChanges()) return true - } else if (root.getClearType() == EventType.POLL_START && !userPreferencesProvider.shouldShowPolls()) { - return true } return false } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt index be9b727017..915ad6a17d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt @@ -20,9 +20,9 @@ import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence import me.saket.bettermovementmethod.BetterLinkMovementMethod @EpoxyModelClass(layout = R.layout.item_timeline_event_base) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt new file mode 100644 index 0000000000..3f030866a5 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2021 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.timeline.item + +import android.widget.FrameLayout +import androidx.constraintlayout.widget.ConstraintLayout +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.onClick +import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider +import im.vector.app.features.location.LocationData +import im.vector.app.features.location.MapTilerMapView + +@EpoxyModelClass(layout = R.layout.item_timeline_event_base) +abstract class MessageLocationItem : AbsMessageItem() { + + interface Callback { + fun onMapClicked() + } + + @EpoxyAttribute + var callback: Callback? = null + + @EpoxyAttribute + var locationData: LocationData? = null + + @EpoxyAttribute + var userId: String? = null + + @EpoxyAttribute + var locationPinProvider: LocationPinProvider? = null + + override fun bind(holder: Holder) { + super.bind(holder) + renderSendState(holder.mapViewContainer, null) + + val location = locationData ?: return + val locationOwnerId = userId ?: return + + holder.clickableMapArea.onClick { + callback?.onMapClicked() + } + + holder.mapView.apply { + initialize { + zoomToLocation(location.latitude, location.longitude, INITIAL_ZOOM) + + locationPinProvider?.create(locationOwnerId) { pinDrawable -> + addPinToMap(locationOwnerId, pinDrawable) + updatePinLocation(locationOwnerId, location.latitude, location.longitude) + } + } + } + } + + override fun getViewType() = STUB_ID + + class Holder : AbsMessageItem.Holder(STUB_ID) { + val mapViewContainer by bind(R.id.mapViewContainer) + val mapView by bind(R.id.mapView) + val clickableMapArea by bind(R.id.clickableMapArea) + } + + companion object { + private const val STUB_ID = R.id.messageContentLocationStub + private const val INITIAL_ZOOM = 15.0 + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt index e499f090fb..1794f04c2a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt @@ -25,7 +25,6 @@ import androidx.core.widget.TextViewCompat import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.onLongClickIgnoringLinks import im.vector.app.features.home.room.detail.timeline.TimelineEventController @@ -34,6 +33,7 @@ import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlUiState import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlView import im.vector.app.features.media.ImageContentRenderer +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence import io.noties.markwon.MarkwonPlugin import org.matrix.android.sdk.api.extensions.orFalse diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/NoticeItem.kt index 2851668df5..689d7e6768 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/NoticeItem.kt @@ -23,11 +23,11 @@ import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.ClickListener -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence import im.vector.app.core.epoxy.onClick import im.vector.app.core.ui.views.ShieldImageView import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel @EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt index 1308fa49c8..b660ee9a59 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt @@ -24,12 +24,13 @@ import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence @EpoxyModelClass(layout = R.layout.item_timeline_event_base) abstract class PollItem : AbsMessageItem() { @EpoxyAttribute - var pollQuestion: String? = null + var pollQuestion: EpoxyCharSequence? = null @EpoxyAttribute var callback: TimelineEventController.Callback? = null @@ -43,6 +44,9 @@ abstract class PollItem : AbsMessageItem() { @EpoxyAttribute var totalVotesText: String? = null + @EpoxyAttribute + var edited: Boolean = false + @EpoxyAttribute lateinit var optionViewStates: List @@ -52,7 +56,7 @@ abstract class PollItem : AbsMessageItem() { renderSendState(holder.view, holder.questionTextView) - holder.questionTextView.text = pollQuestion + holder.questionTextView.text = pollQuestion?.charSequence holder.totalVotesTextView.text = totalVotesText while (holder.optionsContainer.childCount < optionViewStates.size) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt index 2af445041b..2be933d9c3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt @@ -23,6 +23,7 @@ import androidx.appcompat.content.res.AppCompatResources import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isVisible import im.vector.app.R +import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.setAttributeTintedImageResource import im.vector.app.databinding.ItemPollOptionBinding @@ -43,11 +44,12 @@ class PollOptionView @JvmOverloads constructor( views.optionNameTextView.text = state.optionAnswer when (state) { - is PollOptionViewState.PollSending -> renderPollSending() - is PollOptionViewState.PollEnded -> renderPollEnded(state) - is PollOptionViewState.PollReady -> renderPollReady() - is PollOptionViewState.PollVoted -> renderPollVoted(state) - } + is PollOptionViewState.PollSending -> renderPollSending() + is PollOptionViewState.PollEnded -> renderPollEnded(state) + is PollOptionViewState.PollReady -> renderPollReady() + is PollOptionViewState.PollVoted -> renderPollVoted(state) + is PollOptionViewState.PollUndisclosed -> renderPollUndisclosed(state) + }.exhaustive } private fun renderPollSending() { @@ -78,6 +80,12 @@ class PollOptionView @JvmOverloads constructor( renderVoteSelection(state.isSelected) } + private fun renderPollUndisclosed(state: PollOptionViewState.PollUndisclosed) { + views.optionCheckImageView.isVisible = true + views.optionWinnerImageView.isVisible = false + renderVoteSelection(state.isSelected) + } + private fun showVotes(voteCount: Int, votePercentage: Double) { views.optionVoteCountTextView.apply { isVisible = true diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionViewState.kt index 5291e7f20a..ae900d0406 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionViewState.kt @@ -51,4 +51,12 @@ sealed class PollOptionViewState(open val optionId: String, val votePercentage: Double, val isWinner: Boolean ) : PollOptionViewState(optionId, optionAnswer) + + /** + * Represent a poll that is undisclosed, votes will be hidden until the poll is ended. + */ + data class PollUndisclosed(override val optionId: String, + override val optionAnswer: String, + val isSelected: Boolean + ) : PollOptionViewState(optionId, optionAnswer) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/RoomCreateItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/RoomCreateItem.kt index 34edcfe9f4..a6d2bcc66d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/RoomCreateItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/RoomCreateItem.kt @@ -22,7 +22,7 @@ import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence import me.saket.bettermovementmethod.BetterLinkMovementMethod @EpoxyModelClass(layout = R.layout.item_timeline_event_create) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ReactionInfoSimpleItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ReactionInfoSimpleItem.kt index f6976d96bf..f150e13016 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ReactionInfoSimpleItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ReactionInfoSimpleItem.kt @@ -24,8 +24,8 @@ import com.airbnb.epoxy.EpoxyModelWithHolder import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence import im.vector.app.core.epoxy.onClick +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence /** * Item displaying an emoji reaction (single line with emoji, author, time) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsEpoxyController.kt index 17a3ac4a5f..10af3792d5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsEpoxyController.kt @@ -22,10 +22,10 @@ import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Success import im.vector.app.EmojiSpanify import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericLoaderItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import javax.inject.Inject /** diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsController.kt index 5c2ad3799b..b2da3bfc78 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsController.kt @@ -18,11 +18,11 @@ package im.vector.app.features.home.room.detail.widget import com.airbnb.epoxy.TypedEpoxyController import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericButtonItem import im.vector.app.core.ui.list.genericFooterItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.session.widgets.model.Widget import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomsActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomsActivity.kt index 5f8ccc794a..0e16b4b0df 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomsActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomsActivity.kt @@ -24,6 +24,7 @@ import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityFilteredRoomsBinding +import im.vector.app.features.analytics.plan.Screen import im.vector.app.features.home.RoomListDisplayMode import im.vector.app.features.home.room.list.RoomListFragment import im.vector.app.features.home.room.list.RoomListParams @@ -42,7 +43,9 @@ class FilteredRoomsActivity : VectorBaseActivity() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - configureToolbar(views.filteredRoomsToolbar) + analyticsScreenName = Screen.ScreenName.RoomFilter + setupToolbar(views.filteredRoomsToolbar) + .allowBack() if (isFirstCreation()) { val params = RoomListParams(RoomListDisplayMode.FILTERED) replaceFragment(views.filteredRoomsFragmentContainer, RoomListFragment::class.java, params, FRAGMENT_TAG) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt index 5171319a41..b6481c9cbb 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt @@ -42,6 +42,7 @@ import im.vector.app.core.platform.StateView import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.databinding.FragmentRoomListBinding +import im.vector.app.features.analytics.plan.Screen import im.vector.app.features.home.RoomListDisplayMode import im.vector.app.features.home.room.filtered.FilteredRoomFooterItem import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet @@ -100,6 +101,15 @@ class RoomListFragment @Inject constructor( private val adapterInfosList = mutableListOf() private var concatAdapter: ConcatAdapter? = null + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + analyticsScreenName = when (roomListParams.displayMode) { + RoomListDisplayMode.PEOPLE -> Screen.ScreenName.MobilePeople + RoomListDisplayMode.ROOMS -> Screen.ScreenName.MobileRooms + else -> null + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) views.stateView.contentView = views.roomListView diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt index 58db2a4030..77f61149f8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt @@ -104,7 +104,7 @@ class RoomListSectionBuilderGroup( } } - appStateHandler.selectedRoomGroupingObservable + appStateHandler.selectedRoomGroupingFlow .distinctUntilChanged() .onEach { groupingMethod -> val selectedGroupId = (groupingMethod.orNull() as? RoomGroupingMethod.ByLegacyGroup)?.groupSummary?.groupId diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt index bde324e57b..296e61690b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt @@ -132,7 +132,7 @@ class RoomListSectionBuilderSpace( } } - appStateHandler.selectedRoomGroupingObservable + appStateHandler.selectedRoomGroupingFlow .distinctUntilChanged() .onEach { groupingMethod -> val selectedSpace = groupingMethod.orNull()?.space() @@ -222,7 +222,7 @@ class RoomListSectionBuilderSpace( // add suggested rooms val suggestedRoomsFlow = // MutableLiveData>() - appStateHandler.selectedRoomGroupingObservable + appStateHandler.selectedRoomGroupingFlow .distinctUntilChanged() .flatMapLatest { groupingMethod -> val selectedSpace = groupingMethod.orNull()?.space() diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt index e5caaffbda..42c800ab9d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt @@ -32,6 +32,8 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider +import im.vector.app.features.analytics.AnalyticsTracker +import im.vector.app.features.analytics.extensions.toAnalyticsJoinedRoom import im.vector.app.features.displayname.getBestName import im.vector.app.features.invite.AutoAcceptInvites import im.vector.app.features.settings.VectorPreferences @@ -56,7 +58,8 @@ class RoomListViewModel @AssistedInject constructor( stringProvider: StringProvider, appStateHandler: AppStateHandler, vectorPreferences: VectorPreferences, - autoAcceptInvites: AutoAcceptInvites + autoAcceptInvites: AutoAcceptInvites, + private val analyticsTracker: AnalyticsTracker ) : VectorViewModel(initialState) { @AssistedFactory @@ -92,7 +95,7 @@ class RoomListViewModel @AssistedInject constructor( init { observeMembershipChanges() - appStateHandler.selectedRoomGroupingObservable + appStateHandler.selectedRoomGroupingFlow .distinctUntilChanged() .execute { copy( @@ -223,6 +226,7 @@ class RoomListViewModel @AssistedInject constructor( viewModelScope.launch { try { room.join() + analyticsTracker.capture(action.roomSummary.toAnalyticsJoinedRoom()) // We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data. // Instead, we wait for the room to be joined } catch (failure: Throwable) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt index 4261acd7ee..b037191ad1 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt @@ -30,7 +30,6 @@ import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.ui.views.PresenceStateImageView @@ -38,6 +37,7 @@ import im.vector.app.core.ui.views.ShieldImageView import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.themes.ThemeUtils +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.presence.model.UserPresence import org.matrix.android.sdk.api.util.MatrixItem diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt index ff57544a36..6326d9c97a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt @@ -23,12 +23,12 @@ import im.vector.app.R import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.resources.StringProvider import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter import im.vector.app.features.home.room.typing.TypingHelper +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomSummary diff --git a/vector/src/main/java/im/vector/app/features/invite/InviteButtonStateBinder.kt b/vector/src/main/java/im/vector/app/features/invite/InviteButtonStateBinder.kt index 2c8589eca1..43763cdf37 100644 --- a/vector/src/main/java/im/vector/app/features/invite/InviteButtonStateBinder.kt +++ b/vector/src/main/java/im/vector/app/features/invite/InviteButtonStateBinder.kt @@ -16,7 +16,7 @@ package im.vector.app.features.invite -import androidx.core.view.isInvisible +import androidx.core.view.isGone import im.vector.app.core.platform.ButtonStateView import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState @@ -38,11 +38,11 @@ object InviteButtonStateBinder { } // ButtonStateView.State.Loaded not used because roomSummary will not be displayed as a room invitation anymore - rejectView.isInvisible = requestInProgress + rejectView.isGone = requestInProgress - when { - changeMembershipState is ChangeMembershipState.FailedLeaving -> rejectView.render(ButtonStateView.State.Error) - else -> rejectView.render(ButtonStateView.State.Button) + when (changeMembershipState) { + is ChangeMembershipState.FailedLeaving -> rejectView.render(ButtonStateView.State.Error) + else -> rejectView.render(ButtonStateView.State.Button) } } } diff --git a/vector/src/main/java/im/vector/app/features/location/Config.kt b/vector/src/main/java/im/vector/app/features/location/Config.kt new file mode 100644 index 0000000000..630df16a37 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/Config.kt @@ -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.location + +const val INITIAL_MAP_ZOOM = 15.0 +const val MIN_TIME_MILLIS_TO_UPDATE_LOCATION = 1 * 60 * 1000L // every 1 minute +const val MIN_DISTANCE_METERS_TO_UPDATE_LOCATION = 10f diff --git a/vector/src/main/java/im/vector/app/features/location/LocationData.kt b/vector/src/main/java/im/vector/app/features/location/LocationData.kt new file mode 100644 index 0000000000..c3ff09ebcd --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/LocationData.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2021 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.location + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class LocationData( + val latitude: Double, + val longitude: Double, + val uncertainty: Double? +) : Parcelable { + + companion object { + + /** + * Creates location data from geo uri + * @param geoUri geo:latitude,longitude;uncertainty + * @return location data or null if geo uri is not valid + */ + fun create(geoUri: String): LocationData? { + val geoParts = geoUri + .split(":") + .takeIf { it.firstOrNull() == "geo" } + ?.getOrNull(1) + ?.split(",") + + val latitude = geoParts?.firstOrNull() + val geoTailParts = geoParts?.getOrNull(1)?.split(";") + val longitude = geoTailParts?.firstOrNull() + val uncertainty = geoTailParts?.getOrNull(1)?.replace("u=", "") + + return if (latitude != null && longitude != null) { + LocationData( + latitude = latitude.toDouble(), + longitude = longitude.toDouble(), + uncertainty = uncertainty?.toDouble() + ) + } else null + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt new file mode 100644 index 0000000000..6209bf5a4f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2021 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.location + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import com.airbnb.mvrx.args +import im.vector.app.R +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.utils.openLocation +import im.vector.app.databinding.FragmentLocationPreviewBinding +import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider +import javax.inject.Inject + +class LocationPreviewFragment @Inject constructor( + private val locationPinProvider: LocationPinProvider +) : VectorBaseFragment() { + + private val args: LocationSharingArgs by args() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLocationPreviewBinding { + return FragmentLocationPreviewBinding.inflate(layoutInflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + views.mapView.initialize { + if (isAdded) { + onMapReady() + } + } + } + + override fun onPause() { + views.mapView.onPause() + super.onPause() + } + + override fun onStop() { + views.mapView.onStop() + super.onStop() + } + + override fun getMenuRes() = R.menu.menu_location_preview + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.share_external -> { + onShareLocationExternal() + return true + } + } + return super.onOptionsItemSelected(item) + } + + private fun onShareLocationExternal() { + val location = args.initialLocationData ?: return + openLocation(requireActivity(), location.latitude, location.longitude) + } + + private fun onMapReady() { + if (!isAdded) return + + val location = args.initialLocationData ?: return + val userId = args.locationOwnerId + + locationPinProvider.create(userId) { pinDrawable -> + views.mapView.apply { + zoomToLocation(location.latitude, location.longitude, INITIAL_MAP_ZOOM) + deleteAllPins() + addPinToMap(userId, pinDrawable) + updatePinLocation(userId, location.latitude, location.longitude) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt new file mode 100644 index 0000000000..71101d0612 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2021 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.location + +import im.vector.app.core.platform.VectorViewModelAction + +sealed class LocationSharingAction : VectorViewModelAction { + data class OnLocationUpdate(val locationData: LocationData) : LocationSharingAction() + object OnShareLocation : LocationSharingAction() + object OnLocationProviderIsNotAvailable : LocationSharingAction() +} diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingActivity.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingActivity.kt new file mode 100644 index 0000000000..67b36b8442 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingActivity.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2021 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.location + +import android.content.Context +import android.content.Intent +import android.os.Parcelable +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.core.extensions.addFragment +import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.databinding.ActivityLocationSharingBinding +import kotlinx.parcelize.Parcelize + +@Parcelize +data class LocationSharingArgs( + val roomId: String, + val mode: LocationSharingMode, + val initialLocationData: LocationData?, + val locationOwnerId: String +) : Parcelable + +@AndroidEntryPoint +class LocationSharingActivity : VectorBaseActivity() { + + override fun getBinding() = ActivityLocationSharingBinding.inflate(layoutInflater) + + override fun initUiAndData() { + val locationSharingArgs: LocationSharingArgs? = intent?.extras?.getParcelable(EXTRA_LOCATION_SHARING_ARGS) + if (locationSharingArgs == null) { + finish() + return + } + setupToolbar(views.toolbar) + .setTitle(locationSharingArgs.mode.titleRes) + .allowBack() + + if (isFirstCreation()) { + when (locationSharingArgs.mode) { + LocationSharingMode.STATIC_SHARING -> { + addFragment( + views.fragmentContainer, + LocationSharingFragment::class.java, + locationSharingArgs + ) + } + LocationSharingMode.PREVIEW -> { + addFragment( + views.fragmentContainer, + LocationPreviewFragment::class.java, + locationSharingArgs + ) + } + } + } + } + + companion object { + + private const val EXTRA_LOCATION_SHARING_ARGS = "EXTRA_LOCATION_SHARING_ARGS" + + fun getIntent(context: Context, locationSharingArgs: LocationSharingArgs): Intent { + return Intent(context, LocationSharingActivity::class.java).apply { + putExtra(EXTRA_LOCATION_SHARING_ARGS, locationSharingArgs) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt new file mode 100644 index 0000000000..900f465f04 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2021 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.location + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.mvrx.fragmentViewModel +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import im.vector.app.R +import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.databinding.FragmentLocationSharingBinding +import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider +import org.matrix.android.sdk.api.session.Session +import javax.inject.Inject + +class LocationSharingFragment @Inject constructor( + private val locationTracker: LocationTracker, + private val session: Session, + private val locationPinProvider: LocationPinProvider +) : VectorBaseFragment(), LocationTracker.Callback { + + init { + locationTracker.callback = this + } + + private val viewModel: LocationSharingViewModel by fragmentViewModel() + + private var lastZoomValue: Double = -1.0 + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLocationSharingBinding { + return FragmentLocationSharingBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + views.mapView.initialize { + if (isAdded) { + onMapReady() + } + } + + views.shareLocationContainer.debouncedClicks { + viewModel.handle(LocationSharingAction.OnShareLocation) + } + + viewModel.observeViewEvents { + when (it) { + LocationSharingViewEvents.LocationNotAvailableError -> handleLocationNotAvailableError() + LocationSharingViewEvents.Close -> activity?.finish() + }.exhaustive + } + } + + override fun onPause() { + views.mapView.onPause() + super.onPause() + } + + override fun onStop() { + views.mapView.onStop() + super.onStop() + } + + override fun onDestroy() { + locationTracker.stop() + super.onDestroy() + } + + private fun onMapReady() { + if (!isAdded) return + + locationPinProvider.create(session.myUserId) { + views.mapView.addPinToMap( + pinId = USER_PIN_NAME, + image = it, + ) + // All set, start location tracker + locationTracker.start() + } + } + + override fun onLocationUpdate(locationData: LocationData) { + lastZoomValue = if (lastZoomValue == -1.0) INITIAL_MAP_ZOOM else views.mapView.getCurrentZoom() ?: INITIAL_MAP_ZOOM + + views.mapView.zoomToLocation(locationData.latitude, locationData.longitude, lastZoomValue) + views.mapView.deleteAllPins() + views.mapView.updatePinLocation(USER_PIN_NAME, locationData.latitude, locationData.longitude) + + viewModel.handle(LocationSharingAction.OnLocationUpdate(locationData)) + } + + override fun onLocationProviderIsNotAvailable() { + viewModel.handle(LocationSharingAction.OnLocationProviderIsNotAvailable) + } + + private fun handleLocationNotAvailableError() { + MaterialAlertDialogBuilder(requireActivity()) + .setTitle(R.string.location_not_available_dialog_title) + .setMessage(R.string.location_not_available_dialog_content) + .setPositiveButton(R.string.ok) { _, _ -> + activity?.finish() + } + .show() + } + + companion object { + const val USER_PIN_NAME = "USER_PIN_NAME" + } +} diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewEvents.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewEvents.kt new file mode 100644 index 0000000000..743daaf5e0 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewEvents.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2021 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.location + +import im.vector.app.core.platform.VectorViewEvents + +sealed class LocationSharingViewEvents : VectorViewEvents { + object Close : LocationSharingViewEvents() + object LocationNotAvailableError : LocationSharingViewEvents() +} diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt new file mode 100644 index 0000000000..b3c97310e1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2021 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.location + +import com.airbnb.mvrx.MavericksViewModelFactory +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.platform.VectorViewModel +import org.matrix.android.sdk.api.session.Session + +class LocationSharingViewModel @AssistedInject constructor( + @Assisted private val initialState: LocationSharingViewState, + session: Session +) : VectorViewModel(initialState) { + + private val room = session.getRoom(initialState.roomId)!! + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: LocationSharingViewState): LocationSharingViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() { + } + + override fun handle(action: LocationSharingAction) { + when (action) { + is LocationSharingAction.OnLocationUpdate -> handleLocationUpdate(action.locationData) + LocationSharingAction.OnShareLocation -> handleShareLocation() + LocationSharingAction.OnLocationProviderIsNotAvailable -> handleLocationProviderIsNotAvailable() + }.exhaustive + } + + private fun handleShareLocation() = withState { state -> + state.lastKnownLocation?.let { location -> + room.sendLocation( + latitude = location.latitude, + longitude = location.longitude, + uncertainty = location.uncertainty + ) + _viewEvents.post(LocationSharingViewEvents.Close) + } ?: run { + _viewEvents.post(LocationSharingViewEvents.LocationNotAvailableError) + } + } + + private fun handleLocationUpdate(locationData: LocationData) { + setState { + copy(lastKnownLocation = locationData) + } + } + + private fun handleLocationProviderIsNotAvailable() { + _viewEvents.post(LocationSharingViewEvents.LocationNotAvailableError) + } +} diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt new file mode 100644 index 0000000000..2869929b12 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2021 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.location + +import androidx.annotation.StringRes +import com.airbnb.mvrx.MavericksState +import im.vector.app.R + +enum class LocationSharingMode(@StringRes val titleRes: Int) { + STATIC_SHARING(R.string.location_activity_title_static_sharing), + PREVIEW(R.string.location_activity_title_preview) +} + +data class LocationSharingViewState( + val roomId: String, + val mode: LocationSharingMode, + val lastKnownLocation: LocationData? = null +) : MavericksState { + + constructor(locationSharingArgs: LocationSharingArgs) : this( + roomId = locationSharingArgs.roomId, + mode = locationSharingArgs.mode + ) +} diff --git a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt new file mode 100644 index 0000000000..0c0315cf34 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2021 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.location + +import android.Manifest +import android.content.Context +import android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import androidx.annotation.RequiresPermission +import androidx.core.content.getSystemService +import timber.log.Timber +import javax.inject.Inject + +class LocationTracker @Inject constructor( + private val context: Context +) : LocationListener { + + interface Callback { + fun onLocationUpdate(locationData: LocationData) + fun onLocationProviderIsNotAvailable() + } + + private var locationManager: LocationManager? = null + var callback: Callback? = null + + @RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION]) + fun start() { + val locationManager = context.getSystemService() + + locationManager?.let { + val isGpsEnabled = it.isProviderEnabled(LocationManager.GPS_PROVIDER) + val isNetworkEnabled = it.isProviderEnabled(LocationManager.NETWORK_PROVIDER) + + val provider = when { + isGpsEnabled -> LocationManager.GPS_PROVIDER + isNetworkEnabled -> LocationManager.NETWORK_PROVIDER + else -> { + callback?.onLocationProviderIsNotAvailable() + Timber.v("## LocationTracker. There is no location provider available") + return + } + } + + // Send last known location without waiting location updates + it.getLastKnownLocation(provider)?.let { lastKnownLocation -> + callback?.onLocationUpdate(lastKnownLocation.toLocationData()) + } + + it.requestLocationUpdates( + provider, + MIN_TIME_MILLIS_TO_UPDATE_LOCATION, + MIN_DISTANCE_METERS_TO_UPDATE_LOCATION, + this + ) + } ?: run { + callback?.onLocationProviderIsNotAvailable() + Timber.v("## LocationTracker. LocationManager is not available") + } + } + + @RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION]) + fun stop() { + locationManager?.removeUpdates(this) + callback = null + } + + override fun onLocationChanged(location: Location) { + callback?.onLocationUpdate(location.toLocationData()) + } + + private fun Location.toLocationData(): LocationData { + return LocationData(latitude, longitude, accuracy.toDouble()) + } +} diff --git a/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt b/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt new file mode 100644 index 0000000000..c64af1ebaa --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2021 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.location + +import android.content.Context +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import com.mapbox.mapboxsdk.camera.CameraPosition +import com.mapbox.mapboxsdk.geometry.LatLng +import com.mapbox.mapboxsdk.maps.MapView +import com.mapbox.mapboxsdk.maps.MapboxMap +import com.mapbox.mapboxsdk.maps.Style +import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager +import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions +import com.mapbox.mapboxsdk.style.layers.Property +import im.vector.app.BuildConfig + +class MapTilerMapView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : MapView(context, attrs, defStyleAttr), VectorMapView { + + private var map: MapboxMap? = null + private var symbolManager: SymbolManager? = null + private var style: Style? = null + + override fun initialize(onMapReady: () -> Unit) { + getMapAsync { map -> + map.setStyle(styleUrl) { style -> + this.symbolManager = SymbolManager(this, map, style) + this.map = map + this.style = style + onMapReady() + } + } + } + + override fun addPinToMap(pinId: String, image: Drawable) { + style?.addImage(pinId, image) + } + + override fun updatePinLocation(pinId: String, latitude: Double, longitude: Double) { + symbolManager?.create( + SymbolOptions() + .withLatLng(LatLng(latitude, longitude)) + .withIconImage(pinId) + .withIconAnchor(Property.ICON_ANCHOR_BOTTOM) + ) + } + + override fun deleteAllPins() { + symbolManager?.deleteAll() + } + + override fun zoomToLocation(latitude: Double, longitude: Double, zoom: Double) { + map?.cameraPosition = CameraPosition.Builder() + .target(LatLng(latitude, longitude)) + .zoom(zoom) + .build() + } + + override fun getCurrentZoom(): Double? { + return map?.cameraPosition?.zoom + } + + override fun onClick(callback: () -> Unit) { + map?.addOnMapClickListener { + callback() + true + } + } + + companion object { + private const val styleUrl = "https://api.maptiler.com/maps/streets/style.json?key=${BuildConfig.mapTilerKey}" + } +} diff --git a/vector/src/main/java/im/vector/app/features/location/VectorMapView.kt b/vector/src/main/java/im/vector/app/features/location/VectorMapView.kt new file mode 100644 index 0000000000..23b59bf99a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/VectorMapView.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2021 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.location + +import android.graphics.drawable.Drawable + +interface VectorMapView { + fun initialize(onMapReady: () -> Unit) + + fun addPinToMap(pinId: String, image: Drawable) + fun updatePinLocation(pinId: String, latitude: Double, longitude: Double) + fun deleteAllPins() + + fun zoomToLocation(latitude: Double, longitude: Double, zoom: Double) + fun getCurrentZoom(): Double? + + fun onClick(callback: () -> Unit) +} diff --git a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt index 5ab08ffff7..edc77d73f6 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt @@ -29,7 +29,6 @@ import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentTransaction import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.withState -import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R @@ -37,9 +36,9 @@ import im.vector.app.core.extensions.POP_BACK_STACK_EXCLUSIVE import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragmentToBackstack import im.vector.app.core.extensions.exhaustive -import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityLoginBinding +import im.vector.app.features.analytics.plan.Screen import im.vector.app.features.home.HomeActivity import im.vector.app.features.login.terms.LoginTermsFragment import im.vector.app.features.login.terms.LoginTermsFragmentArgument @@ -53,7 +52,7 @@ import org.matrix.android.sdk.api.extensions.tryOrNull * The LoginActivity manages the fragment navigation and also display the loading View */ @AndroidEntryPoint -open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedActivity { +open class LoginActivity : VectorBaseActivity(), UnlockedActivity { private val loginViewModel: LoginViewModel by viewModel() @@ -82,6 +81,8 @@ open class LoginActivity : VectorBaseActivity(), ToolbarCo override fun getCoordinatorLayout() = views.coordinatorLayout override fun initUiAndData() { + analyticsScreenName = Screen.ScreenName.Login + if (isFirstCreation()) { addFirstFragment() } @@ -200,6 +201,10 @@ open class LoginActivity : VectorBaseActivity(), ToolbarCo private fun updateWithState(loginViewState: LoginViewState) { if (loginViewState.isUserLogged()) { + if (loginViewState.signMode == SignMode.SignUp) { + // change the screen name + analyticsScreenName = Screen.ScreenName.Register + } val intent = HomeActivity.newIntent( this, accountCreation = loginViewState.signMode == SignMode.SignUp @@ -346,10 +351,6 @@ open class LoginActivity : VectorBaseActivity(), ToolbarCo } } - override fun configure(toolbar: MaterialToolbar) { - configureToolbar(toolbar) - } - companion object { private const val FRAGMENT_REGISTRATION_STAGE_TAG = "FRAGMENT_REGISTRATION_STAGE_TAG" private const val FRAGMENT_LOGIN_TAG = "FRAGMENT_LOGIN_TAG" diff --git a/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordFragment.kt index 52bd80a16f..0328d09427 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordFragment.kt @@ -31,6 +31,7 @@ import im.vector.app.core.extensions.hidePassword import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.toReducedUrl import im.vector.app.databinding.FragmentLoginResetPasswordBinding +import im.vector.app.features.analytics.plan.Screen import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map @@ -46,6 +47,11 @@ class LoginResetPasswordFragment @Inject constructor() : AbstractLoginFragment getString(R.string.login_signin) else -> getString(R.string.login_signup) } @@ -149,7 +150,7 @@ class LoginWebFragment @Inject constructor( override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { super.onPageStarted(view, url, favicon) - views.loginWebToolbar.subtitle = url + toolbar?.subtitle = url } override fun onPageFinished(view: WebView, url: String) { diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginWebFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginWebFragment2.kt index 080cce4958..ebe59ee1b9 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginWebFragment2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginWebFragment2.kt @@ -65,6 +65,7 @@ class LoginWebFragment2 @Inject constructor( super.onViewCreated(view, savedInstanceState) setupToolbar(views.loginWebToolbar) + .allowBack() } override fun updateWithState(state: LoginViewState2) { @@ -79,7 +80,7 @@ class LoginWebFragment2 @Inject constructor( } private fun setupTitle(state: LoginViewState2) { - views.loginWebToolbar.title = when (state.signMode) { + toolbar?.title = when (state.signMode) { SignMode2.SignIn -> getString(R.string.login_signin) else -> getString(R.string.login_signup) } @@ -150,7 +151,7 @@ class LoginWebFragment2 @Inject constructor( override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { super.onPageStarted(view, url, favicon) - views.loginWebToolbar.subtitle = url + toolbar?.subtitle = url } override fun onPageFinished(view: WebView, url: String) { diff --git a/vector/src/main/java/im/vector/app/features/media/BigImageViewerActivity.kt b/vector/src/main/java/im/vector/app/features/media/BigImageViewerActivity.kt index 84454ee509..a6b166815c 100644 --- a/vector/src/main/java/im/vector/app/features/media/BigImageViewerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/media/BigImageViewerActivity.kt @@ -38,12 +38,9 @@ class BigImageViewerActivity : VectorBaseActivity override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setSupportActionBar(views.bigImageViewerToolbar) - supportActionBar?.apply { - title = intent.getStringExtra(EXTRA_TITLE) - setHomeButtonEnabled(true) - setDisplayHomeAsUpEnabled(true) - } + setupToolbar(views.bigImageViewerToolbar) + .setTitle(intent.getStringExtra(EXTRA_TITLE)) + .allowBack() val uri = sessionHolder.getSafeActiveSession() ?.contentUrlResolver() diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 30ead8a6bf..f66ced3299 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -58,6 +58,10 @@ import im.vector.app.features.home.room.detail.search.SearchActivity import im.vector.app.features.home.room.detail.search.SearchArgs import im.vector.app.features.home.room.filtered.FilteredRoomsActivity import im.vector.app.features.invite.InviteUsersToRoomActivity +import im.vector.app.features.location.LocationData +import im.vector.app.features.location.LocationSharingActivity +import im.vector.app.features.location.LocationSharingArgs +import im.vector.app.features.location.LocationSharingMode import im.vector.app.features.login.LoginActivity import im.vector.app.features.login.LoginConfig import im.vector.app.features.matrixto.MatrixToBottomSheet @@ -70,6 +74,7 @@ import im.vector.app.features.pin.PinArgs import im.vector.app.features.pin.PinMode import im.vector.app.features.poll.create.CreatePollActivity import im.vector.app.features.poll.create.CreatePollArgs +import im.vector.app.features.poll.create.PollMode import im.vector.app.features.roomdirectory.RoomDirectoryActivity import im.vector.app.features.roomdirectory.RoomDirectoryData import im.vector.app.features.roomdirectory.createroom.CreateRoomActivity @@ -309,8 +314,8 @@ class DefaultNavigator @Inject constructor( } } - override fun openCreateRoom(context: Context, initialName: String) { - val intent = CreateRoomActivity.getIntent(context, initialName) + override fun openCreateRoom(context: Context, initialName: String, openAfterCreate: Boolean) { + val intent = CreateRoomActivity.getIntent(context = context, initialName = initialName, openAfterCreate = openAfterCreate) context.startActivity(intent) } @@ -524,10 +529,22 @@ class DefaultNavigator @Inject constructor( context.startActivity(intent) } - override fun openCreatePoll(context: Context, roomId: String) { + override fun openCreatePoll(context: Context, roomId: String, editedEventId: String?, mode: PollMode) { val intent = CreatePollActivity.getIntent( context, - CreatePollArgs(roomId = roomId) + CreatePollArgs(roomId = roomId, editedEventId = editedEventId, mode = mode) + ) + context.startActivity(intent) + } + + override fun openLocationSharing(context: Context, + roomId: String, + mode: LocationSharingMode, + initialLocationData: LocationData?, + locationOwnerId: String) { + val intent = LocationSharingActivity.getIntent( + context, + LocationSharingArgs(roomId = roomId, mode = mode, initialLocationData = initialLocationData, locationOwnerId = locationOwnerId) ) context.startActivity(intent) } diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index 6778c39a22..775272bd33 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -25,9 +25,12 @@ import androidx.activity.result.ActivityResultLauncher import androidx.core.util.Pair import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.displayname.getBestName +import im.vector.app.features.location.LocationData +import im.vector.app.features.location.LocationSharingMode import im.vector.app.features.login.LoginConfig import im.vector.app.features.media.AttachmentData import im.vector.app.features.pin.PinMode +import im.vector.app.features.poll.create.PollMode import im.vector.app.features.roomdirectory.RoomDirectoryData import im.vector.app.features.roomdirectory.roompreview.RoomPreviewData import im.vector.app.features.settings.VectorSettingsActivity @@ -76,7 +79,7 @@ interface Navigator { fun openMatrixToBottomSheet(context: Context, link: String) - fun openCreateRoom(context: Context, initialName: String = "") + fun openCreateRoom(context: Context, initialName: String = "", openAfterCreate: Boolean = true) fun openCreateDirectRoom(context: Context) @@ -148,5 +151,11 @@ interface Navigator { fun openCallTransfer(context: Context, callId: String) - fun openCreatePoll(context: Context, roomId: String) + fun openCreatePoll(context: Context, roomId: String, editedEventId: String?, mode: PollMode) + + fun openLocationSharing(context: Context, + roomId: String, + mode: LocationSharingMode, + initialLocationData: LocationData?, + locationOwnerId: String) } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt index b1905059a1..ac2ec06474 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt @@ -23,6 +23,8 @@ import androidx.core.app.RemoteInput import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.features.analytics.AnalyticsTracker +import im.vector.app.features.analytics.extensions.toAnalyticsJoinedRoom import im.vector.app.features.session.coroutineScope import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.tryOrNull @@ -41,6 +43,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { @Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var activeSessionHolder: ActiveSessionHolder + @Inject lateinit var analyticsTracker: AnalyticsTracker override fun onReceive(context: Context?, intent: Intent?) { if (intent == null || context == null) return @@ -79,7 +82,10 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { val room = session.getRoom(roomId) if (room != null) { session.coroutineScope.launch { - tryOrNull { room.join() } + tryOrNull { + room.join() + analyticsTracker.capture(room.roomSummary().toAnalyticsJoinedRoom()) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt index 96e8c2af4b..27568dae95 100755 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt @@ -773,7 +773,7 @@ class NotificationUtils @Inject constructor(private val context: Context, } private fun buildOpenRoomIntent(roomId: String): PendingIntent? { - val roomIntentTap = RoomDetailActivity.newIntent(context, RoomDetailArgs(roomId)) + val roomIntentTap = RoomDetailActivity.newIntent(context, RoomDetailArgs(roomId = roomId, switchToParentSpace = true)) roomIntentTap.action = TAP_TO_VIEW_ACTION // pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that roomIntentTap.data = createIgnoredUri("openRoom?$roomId") diff --git a/vector/src/main/java/im/vector/app/core/platform/ToolbarConfigurable.kt b/vector/src/main/java/im/vector/app/features/onboarding/FtueUseCase.kt similarity index 67% rename from vector/src/main/java/im/vector/app/core/platform/ToolbarConfigurable.kt rename to vector/src/main/java/im/vector/app/features/onboarding/FtueUseCase.kt index 9aca8dd17f..e720b7307c 100644 --- a/vector/src/main/java/im/vector/app/core/platform/ToolbarConfigurable.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/FtueUseCase.kt @@ -1,11 +1,11 @@ /* - * Copyright 2019 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. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -14,11 +14,11 @@ * limitations under the License. */ -package im.vector.app.core.platform +package im.vector.app.features.onboarding -import com.google.android.material.appbar.MaterialToolbar - -interface ToolbarConfigurable { - - fun configure(toolbar: MaterialToolbar) +enum class FtueUseCase { + FRIENDS_FAMILY, + TEAMS, + COMMUNITIES, + SKIP } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt index bb1d3cc52d..2ca6a1f2fd 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt @@ -31,6 +31,8 @@ sealed class OnboardingAction : VectorViewModelAction { data class UpdateServerType(val serverType: ServerType) : OnboardingAction() data class UpdateHomeServer(val homeServerUrl: String) : OnboardingAction() + data class UpdateUseCase(val useCase: FtueUseCase) : OnboardingAction() + object ResetUseCase : OnboardingAction() data class UpdateSignMode(val signMode: SignMode) : OnboardingAction() data class LoginWithToken(val loginToken: String) : OnboardingAction() data class WebLoginSuccess(val credentials: Credentials) : OnboardingAction() diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingActivity.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingActivity.kt index f0cf9464a6..4165d4cb65 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingActivity.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingActivity.kt @@ -19,10 +19,8 @@ package im.vector.app.features.onboarding import android.content.Context import android.content.Intent import android.net.Uri -import com.google.android.material.appbar.MaterialToolbar import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.lazyViewModel -import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.lifecycleAwareLazy import im.vector.app.databinding.ActivityLoginBinding @@ -31,7 +29,7 @@ import im.vector.app.features.pin.UnlockedActivity import javax.inject.Inject @AndroidEntryPoint -class OnboardingActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedActivity { +class OnboardingActivity : VectorBaseActivity(), UnlockedActivity { private val onboardingVariant by lifecycleAwareLazy { onboardingVariantFactory.create(this, views = views, onboardingViewModel = lazyViewModel(), loginViewModel2 = lazyViewModel()) @@ -43,10 +41,6 @@ class OnboardingActivity : VectorBaseActivity(), ToolbarCo override fun getCoordinatorLayout() = views.coordinatorLayout - override fun configure(toolbar: MaterialToolbar) { - configureToolbar(toolbar) - } - override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) onboardingVariant.onNewIntent(intent) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingVariantFactory.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingVariantFactory.kt index c171fc223d..52423d7019 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingVariantFactory.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingVariantFactory.kt @@ -16,6 +16,7 @@ package im.vector.app.features.onboarding +import im.vector.app.core.platform.ScreenOrientationLocker import im.vector.app.databinding.ActivityLoginBinding import im.vector.app.features.VectorFeatures import im.vector.app.features.login2.LoginViewModel2 @@ -24,6 +25,7 @@ import javax.inject.Inject class OnboardingVariantFactory @Inject constructor( private val vectorFeatures: VectorFeatures, + private val orientationLocker: ScreenOrientationLocker, ) { fun create(activity: OnboardingActivity, @@ -37,7 +39,8 @@ class OnboardingVariantFactory @Inject constructor( onboardingViewModel = onboardingViewModel.value, activity = activity, supportFragmentManager = activity.supportFragmentManager, - vectorFeatures = vectorFeatures + vectorFeatures = vectorFeatures, + orientationLocker = orientationLocker ) VectorFeatures.OnboardingVariant.LOGIN_2 -> Login2Variant( views = views, diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt index ab782a9908..d6105cda13 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt @@ -34,6 +34,7 @@ sealed class OnboardingViewEvents : VectorViewEvents { // Navigation event + object OpenUseCaseSelection : OnboardingViewEvents() object OpenServerSelection : OnboardingViewEvents() data class OnServerSelectionDone(val serverType: ServerType) : OnboardingViewEvents() object OnLoginFlowRetrieved : OnboardingViewEvents() diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt index 4b3ce14002..43f37f4601 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt @@ -35,6 +35,7 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.ensureTrailingSlash +import im.vector.app.features.VectorFeatures import im.vector.app.features.login.HomeServerConnectionConfigFactory import im.vector.app.features.login.LoginConfig import im.vector.app.features.login.LoginMode @@ -71,7 +72,8 @@ class OnboardingViewModel @AssistedInject constructor( private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory, private val reAuthHelper: ReAuthHelper, private val stringProvider: StringProvider, - private val homeServerHistoryService: HomeServerHistoryService + private val homeServerHistoryService: HomeServerHistoryService, + private val vectorFeatures: VectorFeatures ) : VectorViewModel(initialState) { @AssistedFactory @@ -123,6 +125,8 @@ class OnboardingViewModel @AssistedInject constructor( when (action) { is OnboardingAction.OnGetStarted -> handleSplashAction(action.resetLoginConfig, action.onboardingFlow) is OnboardingAction.OnIAlreadyHaveAnAccount -> handleSplashAction(action.resetLoginConfig, action.onboardingFlow) + is OnboardingAction.UpdateUseCase -> handleUpdateUseCase() + OnboardingAction.ResetUseCase -> resetUseCase() is OnboardingAction.UpdateServerType -> handleUpdateServerType(action) is OnboardingAction.UpdateSignMode -> handleUpdateSignMode(action) is OnboardingAction.InitWith -> handleInitWith(action) @@ -154,15 +158,28 @@ class OnboardingViewModel @AssistedInject constructor( if (homeServerConnectionConfig == null) { // Url is invalid, in this case, just use the regular flow Timber.w("Url from config url was invalid: $configUrl") - _viewEvents.post(OnboardingViewEvents.OpenServerSelection) + continueToPageAfterSplash(onboardingFlow) } else { getLoginFlow(homeServerConnectionConfig, ServerType.Other) } } else { - _viewEvents.post(OnboardingViewEvents.OpenServerSelection) + continueToPageAfterSplash(onboardingFlow) } } + private fun continueToPageAfterSplash(onboardingFlow: OnboardingFlow) { + val nextOnboardingStep = when (onboardingFlow) { + OnboardingFlow.SignUp -> if (vectorFeatures.isOnboardingUseCaseEnabled()) { + OnboardingViewEvents.OpenUseCaseSelection + } else { + OnboardingViewEvents.OpenServerSelection + } + OnboardingFlow.SignIn, + OnboardingFlow.SignInSignUp -> OnboardingViewEvents.OpenServerSelection + } + _viewEvents.post(nextOnboardingStep) + } + private fun handleUserAcceptCertificate(action: OnboardingAction.UserAcceptCertificate) { // It happens when we get the login flow, or during direct authentication. // So alter the homeserver config and retrieve again the login flow @@ -441,6 +458,15 @@ class OnboardingViewModel @AssistedInject constructor( } } + private fun handleUpdateUseCase() { + // TODO act on the use case selection + _viewEvents.post(OnboardingViewEvents.OpenServerSelection) + } + + private fun resetUseCase() { + // TODO remove stored use case + } + private fun handleUpdateServerType(action: OnboardingAction.UpdateServerType) { setState { copy( @@ -826,7 +852,7 @@ class OnboardingViewModel @AssistedInject constructor( } withState { - if (it.serverType == ServerType.MatrixOrg) { + if (loginMode.supportsSignModeScreen()) { when (it.onboardingFlow) { OnboardingFlow.SignIn -> handleUpdateSignMode(OnboardingAction.UpdateSignMode(SignMode.SignIn)) OnboardingFlow.SignUp -> handleUpdateSignMode(OnboardingAction.UpdateSignMode(SignMode.SignUp)) @@ -854,3 +880,13 @@ class OnboardingViewModel @AssistedInject constructor( return authenticationService.getFallbackUrl(forSignIn, deviceId) } } + +private fun LoginMode.supportsSignModeScreen(): Boolean { + return when (this) { + LoginMode.Password, + is LoginMode.SsoAndPassword -> true + is LoginMode.Sso, + LoginMode.Unknown, + LoginMode.Unsupported -> false + } +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt index 152754f241..49e8875cb5 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt @@ -49,7 +49,8 @@ private const val CAROUSEL_TRANSITION_TIME_MS = 500L class FtueAuthSplashCarouselFragment @Inject constructor( private val vectorPreferences: VectorPreferences, private val vectorFeatures: VectorFeatures, - private val carouselController: SplashCarouselController + private val carouselController: SplashCarouselController, + private val carouselStateFactory: SplashCarouselStateFactory ) : AbstractFtueAuthFragment() { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueSplashCarouselBinding { @@ -65,11 +66,15 @@ class FtueAuthSplashCarouselFragment @Inject constructor( val carouselAdapter = carouselController.adapter views.splashCarousel.adapter = carouselAdapter TabLayoutMediator(views.carouselIndicator, views.splashCarousel) { _, _ -> }.attach() - carouselController.setData(SplashCarouselState()) + carouselController.setData(carouselStateFactory.create()) - views.loginSplashSubmit.debouncedClicks { getStarted() } + val isAlreadyHaveAccountEnabled = vectorFeatures.isOnboardingAlreadyHaveAccountSplashEnabled() + views.loginSplashSubmit.apply { + setText(if (isAlreadyHaveAccountEnabled) R.string.login_splash_create_account else R.string.login_splash_submit) + debouncedClicks { splashSubmit(isAlreadyHaveAccountEnabled) } + } views.loginSplashAlreadyHaveAccount.apply { - isVisible = vectorFeatures.isAlreadyHaveAccountSplashEnabled() + isVisible = isAlreadyHaveAccountEnabled debouncedClicks { alreadyHaveAnAccount() } } @@ -80,17 +85,26 @@ class FtueAuthSplashCarouselFragment @Inject constructor( "Branch: ${BuildConfig.GIT_BRANCH_NAME}" views.loginSplashVersion.debouncedClicks { navigator.openDebug(requireContext()) } } + views.splashCarousel.registerAutomaticUntilInteractionTransitions() + } - views.splashCarousel.apply { - var scheduledTransition: Job? = null - registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { - override fun onPageSelected(position: Int) { - scheduledTransition?.cancel() + private fun ViewPager2.registerAutomaticUntilInteractionTransitions() { + var scheduledTransition: Job? = null + registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + private var hasUserManuallyInteractedWithCarousel: Boolean = false + + override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { + hasUserManuallyInteractedWithCarousel = !isFakeDragging + } + + override fun onPageSelected(position: Int) { + scheduledTransition?.cancel() + // only schedule automatic transitions whilst the user has not interacted with the carousel + if (!hasUserManuallyInteractedWithCarousel) { scheduledTransition = scheduleCarouselTransition() } - }) - scheduledTransition = scheduleCarouselTransition() - } + } + }) } private fun ViewPager2.scheduleCarouselTransition(): Job { @@ -101,8 +115,8 @@ class FtueAuthSplashCarouselFragment @Inject constructor( } } - private fun getStarted() { - val getStartedFlow = if (vectorFeatures.isAlreadyHaveAccountSplashEnabled()) OnboardingFlow.SignUp else OnboardingFlow.SignInSignUp + private fun splashSubmit(isAlreadyHaveAccountEnabled: Boolean) { + val getStartedFlow = if (isAlreadyHaveAccountEnabled) OnboardingFlow.SignUp else OnboardingFlow.SignInSignUp viewModel.handle(OnboardingAction.OnGetStarted(resetLoginConfig = false, onboardingFlow = getStartedFlow)) } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashFragment.kt index f8f1d7919b..031579db5f 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashFragment.kt @@ -53,9 +53,13 @@ class FtueAuthSplashFragment @Inject constructor( } private fun setupViews() { - views.loginSplashSubmit.debouncedClicks { getStarted() } + val isAlreadyHaveAccountEnabled = vectorFeatures.isOnboardingAlreadyHaveAccountSplashEnabled() + views.loginSplashSubmit.apply { + setText(if (isAlreadyHaveAccountEnabled) R.string.login_splash_create_account else R.string.login_splash_submit) + debouncedClicks { splashSubmit(isAlreadyHaveAccountEnabled) } + } views.loginSplashAlreadyHaveAccount.apply { - isVisible = vectorFeatures.isAlreadyHaveAccountSplashEnabled() + isVisible = vectorFeatures.isOnboardingAlreadyHaveAccountSplashEnabled() debouncedClicks { alreadyHaveAnAccount() } } @@ -69,8 +73,8 @@ class FtueAuthSplashFragment @Inject constructor( } } - private fun getStarted() { - val getStartedFlow = if (vectorFeatures.isAlreadyHaveAccountSplashEnabled()) OnboardingFlow.SignUp else OnboardingFlow.SignInSignUp + private fun splashSubmit(isAlreadyHaveAccountEnabled: Boolean) { + val getStartedFlow = if (isAlreadyHaveAccountEnabled) OnboardingFlow.SignUp else OnboardingFlow.SignInSignUp viewModel.handle(OnboardingAction.OnGetStarted(resetLoginConfig = false, onboardingFlow = getStartedFlow)) } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthUseCaseFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthUseCaseFragment.kt new file mode 100644 index 0000000000..5325b25e93 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthUseCaseFragment.kt @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2021 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.onboarding.ftueauth + +import android.graphics.Color +import android.graphics.drawable.Drawable +import android.graphics.drawable.LayerDrawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.core.content.ContextCompat +import im.vector.app.R +import im.vector.app.core.extensions.getResTintedDrawable +import im.vector.app.core.extensions.getTintedDrawable +import im.vector.app.core.extensions.setLeftDrawable +import im.vector.app.core.extensions.setTextWithColoredPart +import im.vector.app.databinding.FragmentFtueAuthUseCaseBinding +import im.vector.app.features.login.ServerType +import im.vector.app.features.onboarding.FtueUseCase +import im.vector.app.features.onboarding.OnboardingAction +import im.vector.app.features.themes.ThemeProvider +import javax.inject.Inject + +private const val DARK_MODE_ICON_BACKGROUND_ALPHA = 0.30f +private const val LIGHT_MODE_ICON_BACKGROUND_ALPHA = 0.15f + +class FtueAuthUseCaseFragment @Inject constructor( + private val themeProvider: ThemeProvider +) : AbstractFtueAuthFragment() { + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueAuthUseCaseBinding { + return FragmentFtueAuthUseCaseBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupViews() + } + + private fun setupViews() { + views.useCaseOptionOne.renderUseCase( + useCase = FtueUseCase.FRIENDS_FAMILY, + label = R.string.ftue_auth_use_case_option_one, + icon = R.drawable.ic_use_case_friends, + tint = R.color.palette_grape + ) + views.useCaseOptionTwo.renderUseCase( + useCase = FtueUseCase.TEAMS, + label = R.string.ftue_auth_use_case_option_two, + icon = R.drawable.ic_use_case_teams, + tint = R.color.palette_element_green + ) + views.useCaseOptionThree.renderUseCase( + useCase = FtueUseCase.COMMUNITIES, + label = R.string.ftue_auth_use_case_option_three, + icon = R.drawable.ic_use_case_communities, + tint = R.color.palette_azure + ) + + views.useCaseSkip.setTextWithColoredPart( + fullTextRes = R.string.ftue_auth_use_case_skip, + coloredTextRes = R.string.ftue_auth_use_case_skip_partial, + underline = false, + colorAttribute = R.attr.colorAccent, + onClick = { viewModel.handle(OnboardingAction.UpdateUseCase(FtueUseCase.SKIP)) } + ) + + views.useCaseConnectToServer.setOnClickListener { + viewModel.handle(OnboardingAction.UpdateServerType(ServerType.Other)) + } + } + + override fun resetViewModel() { + viewModel.handle(OnboardingAction.ResetUseCase) + } + + private fun TextView.renderUseCase(useCase: FtueUseCase, @StringRes label: Int, @DrawableRes icon: Int, @ColorRes tint: Int) { + setLeftDrawable(createIcon(tint, icon, isLightMode = themeProvider.isLightTheme())) + setText(label) + debouncedClicks { + viewModel.handle(OnboardingAction.UpdateUseCase(useCase)) + } + } + + private fun createIcon(@ColorRes tint: Int, icon: Int, isLightMode: Boolean): Drawable { + val context = requireContext() + val alpha = when (isLightMode) { + true -> LIGHT_MODE_ICON_BACKGROUND_ALPHA + false -> DARK_MODE_ICON_BACKGROUND_ALPHA + } + val iconBackground = context.getResTintedDrawable(R.drawable.bg_feature_icon, tint, alpha = alpha) + val whiteLayer = context.getTintedDrawable(R.drawable.bg_feature_icon, Color.WHITE) + return LayerDrawable(arrayOf(whiteLayer, iconBackground, ContextCompat.getDrawable(context, icon))) + } +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt index f177eda114..33d57dd95c 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt @@ -15,6 +15,7 @@ */ package im.vector.app.features.onboarding.ftueauth + import android.content.Intent import android.view.View import android.view.ViewGroup @@ -31,6 +32,7 @@ import im.vector.app.core.extensions.POP_BACK_STACK_EXCLUSIVE import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragmentToBackstack import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.platform.ScreenOrientationLocker import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityLoginBinding import im.vector.app.features.VectorFeatures @@ -62,7 +64,8 @@ class FtueAuthVariant( private val onboardingViewModel: OnboardingViewModel, private val activity: VectorBaseActivity, private val supportFragmentManager: FragmentManager, - private val vectorFeatures: VectorFeatures + private val vectorFeatures: VectorFeatures, + private val orientationLocker: ScreenOrientationLocker, ) : OnboardingVariant { private val enterAnim = R.anim.enter_fade_in @@ -91,6 +94,7 @@ class FtueAuthVariant( } with(activity) { + orientationLocker.lockPhonesToPortrait(this) onboardingViewModel.onEach { updateWithState(it) } @@ -109,7 +113,7 @@ class FtueAuthVariant( } private fun addFirstFragment() { - val splashFragment = when (vectorFeatures.isSplashCarouselEnabled()) { + val splashFragment = when (vectorFeatures.isOnboardingSplashCarouselEnabled()) { true -> FtueAuthSplashCarouselFragment::class.java else -> FtueAuthSplashFragment::class.java } @@ -151,13 +155,16 @@ class FtueAuthVariant( activity.addFragmentToBackstack(views.loginFragmentContainer, FtueAuthServerSelectionFragment::class.java, option = { ft -> - activity.findViewById(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } - // Disable transition of text - // findViewById(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } - // No transition here now actually - // findViewById(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } - // TODO Disabled because it provokes a flickering - // ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) + if (vectorFeatures.isOnboardingUseCaseEnabled()) { + ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) + } else { + activity.findViewById(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + // TODO Disabled because it provokes a flickering + // Disable transition of text + // findViewById(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + // No transition here now actually + // findViewById(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + } }) is OnboardingViewEvents.OnServerSelectionDone -> onServerSelectionDone(viewEvents) is OnboardingViewEvents.OnSignModeSelected -> onSignModeSelected(viewEvents) @@ -208,6 +215,11 @@ class FtueAuthVariant( is OnboardingViewEvents.Loading -> // This is handled by the Fragments Unit + OnboardingViewEvents.OpenUseCaseSelection -> { + activity.addFragmentToBackstack(views.loginFragmentContainer, + FtueAuthUseCaseFragment::class.java, + option = commonOption) + } }.exhaustive } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWebFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWebFragment.kt index d479439a0f..879830a1c0 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWebFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWebFragment.kt @@ -69,6 +69,7 @@ class FtueAuthWebFragment @Inject constructor( super.onViewCreated(view, savedInstanceState) setupToolbar(views.loginWebToolbar) + .allowBack() } override fun updateWithState(state: OnboardingViewState) { @@ -83,7 +84,7 @@ class FtueAuthWebFragment @Inject constructor( } private fun setupTitle(state: OnboardingViewState) { - views.loginWebToolbar.title = when (state.signMode) { + toolbar?.title = when (state.signMode) { SignMode.SignIn -> getString(R.string.login_signin) else -> getString(R.string.login_signup) } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/SplashCarouselItem.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/SplashCarouselItem.kt index 1230d8109a..dc56820424 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/SplashCarouselItem.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/SplashCarouselItem.kt @@ -35,7 +35,7 @@ abstract class SplashCarouselItem : VectorEpoxyModel( holder.view.setBackgroundResource(item.pageBackground) holder.image.setImageResource(item.image) - holder.title.setText(item.title) + holder.title.text = item.title.charSequence holder.body.setText(item.body) } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/SplashCarouselState.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/SplashCarouselState.kt index c86e78d139..7f68cef307 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/SplashCarouselState.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/SplashCarouselState.kt @@ -18,38 +18,13 @@ package im.vector.app.features.onboarding.ftueauth import androidx.annotation.DrawableRes import androidx.annotation.StringRes -import im.vector.app.R +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence data class SplashCarouselState( - val items: List = listOf( - Item( - R.string.ftue_auth_carousel_1_title, - R.string.ftue_auth_carousel_1_body, - R.drawable.onboarding_carousel_conversations, - R.drawable.bg_carousel_page_1 - ), - Item( - R.string.ftue_auth_carousel_2_title, - R.string.ftue_auth_carousel_2_body, - R.drawable.onboarding_carousel_ems, - R.drawable.bg_carousel_page_2 - ), - Item( - R.string.ftue_auth_carousel_3_title, - R.string.ftue_auth_carousel_3_body, - R.drawable.onboarding_carousel_connect, - R.drawable.bg_carousel_page_3 - ), - Item( - R.string.ftue_auth_carousel_4_title, - R.string.ftue_auth_carousel_4_body, - R.drawable.onboarding_carousel_universal, - R.drawable.bg_carousel_page_4 - ) - ) + val items: List ) { data class Item( - @StringRes val title: Int, + val title: EpoxyCharSequence, @StringRes val body: Int, @DrawableRes val image: Int, @DrawableRes val pageBackground: Int diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/SplashCarouselStateFactory.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/SplashCarouselStateFactory.kt new file mode 100644 index 0000000000..da5f8b6379 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/SplashCarouselStateFactory.kt @@ -0,0 +1,94 @@ +/* + * 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.onboarding.ftueauth + +import android.content.Context +import androidx.annotation.AttrRes +import androidx.annotation.DrawableRes +import im.vector.app.R +import im.vector.app.core.resources.LocaleProvider +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.resources.isEnglishSpeaking +import im.vector.app.features.themes.ThemeProvider +import im.vector.app.features.themes.ThemeUtils +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence +import me.gujun.android.span.span +import javax.inject.Inject + +class SplashCarouselStateFactory @Inject constructor( + private val context: Context, + private val stringProvider: StringProvider, + private val localeProvider: LocaleProvider, + private val themeProvider: ThemeProvider, +) { + + fun create(): SplashCarouselState { + val lightTheme = themeProvider.isLightTheme() + fun background(@DrawableRes lightDrawable: Int) = if (lightTheme) lightDrawable else R.drawable.bg_carousel_page_dark + fun hero(@DrawableRes lightDrawable: Int, @DrawableRes darkDrawable: Int) = if (lightTheme) lightDrawable else darkDrawable + return SplashCarouselState(listOf( + SplashCarouselState.Item( + R.string.ftue_auth_carousel_1_title.colorTerminatingFullStop(R.attr.colorAccent), + R.string.ftue_auth_carousel_body_secure, + hero(R.drawable.ic_splash_conversations, R.drawable.ic_splash_conversations_dark), + background(R.drawable.bg_carousel_page_1) + ), + SplashCarouselState.Item( + R.string.ftue_auth_carousel_2_title.colorTerminatingFullStop(R.attr.colorAccent), + R.string.ftue_auth_carousel_body_control, + hero(R.drawable.ic_splash_control, R.drawable.ic_splash_control_dark), + background(R.drawable.bg_carousel_page_2) + ), + SplashCarouselState.Item( + R.string.ftue_auth_carousel_3_title.colorTerminatingFullStop(R.attr.colorAccent), + R.string.ftue_auth_carousel_body_encrypted, + hero(R.drawable.ic_splash_secure, R.drawable.ic_splash_secure_dark), + background(R.drawable.bg_carousel_page_3) + ), + SplashCarouselState.Item( + collaborationTitle().colorTerminatingFullStop(R.attr.colorAccent), + R.string.ftue_auth_carousel_body_workplace, + hero(R.drawable.ic_splash_collaboration, R.drawable.ic_splash_collaboration_dark), + background(R.drawable.bg_carousel_page_4) + ) + )) + } + + private fun collaborationTitle(): Int { + return when { + localeProvider.isEnglishSpeaking() -> R.string.cut_the_slack_from_teams + else -> R.string.ftue_auth_carousel_title_messaging + } + } + + private fun Int.colorTerminatingFullStop(@AttrRes color: Int): EpoxyCharSequence { + val string = stringProvider.getString(this) + val fullStop = "." + val charSequence = if (string.endsWith(fullStop)) { + span { + +string.removeSuffix(fullStop) + span(fullStop) { + textColor = ThemeUtils.getColor(context, color) + } + } + } else { + string + } + return charSequence.toEpoxyCharSequence() + } +} diff --git a/vector/src/main/java/im/vector/app/features/pin/PinActivity.kt b/vector/src/main/java/im/vector/app/features/pin/PinActivity.kt index ef79799074..faf15d8006 100644 --- a/vector/src/main/java/im/vector/app/features/pin/PinActivity.kt +++ b/vector/src/main/java/im/vector/app/features/pin/PinActivity.kt @@ -19,15 +19,13 @@ package im.vector.app.features.pin import android.content.Context import android.content.Intent import com.airbnb.mvrx.Mavericks -import com.google.android.material.appbar.MaterialToolbar import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.addFragment -import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleBinding @AndroidEntryPoint -class PinActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedActivity { +class PinActivity : VectorBaseActivity(), UnlockedActivity { companion object { fun newIntent(context: Context, args: PinArgs): Intent { @@ -47,8 +45,4 @@ class PinActivity : VectorBaseActivity(), ToolbarConfigur addFragment(views.simpleFragmentContainer, PinFragment::class.java, fragmentArgs) } } - - override fun configure(toolbar: MaterialToolbar) { - configureToolbar(toolbar) - } } diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollAction.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollAction.kt index 182750fbd2..5fddcac568 100644 --- a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollAction.kt +++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollAction.kt @@ -17,11 +17,13 @@ package im.vector.app.features.poll.create import im.vector.app.core.platform.VectorViewModelAction +import org.matrix.android.sdk.api.session.room.model.message.PollType sealed class CreatePollAction : VectorViewModelAction { data class OnQuestionChanged(val question: String) : CreatePollAction() data class OnOptionChanged(val index: Int, val option: String) : CreatePollAction() data class OnDeleteOption(val index: Int) : CreatePollAction() + data class OnPollTypeChanged(val pollType: PollType) : CreatePollAction() object OnAddOption : CreatePollAction() object OnCreatePoll : CreatePollAction() } diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollController.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollController.kt index 90128d9263..d938b98eed 100644 --- a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollController.kt +++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollController.kt @@ -20,7 +20,6 @@ import android.view.Gravity import android.view.inputmethod.EditorInfo import com.airbnb.epoxy.EpoxyController import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.ItemStyle @@ -28,6 +27,8 @@ import im.vector.app.core.ui.list.genericButtonItem import im.vector.app.core.ui.list.genericItem import im.vector.app.features.form.formEditTextItem import im.vector.app.features.form.formEditTextWithDeleteItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence +import org.matrix.android.sdk.api.session.room.model.message.PollType import javax.inject.Inject class CreatePollController @Inject constructor( @@ -47,6 +48,28 @@ class CreatePollController @Inject constructor( val currentState = state ?: return val host = this + genericItem { + id("poll_type_title") + style(ItemStyle.BIG_TEXT) + title(host.stringProvider.getString(R.string.poll_type_title).toEpoxyCharSequence()) + } + + /* + pollTypeSelectionItem { + id("poll_type_selection") + pollType(currentState.pollType) + pollTypeChangedListener { _, id -> + host.callback?.onPollTypeChanged( + if (id == R.id.openPollTypeRadioButton) { + PollType.DISCLOSED + } else { + PollType.UNDISCLOSED + } + ) + } + } + */ + genericItem { id("question_title") style(ItemStyle.BIG_TEXT) @@ -110,5 +133,6 @@ class CreatePollController @Inject constructor( fun onOptionChanged(index: Int, option: String) fun onDeleteOption(index: Int) fun onAddOption() + fun onPollTypeChanged(type: PollType) } } diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollFragment.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollFragment.kt index 1d807654e8..4483b00158 100644 --- a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollFragment.kt +++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollFragment.kt @@ -23,18 +23,23 @@ import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.args import com.airbnb.mvrx.withState import im.vector.app.R import im.vector.app.core.extensions.configureWith +import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentCreatePollBinding import im.vector.app.features.poll.create.CreatePollViewModel.Companion.MAX_OPTIONS_COUNT import kotlinx.parcelize.Parcelize +import org.matrix.android.sdk.api.session.room.model.message.PollType import javax.inject.Inject @Parcelize data class CreatePollArgs( val roomId: String, + val editedEventId: String?, + val mode: PollMode ) : Parcelable class CreatePollFragment @Inject constructor( @@ -42,6 +47,7 @@ class CreatePollFragment @Inject constructor( ) : VectorBaseFragment(), CreatePollController.Callback { private val viewModel: CreatePollViewModel by activityViewModel() + private val args: CreatePollArgs by args() override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentCreatePollBinding { return FragmentCreatePollBinding.inflate(inflater, container, false) @@ -49,17 +55,26 @@ class CreatePollFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - vectorBaseActivity.setSupportActionBar(views.createPollToolbar) + + setupToolbar(views.createPollToolbar) + .allowBack(useCross = true) + + when (args.mode) { + PollMode.CREATE -> { + views.createPollToolbar.title = getString(R.string.create_poll_title) + views.createPollButton.text = getString(R.string.create_poll_title) + } + PollMode.EDIT -> { + views.createPollToolbar.title = getString(R.string.edit_poll_title) + views.createPollButton.text = getString(R.string.edit_poll_title) + } + }.exhaustive views.createPollRecyclerView.configureWith(controller, disableItemAnimation = true) // workaround for https://github.com/vector-im/element-android/issues/4735 - views.createPollRecyclerView.setItemViewCacheSize(MAX_OPTIONS_COUNT + 4) + views.createPollRecyclerView.setItemViewCacheSize(MAX_OPTIONS_COUNT + 6) controller.callback = this - views.createPollClose.debouncedClicks { - requireActivity().finish() - } - views.createPollButton.debouncedClicks { viewModel.handle(CreatePollAction.OnCreatePoll) } @@ -103,6 +118,10 @@ class CreatePollFragment @Inject constructor( } } + override fun onPollTypeChanged(type: PollType) { + viewModel.handle(CreatePollAction.OnPollTypeChanged(type)) + } + private fun handleSuccess() { requireActivity().finish() } diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt index b5e66ae682..7750e6d909 100644 --- a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt @@ -24,6 +24,9 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent +import org.matrix.android.sdk.api.session.room.model.message.PollType +import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent class CreatePollViewModel @AssistedInject constructor( @Assisted private val initialState: CreatePollViewState, @@ -45,6 +48,9 @@ class CreatePollViewModel @AssistedInject constructor( init { observeState() + initialState.editedEventId?.let { + initializeEditedPoll(it) + } } private fun observeState() { @@ -61,6 +67,23 @@ class CreatePollViewModel @AssistedInject constructor( } } + private fun initializeEditedPoll(eventId: String) { + val event = room.getTimeLineEvent(eventId) ?: return + val content = event.getLastMessageContent() as? MessagePollContent ?: return + + val pollType = content.pollCreationInfo?.kind ?: PollType.DISCLOSED + val question = content.pollCreationInfo?.question?.question ?: "" + val options = content.pollCreationInfo?.answers?.mapNotNull { it.answer } ?: List(MIN_OPTIONS_COUNT) { "" } + + setState { + copy( + question = question, + options = options, + pollType = pollType + ) + } + } + override fun handle(action: CreatePollAction) { when (action) { CreatePollAction.OnCreatePoll -> handleOnCreatePoll() @@ -68,6 +91,7 @@ class CreatePollViewModel @AssistedInject constructor( is CreatePollAction.OnDeleteOption -> handleOnDeleteOption(action.index) is CreatePollAction.OnOptionChanged -> handleOnOptionChanged(action.index, action.option) is CreatePollAction.OnQuestionChanged -> handleOnQuestionChanged(action.question) + is CreatePollAction.OnPollTypeChanged -> handleOnPollTypeChanged(action.pollType) } } @@ -81,12 +105,20 @@ class CreatePollViewModel @AssistedInject constructor( _viewEvents.post(CreatePollViewEvents.NotEnoughOptionsError(requiredOptionsCount = MIN_OPTIONS_COUNT)) } else -> { - room.sendPoll(state.question, nonEmptyOptions) + when (state.mode) { + PollMode.CREATE -> room.sendPoll(state.pollType, state.question, nonEmptyOptions) + PollMode.EDIT -> sendEditedPoll(state.editedEventId!!, state.pollType, state.question, nonEmptyOptions) + } _viewEvents.post(CreatePollViewEvents.Success) } } } + private fun sendEditedPoll(editedEventId: String, pollType: PollType, question: String, options: List) { + val editedEvent = room.getTimeLineEvent(editedEventId) ?: return + room.editPoll(editedEvent, pollType, question, options) + } + private fun handleOnAddOption() { setState { val extendedOptions = options + "" @@ -122,6 +154,14 @@ class CreatePollViewModel @AssistedInject constructor( } } + private fun handleOnPollTypeChanged(pollType: PollType) { + setState { + copy( + pollType = pollType + ) + } + } + private fun canCreatePoll(question: String, options: List): Boolean { return question.isNotEmpty() && options.filter { it.isNotEmpty() }.size >= MIN_OPTIONS_COUNT diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt index a9060cc89f..175d1b0116 100644 --- a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt +++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt @@ -17,16 +17,22 @@ package im.vector.app.features.poll.create import com.airbnb.mvrx.MavericksState +import org.matrix.android.sdk.api.session.room.model.message.PollType data class CreatePollViewState( val roomId: String, + val editedEventId: String?, + val mode: PollMode, val question: String = "", val options: List = List(CreatePollViewModel.MIN_OPTIONS_COUNT) { "" }, val canCreatePoll: Boolean = false, - val canAddMoreOptions: Boolean = true + val canAddMoreOptions: Boolean = true, + val pollType: PollType = PollType.DISCLOSED ) : MavericksState { constructor(args: CreatePollArgs) : this( - roomId = args.roomId + roomId = args.roomId, + editedEventId = args.editedEventId, + mode = args.mode ) } diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/SafeCharSequence.kt b/vector/src/main/java/im/vector/app/features/poll/create/PollMode.kt similarity index 55% rename from library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/SafeCharSequence.kt rename to vector/src/main/java/im/vector/app/features/poll/create/PollMode.kt index 79556f81d7..0007589d10 100644 --- a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/SafeCharSequence.kt +++ b/vector/src/main/java/im/vector/app/features/poll/create/PollMode.kt @@ -14,17 +14,9 @@ * limitations under the License. */ -package org.billcarsonfr.jsonviewer +package im.vector.app.features.poll.create -/** - * Wrapper for a CharSequence, which support mutation of the CharSequence, which can happen during rendering - * TODO Mutualize - */ -internal class SafeCharSequence(val charSequence: CharSequence) { - private val hash = charSequence.toString().hashCode() - - override fun hashCode() = hash - override fun equals(other: Any?) = other is SafeCharSequence && other.hash == hash +enum class PollMode { + CREATE, + EDIT } - -internal fun CharSequence.toSafeCharSequence() = SafeCharSequence(this) diff --git a/vector/src/main/java/im/vector/app/features/poll/create/PollTypeSelectionItem.kt b/vector/src/main/java/im/vector/app/features/poll/create/PollTypeSelectionItem.kt new file mode 100644 index 0000000000..1b24a70cb9 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/poll/create/PollTypeSelectionItem.kt @@ -0,0 +1,57 @@ +/* + * 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.poll.create + +import android.widget.RadioGroup +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import org.matrix.android.sdk.api.session.room.model.message.PollType + +@EpoxyModelClass(layout = R.layout.item_poll_type_selection) +abstract class PollTypeSelectionItem : VectorEpoxyModel() { + + @EpoxyAttribute + var pollType: PollType = PollType.DISCLOSED + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var pollTypeChangedListener: RadioGroup.OnCheckedChangeListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + + holder.pollTypeRadioGroup.check( + when (pollType) { + PollType.DISCLOSED -> R.id.openPollTypeRadioButton + PollType.UNDISCLOSED -> R.id.closedPollTypeRadioButton + } + ) + + holder.pollTypeRadioGroup.setOnCheckedChangeListener(pollTypeChangedListener) + } + + override fun unbind(holder: Holder) { + super.unbind(holder) + holder.pollTypeRadioGroup.setOnCheckedChangeListener(null) + } + + class Holder : VectorEpoxyHolder() { + val pollTypeRadioGroup by bind(R.id.pollTypeRadioGroup) + } +} diff --git a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt index 22bbabf9e3..ae03b5345a 100644 --- a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt +++ b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt @@ -25,6 +25,7 @@ import com.tapadoo.alerter.Alerter import im.vector.app.R import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.isAnimationDisabled +import im.vector.app.features.analytics.ui.consent.AnalyticsOptInActivity import im.vector.app.features.pin.PinActivity import im.vector.app.features.signout.hard.SignedOutActivity import im.vector.app.features.themes.ThemeUtils @@ -300,6 +301,7 @@ class PopupAlertManager @Inject constructor() { return alert != null && activity !is PinActivity && activity !is SignedOutActivity && + activity !is AnalyticsOptInActivity && activity is VectorBaseActivity<*> && alert.shouldBeDisplayedIn.invoke(activity) } diff --git a/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerFragment.kt b/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerFragment.kt index 95d3ce4a07..a7231a0c5b 100644 --- a/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerFragment.kt @@ -38,10 +38,9 @@ class QrCodeScannerFragment @Inject constructor() : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - views.qrScannerClose.debouncedClicks { - requireActivity().onBackPressed() - } - views.qrScannerTitle.text = getString(R.string.verification_scan_their_code) + setupToolbar(views.qrScannerToolbar) + .setTitle(R.string.verification_scan_their_code) + .allowBack(useCross = true) } override fun onResume() { diff --git a/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt b/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt index 02df86e14b..0aec24f4ac 100755 --- a/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt +++ b/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt @@ -45,7 +45,8 @@ class BugReportActivity : VectorBaseActivity() { private var reportType: ReportType = ReportType.BUG_REPORT override fun initUiAndData() { - configureToolbar(views.bugReportToolbar) + setupToolbar(views.bugReportToolbar) + .allowBack() setupViews() if (bugReporter.screenshot != null) { diff --git a/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt b/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt index 26e9cabccb..b62a182fd8 100755 --- a/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt +++ b/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt @@ -259,13 +259,13 @@ class BugReporter @Inject constructor( ReportType.SUGGESTION -> "[Element] [Suggestion] $bugDescription" ReportType.SPACE_BETA_FEEDBACK -> "[Element] [spaces-feedback] $bugDescription" ReportType.AUTO_UISI_SENDER, - ReportType.AUTO_UISI -> "[AutoUISI] $bugDescription" + ReportType.AUTO_UISI -> bugDescription } // build the multi part request val builder = BugReporterMultipartBody.Builder() .addFormDataPart("text", text) - .addFormDataPart("app", "riot-android") + .addFormDataPart("app", rageShakeAppNameForReport(context, reportType)) .addFormDataPart("user_agent", Matrix.getInstance(context).getUserAgent()) .addFormDataPart("user_id", userId) .addFormDataPart("can_contact", canContact.toString()) @@ -340,9 +340,15 @@ class BugReporter @Inject constructor( } ReportType.SUGGESTION -> builder.addFormDataPart("label", "[Suggestion]") ReportType.SPACE_BETA_FEEDBACK -> builder.addFormDataPart("label", "spaces-feedback") - ReportType.AUTO_UISI, + ReportType.AUTO_UISI -> { + builder.addFormDataPart("label", "Z-UISI") + builder.addFormDataPart("label", "android") + builder.addFormDataPart("label", "uisi-recipient") + } ReportType.AUTO_UISI_SENDER -> { builder.addFormDataPart("label", "Z-UISI") + builder.addFormDataPart("label", "android") + builder.addFormDataPart("label", "uisi-sender") } } @@ -481,6 +487,21 @@ class BugReporter @Inject constructor( activity.startActivity(BugReportActivity.intent(activity, reportType)) } + private fun rageShakeAppNameForReport(context: Context, reportType: ReportType): String { + // As per https://github.com/matrix-org/rageshake + // app: Identifier for the application (eg 'riot-web'). + // Should correspond to a mapping configured in the configuration file for github issue reporting to work. + // (see R.string.bug_report_url for configured RS server) + return when (reportType) { + ReportType.AUTO_UISI_SENDER, + ReportType.AUTO_UISI -> { + context.getString(R.string.bug_report_auto_uisi_app_name) + } + else -> { + context.getString(R.string.bug_report_app_name) + } + } + } // ============================================================================================================== // crash report management // ============================================================================================================== diff --git a/vector/src/main/java/im/vector/app/features/reactions/EmojiReactionPickerActivity.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiReactionPickerActivity.kt index d377c74ad7..7062a5d02d 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/EmojiReactionPickerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/EmojiReactionPickerActivity.kt @@ -32,10 +32,10 @@ import dagger.hilt.android.AndroidEntryPoint import im.vector.app.EmojiCompatFontProvider import im.vector.app.R import im.vector.app.core.extensions.observeEvent -import im.vector.app.core.flow.throttleFirst import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityEmojiReactionPickerBinding import im.vector.app.features.reactions.data.EmojiDataSource +import im.vector.lib.core.utils.flow.throttleFirst import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -79,7 +79,8 @@ class EmojiReactionPickerActivity : VectorBaseActivity { val message = senderDisplayName?.let { - stringProvider.getString(R.string.has_been_kicked, roomSummary.displayName, it) + stringProvider.getString(R.string.has_been_removed, roomSummary.displayName, it) } RequireActiveMembershipViewEvents.RoomLeft(message) } Membership.KNOCK -> { val message = senderDisplayName?.let { - stringProvider.getString(R.string.has_been_kicked, roomSummary.displayName, it) + stringProvider.getString(R.string.has_been_removed, roomSummary.displayName, it) } RequireActiveMembershipViewEvents.RoomLeft(message) } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt index be1523f4ab..b6b8aa9653 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt @@ -69,12 +69,8 @@ class PublicRoomsFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - vectorBaseActivity.setSupportActionBar(views.publicRoomsToolbar) - - vectorBaseActivity.supportActionBar?.let { - it.setDisplayShowHomeEnabled(true) - it.setDisplayHomeAsUpEnabled(true) - } + setupToolbar(views.publicRoomsToolbar) + .allowBack() sharedActionViewModel = activityViewModelProvider.get(RoomDirectorySharedActionViewModel::class.java) setupRecyclerView() @@ -160,7 +156,7 @@ class PublicRoomsFragment @Inject constructor( override fun onPublicRoomJoin(publicRoom: PublicRoom) { Timber.v("PublicRoomJoinClicked: $publicRoom") - viewModel.handle(RoomDirectoryAction.JoinRoom(publicRoom.roomId)) + viewModel.handle(RoomDirectoryAction.JoinRoom(publicRoom)) } override fun loadMore() { diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryAction.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryAction.kt index 77eec57ab3..d95a4cf792 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryAction.kt @@ -17,10 +17,11 @@ package im.vector.app.features.roomdirectory import im.vector.app.core.platform.VectorViewModelAction +import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom sealed class RoomDirectoryAction : VectorViewModelAction { data class SetRoomDirectoryData(val roomDirectoryData: RoomDirectoryData) : RoomDirectoryAction() data class FilterWith(val filter: String) : RoomDirectoryAction() object LoadMore : RoomDirectoryAction() - data class JoinRoom(val roomId: String) : RoomDirectoryAction() + data class JoinRoom(val publicRoom: PublicRoom) : RoomDirectoryAction() } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt index a52732d790..3cd9955e74 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt @@ -28,6 +28,7 @@ import im.vector.app.core.extensions.addFragmentToBackstack import im.vector.app.core.extensions.popBackstack import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleBinding +import im.vector.app.features.analytics.plan.Screen import im.vector.app.features.matrixto.MatrixToBottomSheet import im.vector.app.features.navigation.Navigator import im.vector.app.features.roomdirectory.createroom.CreateRoomArgs @@ -50,6 +51,7 @@ class RoomDirectoryActivity : VectorBaseActivity(), Matri override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + analyticsScreenName = Screen.ScreenName.RoomDirectory sharedActionViewModel = viewModelProvider.get(RoomDirectorySharedActionViewModel::class.java) if (isFirstCreation()) { diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectorySharedAction.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectorySharedAction.kt index 9911ce6686..ea9211cc7b 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectorySharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectorySharedAction.kt @@ -25,5 +25,6 @@ sealed class RoomDirectorySharedAction : VectorSharedAction { object Back : RoomDirectorySharedAction() object CreateRoom : RoomDirectorySharedAction() object Close : RoomDirectorySharedAction() + data class CreateRoomSuccess(val createdRoomId: String) : RoomDirectorySharedAction() object ChangeProtocol : RoomDirectorySharedAction() } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt index 431dba21cf..710d4d5b5f 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt @@ -27,6 +27,8 @@ import dagger.assisted.AssistedInject 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.AnalyticsTracker +import im.vector.app.features.analytics.extensions.toAnalyticsJoinedRoom import im.vector.app.features.settings.VectorPreferences import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Job @@ -45,6 +47,7 @@ class RoomDirectoryViewModel @AssistedInject constructor( @Assisted initialState: PublicRoomsViewState, vectorPreferences: VectorPreferences, private val session: Session, + private val analyticsTracker: AnalyticsTracker, private val explicitTermFilter: ExplicitTermFilter ) : VectorViewModel(initialState) { @@ -213,7 +216,7 @@ class RoomDirectoryViewModel @AssistedInject constructor( } private fun joinRoom(action: RoomDirectoryAction.JoinRoom) = withState { state -> - val roomMembershipChange = state.changeMembershipStates[action.roomId] + val roomMembershipChange = state.changeMembershipStates[action.publicRoom.roomId] if (roomMembershipChange?.isInProgress().orFalse()) { // Request already sent, should not happen Timber.w("Try to join an already joining room. Should not happen") @@ -222,7 +225,8 @@ class RoomDirectoryViewModel @AssistedInject constructor( val viaServers = listOfNotNull(state.roomDirectoryData.homeServer) viewModelScope.launch { try { - session.joinRoom(action.roomId, viaServers = viaServers) + session.joinRoom(action.publicRoom.roomId, viaServers = viaServers) + analyticsTracker.capture(action.publicRoom.toAnalyticsJoinedRoom()) // We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data. // Instead, we wait for the room to be joined } catch (failure: Throwable) { diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomActivity.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomActivity.kt index e9762d09d3..339c819a65 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomActivity.kt @@ -16,16 +16,17 @@ package im.vector.app.features.roomdirectory.createroom +import android.app.Activity import android.content.Context import android.content.Intent import android.os.Bundle import androidx.lifecycle.lifecycleScope -import com.google.android.material.appbar.MaterialToolbar +import com.airbnb.mvrx.Mavericks import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.addFragment -import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleBinding +import im.vector.app.features.analytics.plan.Screen import im.vector.app.features.roomdirectory.RoomDirectorySharedAction import im.vector.app.features.roomdirectory.RoomDirectorySharedActionViewModel import kotlinx.coroutines.flow.launchIn @@ -35,7 +36,7 @@ import kotlinx.coroutines.flow.onEach * Simple container for [CreateRoomFragment] */ @AndroidEntryPoint -class CreateRoomActivity : VectorBaseActivity(), ToolbarConfigurable { +class CreateRoomActivity : VectorBaseActivity() { private lateinit var sharedActionViewModel: RoomDirectorySharedActionViewModel @@ -43,46 +44,60 @@ class CreateRoomActivity : VectorBaseActivity(), ToolbarC override fun getCoordinatorLayout() = views.coordinatorLayout - override fun configure(toolbar: MaterialToolbar) { - configureToolbar(toolbar) - } - override fun initUiAndData() { if (isFirstCreation()) { + val fragmentArgs: CreateRoomArgs = intent?.extras?.getParcelable(Mavericks.KEY_ARG) ?: return addFragment( views.simpleFragmentContainer, CreateRoomFragment::class.java, - CreateRoomArgs( - intent?.getStringExtra(INITIAL_NAME) ?: "", - isSpace = intent?.getBooleanExtra(IS_SPACE, false) ?: false - ) + fragmentArgs ) } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + analyticsScreenName = Screen.ScreenName.CreateRoom sharedActionViewModel = viewModelProvider.get(RoomDirectorySharedActionViewModel::class.java) sharedActionViewModel .stream() .onEach { sharedAction -> when (sharedAction) { is RoomDirectorySharedAction.Back, - is RoomDirectorySharedAction.Close -> finish() + is RoomDirectorySharedAction.Close -> finish() + is RoomDirectorySharedAction.CreateRoomSuccess -> { + setResult(Activity.RESULT_OK, Intent().apply { putExtra(RESULT_CREATED_ROOM_ID, sharedAction.createdRoomId) }) + finish() + } + else -> { + // nop + } } } .launchIn(lifecycleScope) } companion object { - private const val INITIAL_NAME = "INITIAL_NAME" - private const val IS_SPACE = "IS_SPACE" - fun getIntent(context: Context, initialName: String = "", isSpace: Boolean = false): Intent { + private const val RESULT_CREATED_ROOM_ID = "RESULT_CREATED_ROOM_ID" + + fun getIntent(context: Context, + initialName: String = "", + isSpace: Boolean = false, + openAfterCreate: Boolean = true, + currentSpaceId: String? = null): Intent { return Intent(context, CreateRoomActivity::class.java).apply { - putExtra(INITIAL_NAME, initialName) - putExtra(IS_SPACE, isSpace) + putExtra(Mavericks.KEY_ARG, CreateRoomArgs( + initialName = initialName, + isSpace = isSpace, + openAfterCreate = openAfterCreate, + parentSpaceId = currentSpaceId + )) } } + + fun getCreatedRoomId(data: Intent?): String? { + return data?.extras?.getString(RESULT_CREATED_ROOM_ID) + } } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt index 1244a0f64e..2bd41ae3af 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt @@ -56,7 +56,8 @@ import javax.inject.Inject data class CreateRoomArgs( val initialName: String, val parentSpaceId: String? = null, - val isSpace: Boolean = false + val isSpace: Boolean = false, + val openAfterCreate: Boolean = true ) : Parcelable class CreateRoomFragment @Inject constructor( @@ -82,14 +83,13 @@ class CreateRoomFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - vectorBaseActivity.setSupportActionBar(views.createRoomToolbar) sharedActionViewModel = activityViewModelProvider.get(RoomDirectorySharedActionViewModel::class.java) setupRoomJoinRuleSharedActionViewModel() setupWaitingView() setupRecyclerView() - views.createRoomClose.debouncedClicks { - sharedActionViewModel.post(RoomDirectorySharedAction.Back) - } + setupToolbar(views.createRoomToolbar) + .setTitle(if (args.isSpace) R.string.create_new_space else R.string.create_new_room) + .allowBack(useCross = true) viewModel.observeViewEvents { when (it) { CreateRoomViewEvents.Quit -> vectorBaseActivity.onBackPressed() @@ -98,11 +98,6 @@ class CreateRoomFragment @Inject constructor( } } - override fun onResume() { - super.onResume() - views.createRoomTitle.text = getString(if (args.isSpace) R.string.create_new_space else R.string.create_new_room) - } - private fun setupRoomJoinRuleSharedActionViewModel() { roomJoinRuleSharedActionViewModel = activityViewModelProvider.get(RoomJoinRuleSharedActionViewModel::class.java) roomJoinRuleSharedActionViewModel @@ -226,16 +221,19 @@ class CreateRoomFragment @Inject constructor( views.waitingView.root.isVisible = async is Loading if (async is Success) { // Navigate to freshly created room - if (state.isSubSpace) { - navigator.switchToSpace( - requireContext(), - async(), - Navigator.PostSwitchSpaceAction.None - ) - } else { - navigator.openRoom(requireActivity(), async()) + if (state.openAfterCreate) { + if (state.isSubSpace) { + navigator.switchToSpace( + requireContext(), + async(), + Navigator.PostSwitchSpaceAction.None + ) + } else { + navigator.openRoom(requireActivity(), async()) + } } + sharedActionViewModel.post(RoomDirectorySharedAction.CreateRoomSuccess(async())) sharedActionViewModel.post(RoomDirectorySharedAction.Close) } else { // Populate list with Epoxy diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt index 4835cfecbe..3b2e9de2d1 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt @@ -25,10 +25,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.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.analytics.AnalyticsTracker +import im.vector.app.features.analytics.plan.CreatedRoom import im.vector.app.features.raw.wellknown.getElementWellknown import im.vector.app.features.raw.wellknown.isE2EByDefault import kotlinx.coroutines.Dispatchers @@ -51,9 +54,12 @@ import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset import org.matrix.android.sdk.api.session.room.model.create.RestrictedRoomPreset import timber.log.Timber -class CreateRoomViewModel @AssistedInject constructor(@Assisted private val initialState: CreateRoomViewState, - private val session: Session, - private val rawService: RawService +class CreateRoomViewModel @AssistedInject constructor( + @Assisted private val initialState: CreateRoomViewState, + private val session: Session, + private val rawService: RawService, + appStateHandler: AppStateHandler, + private val analyticsTracker: AnalyticsTracker ) : VectorViewModel(initialState) { @AssistedFactory @@ -67,10 +73,12 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted private val init initHomeServerName() initAdminE2eByDefault() + val parentSpaceId = initialState.parentSpaceId ?: appStateHandler.safeActiveSpaceId() + val restrictedSupport = session.getHomeServerCapabilities().isFeatureSupported(HomeServerCapabilities.ROOM_CAP_RESTRICTED) val createRestricted = restrictedSupport == HomeServerCapabilities.RoomCapabilitySupport.SUPPORTED - val defaultJoinRules = if (initialState.parentSpaceId != null && createRestricted) { + val defaultJoinRules = if (parentSpaceId != null && createRestricted) { RoomJoinRules.RESTRICTED } else { RoomJoinRules.INVITE @@ -78,9 +86,10 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted private val init setState { copy( + parentSpaceId = parentSpaceId, supportsRestricted = createRestricted, roomJoinRules = defaultJoinRules, - parentSpaceSummary = initialState.parentSpaceId?.let { session.getRoomSummary(it) } + parentSpaceSummary = parentSpaceId?.let { session.getRoomSummary(it) } ) } } @@ -156,7 +165,7 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted private val init CreateRoomViewState( isEncrypted = adminE2EByDefault, hsAdminHasDisabledE2E = !adminE2EByDefault, - parentSpaceId = initialState.parentSpaceId + parentSpaceId = this.parentSpaceId ) } @@ -291,12 +300,12 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted private val init viewModelScope.launch { runCatching { session.createRoom(createRoomParams) }.fold( { roomId -> - - if (initialState.parentSpaceId != null) { + analyticsTracker.capture(CreatedRoom(isDM = createRoomParams.isDirect.orFalse())) + if (state.parentSpaceId != null) { // add it as a child try { session.spaceService() - .getSpace(initialState.parentSpaceId) + .getSpace(state.parentSpaceId) ?.addChildren(roomId, viaServers = null, order = null) } catch (failure: Throwable) { Timber.w(failure, "Failed to add as a child") diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt index 389d365875..cf8cc669ab 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt @@ -39,13 +39,15 @@ data class CreateRoomViewState( val parentSpaceSummary: RoomSummary? = null, val supportsRestricted: Boolean = false, val aliasLocalPart: String? = null, - val isSubSpace: Boolean = false + val isSubSpace: Boolean = false, + val openAfterCreate: Boolean = true ) : MavericksState { constructor(args: CreateRoomArgs) : this( roomName = args.initialName, parentSpaceId = args.parentSpaceId, - isSubSpace = args.isSpace + isSubSpace = args.isSpace, + openAfterCreate = args.openAfterCreate ) /** diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateSubSpaceController.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateSubSpaceController.kt index c8d907b0b2..e67b272c32 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateSubSpaceController.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateSubSpaceController.kt @@ -20,7 +20,6 @@ import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.epoxy.profiles.buildProfileAction import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericPillItem @@ -29,6 +28,7 @@ import im.vector.app.features.form.formEditTextItem import im.vector.app.features.form.formEditableSquareAvatarItem import im.vector.app.features.form.formMultiLineEditTextItem import im.vector.app.features.form.formSubmitButtonItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.MatrixConstants import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure import org.matrix.android.sdk.api.session.room.model.RoomJoinRules diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt index 2707b87c1f..48610dda7b 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt @@ -20,7 +20,6 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.appcompat.app.AppCompatActivity import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState @@ -30,6 +29,7 @@ import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentRoomDirectoryPickerBinding +import im.vector.app.features.analytics.plan.Screen import im.vector.app.features.roomdirectory.RoomDirectoryAction import im.vector.app.features.roomdirectory.RoomDirectoryData import im.vector.app.features.roomdirectory.RoomDirectoryServer @@ -52,15 +52,17 @@ class RoomDirectoryPickerFragment @Inject constructor(private val roomDirectoryP return FragmentRoomDirectoryPickerBinding.inflate(inflater, container, false) } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + analyticsScreenName = Screen.ScreenName.MobileSwitchDirectory + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - vectorBaseActivity.setSupportActionBar(views.toolbar) - - vectorBaseActivity.supportActionBar?.let { - it.setDisplayShowHomeEnabled(true) - it.setDisplayHomeAsUpEnabled(true) - } + setupToolbar(views.toolbar) + .setTitle(R.string.select_room_directory) + .allowBack() sharedActionViewModel = activityViewModelProvider.get(RoomDirectorySharedActionViewModel::class.java) setupRecyclerView() @@ -109,11 +111,6 @@ class RoomDirectoryPickerFragment @Inject constructor(private val roomDirectoryP pickerViewModel.handle(RoomDirectoryPickerAction.RemoveServer(roomDirectoryServer)) } - override fun onResume() { - super.onResume() - (activity as? AppCompatActivity)?.supportActionBar?.setTitle(R.string.select_room_directory) - } - override fun retry() { Timber.v("Retry") pickerViewModel.handle(RoomDirectoryPickerAction.Retry) diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt index 394d738b26..b69788b1ed 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt @@ -19,10 +19,8 @@ package im.vector.app.features.roomdirectory.roompreview import android.content.Context import android.content.Intent import android.os.Parcelable -import com.google.android.material.appbar.MaterialToolbar import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.addFragment -import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleBinding import im.vector.app.features.roomdirectory.RoomDirectoryData @@ -40,6 +38,7 @@ data class RoomPreviewData( val roomAlias: String? = null, val roomType: String? = null, val topic: String? = null, + val numJoinedMembers: Int? = null, val worldReadable: Boolean = false, val avatarUrl: String? = null, val homeServers: List = emptyList(), @@ -52,7 +51,7 @@ data class RoomPreviewData( } @AndroidEntryPoint -class RoomPreviewActivity : VectorBaseActivity(), ToolbarConfigurable { +class RoomPreviewActivity : VectorBaseActivity() { companion object { private const val ARG = "ARG" @@ -69,6 +68,7 @@ class RoomPreviewActivity : VectorBaseActivity(), Toolbar roomName = publicRoom.name, roomAlias = publicRoom.getPrimaryAlias(), topic = publicRoom.topic, + numJoinedMembers = publicRoom.numJoinedMembers, worldReadable = publicRoom.worldReadable, avatarUrl = publicRoom.avatarUrl, homeServers = listOfNotNull(roomDirectoryData.homeServer) @@ -81,10 +81,6 @@ class RoomPreviewActivity : VectorBaseActivity(), Toolbar override fun getCoordinatorLayout() = views.coordinatorLayout - override fun configure(toolbar: MaterialToolbar) { - configureToolbar(toolbar) - } - override fun initUiAndData() { if (isFirstCreation()) { val args = intent.getParcelableExtra(ARG) diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt index 52617e2f1d..6d0195fae3 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt @@ -64,6 +64,7 @@ class RoomPreviewNoPreviewFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupToolbar(views.roomPreviewNoPreviewToolbar) + .allowBack() views.roomPreviewNoPreviewJoin.commonClicked = { roomPreviewViewModel.handle(RoomPreviewAction.Join) } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt index bdb0a96e9e..b1fa0e974a 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt @@ -27,6 +27,9 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.analytics.AnalyticsTracker +import im.vector.app.features.analytics.extensions.toAnalyticsRoomSize +import im.vector.app.features.analytics.plan.JoinedRoom import im.vector.app.features.roomdirectory.JoinState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn @@ -44,9 +47,11 @@ import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.flow.flow import timber.log.Timber -class RoomPreviewViewModel @AssistedInject constructor(@Assisted private val initialState: RoomPreviewViewState, - private val session: Session) : - VectorViewModel(initialState) { +class RoomPreviewViewModel @AssistedInject constructor( + @Assisted private val initialState: RoomPreviewViewState, + private val analyticsTracker: AnalyticsTracker, + private val session: Session +) : VectorViewModel(initialState) { @AssistedFactory interface Factory : MavericksAssistedViewModelFactory { @@ -243,6 +248,11 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted private val ini viewModelScope.launch { try { session.joinRoom(state.roomId, viaServers = state.homeServers) + analyticsTracker.capture(JoinedRoom( + // Always false in this case (?) + isDM = false, + roomSize = state.numJoinMembers.toAnalyticsRoomSize() + )) // We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data. // Instead, we wait for the room to be joined } catch (failure: Throwable) { diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt index 8488dd7267..b2cb43115d 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt @@ -33,6 +33,7 @@ data class RoomPreviewViewState( val roomName: String? = null, val roomTopic: String? = null, + val numJoinMembers: Int? = null, val avatarUrl: String? = null, val shouldPeekFromServer: Boolean = false, @@ -56,6 +57,7 @@ data class RoomPreviewViewState( homeServers = args.homeServers, roomName = args.roomName, roomTopic = args.topic, + numJoinMembers = args.numJoinedMembers, avatarUrl = args.avatarUrl, shouldPeekFromServer = args.peekFromServer, fromEmailInvite = args.fromEmailInvite, @@ -64,6 +66,6 @@ data class RoomPreviewViewState( fun matrixItem(): MatrixItem { return if (roomType == RoomType.SPACE) MatrixItem.SpaceItem(roomId, roomName ?: roomAlias, avatarUrl) - else MatrixItem.RoomItem(roomId, roomName ?: roomAlias, avatarUrl) + else MatrixItem.RoomItem(roomId, roomName ?: roomAlias, avatarUrl) } } diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileActivity.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileActivity.kt index 2d925270b3..1b55207743 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileActivity.kt @@ -22,19 +22,15 @@ import android.content.Intent import android.widget.Toast import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel -import com.google.android.material.appbar.MaterialToolbar import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.addFragment -import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleBinding import im.vector.app.features.room.RequireActiveMembershipViewEvents import im.vector.app.features.room.RequireActiveMembershipViewModel @AndroidEntryPoint -class RoomMemberProfileActivity : - VectorBaseActivity(), - ToolbarConfigurable { +class RoomMemberProfileActivity : VectorBaseActivity() { companion object { fun newIntent(context: Context, args: RoomMemberProfileArgs): Intent { @@ -63,10 +59,6 @@ class RoomMemberProfileActivity : } } - override fun configure(toolbar: MaterialToolbar) { - configureToolbar(toolbar) - } - private fun handleRoomLeft(roomLeft: RequireActiveMembershipViewEvents.RoomLeft) { if (roomLeft.leftMessage != null) { Toast.makeText(this, roomLeft.leftMessage, Toast.LENGTH_LONG).show() diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt index ca022edcab..545e9f7190 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt @@ -19,11 +19,11 @@ package im.vector.app.features.roommemberprofile import com.airbnb.epoxy.TypedEpoxyController import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.epoxy.profiles.buildProfileAction import im.vector.app.core.epoxy.profiles.buildProfileSection import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper @@ -282,7 +282,7 @@ class RoomMemberProfileController @Inject constructor( editable = false, divider = canBan, destructive = true, - title = stringProvider.getString(R.string.room_participants_action_kick), + title = stringProvider.getString(R.string.room_participants_action_remove), action = { callback?.onKickClicked(state.isSpace) } ) } diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt index 0243b44a8c..c68bfca973 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt @@ -47,6 +47,7 @@ import im.vector.app.databinding.DialogBaseEditTextBinding import im.vector.app.databinding.DialogShareQrCodeBinding import im.vector.app.databinding.FragmentMatrixProfileBinding import im.vector.app.databinding.ViewStubRoomMemberProfileHeaderBinding +import im.vector.app.features.analytics.plan.Screen import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer @@ -88,9 +89,15 @@ class RoomMemberProfileFragment @Inject constructor( override fun getMenuRes() = R.menu.vector_room_member_profile + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + analyticsScreenName = Screen.ScreenName.User + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupToolbar(views.matrixProfileToolbar) + .allowBack() val headerView = views.matrixProfileHeaderView.let { it.layoutResource = R.layout.view_stub_room_member_profile_header it.inflate() @@ -356,11 +363,11 @@ class RoomMemberProfileFragment @Inject constructor( .show( activity = requireActivity(), askForReason = true, - confirmationRes = if (isSpace) R.string.space_participants_kick_prompt_msg - else R.string.room_participants_kick_prompt_msg, - positiveRes = R.string.room_participants_action_kick, - reasonHintRes = R.string.room_participants_kick_reason, - titleRes = R.string.room_participants_kick_title + confirmationRes = if (isSpace) R.string.space_participants_remove_prompt_msg + else R.string.room_participants_remove_prompt_msg, + positiveRes = R.string.room_participants_action_remove, + reasonHintRes = R.string.room_participants_remove_reason, + titleRes = R.string.room_participants_remove_title ) { reason -> viewModel.handle(RoomMemberProfileAction.KickUser(reason)) } diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt index 3765d96119..c219c85185 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt @@ -211,7 +211,7 @@ class RoomMemberProfileViewModel @AssistedInject constructor( viewModelScope.launch { _viewEvents.post(RoomMemberProfileViewEvents.Loading()) try { - room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, null, newPowerLevelsContent) + room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, stateKey = "", newPowerLevelsContent) _viewEvents.post(RoomMemberProfileViewEvents.OnSetPowerLevelSuccess) } catch (failure: Throwable) { _viewEvents.post(RoomMemberProfileViewEvents.Failure(failure)) @@ -255,7 +255,7 @@ class RoomMemberProfileViewModel @AssistedInject constructor( viewModelScope.launch { try { _viewEvents.post(RoomMemberProfileViewEvents.Loading()) - room.kick(initialState.userId, action.reason) + room.remove(initialState.userId, action.reason) _viewEvents.post(RoomMemberProfileViewEvents.OnKickActionSuccess) } catch (failure: Throwable) { _viewEvents.post(RoomMemberProfileViewEvents.Failure(failure)) diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListEpoxyController.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListEpoxyController.kt index 8b49c694c4..3bfb210f8d 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListEpoxyController.kt @@ -22,7 +22,6 @@ import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.epoxy.errorWithRetryItem import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.resources.ColorProvider @@ -33,6 +32,7 @@ import im.vector.app.core.ui.list.genericItem import im.vector.app.core.ui.list.genericWithValueItem import im.vector.app.core.utils.DimensionConverter import im.vector.app.features.settings.VectorPreferences +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.span import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt index 8ee6967afa..c7f6e64f5c 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt @@ -18,7 +18,6 @@ package im.vector.app.features.roommemberprofile.devices import com.airbnb.epoxy.TypedEpoxyController import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.ItemStyle @@ -28,6 +27,7 @@ import im.vector.app.core.ui.list.genericWithValueItem import im.vector.app.core.utils.DimensionConverter import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationActionItem import im.vector.app.features.settings.VectorPreferences +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.span import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt index 7b92007692..4c6d2ed2e3 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt @@ -23,12 +23,10 @@ import android.widget.Toast import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel -import com.google.android.material.appbar.MaterialToolbar import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragmentToBackstack import im.vector.app.core.extensions.exhaustive -import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleBinding import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore @@ -47,8 +45,7 @@ import javax.inject.Inject @AndroidEntryPoint class RoomProfileActivity : - VectorBaseActivity(), - ToolbarConfigurable { + VectorBaseActivity() { companion object { @@ -157,8 +154,4 @@ class RoomProfileActivity : private fun openRoomNotificationSettings() { addFragmentToBackstack(views.simpleFragmentContainer, RoomNotificationSettingsFragment::class.java, roomProfileArgs) } - - override fun configure(toolbar: MaterialToolbar) { - configureToolbar(toolbar) - } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt index a1eeaf9286..e4630dae3f 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt @@ -19,7 +19,6 @@ package im.vector.app.features.roomprofile import com.airbnb.epoxy.TypedEpoxyController import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.epoxy.expandableTextItem import im.vector.app.core.epoxy.profiles.buildProfileAction import im.vector.app.core.epoxy.profiles.buildProfileSection @@ -32,6 +31,7 @@ import im.vector.app.features.home.ShortcutCreator import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod import im.vector.app.features.settings.VectorPreferences +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.image import me.gujun.android.span.span import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt index d82b853fe3..8acf53088d 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt @@ -44,6 +44,7 @@ import im.vector.app.core.utils.copyToClipboard import im.vector.app.core.utils.startSharePlainTextIntent import im.vector.app.databinding.FragmentMatrixProfileBinding import im.vector.app.databinding.ViewStubRoomProfileHeaderBinding +import im.vector.app.features.analytics.plan.Screen import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.RoomDetailPendingAction import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore @@ -88,6 +89,7 @@ class RoomProfileFragment @Inject constructor( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + analyticsScreenName = Screen.ScreenName.RoomSettings setFragmentResultListener(MigrateRoomBottomSheet.REQUEST_KEY) { _, bundle -> bundle.getString(MigrateRoomBottomSheet.BUNDLE_KEY_REPLACEMENT_ROOM)?.let { replacementRoomId -> roomDetailPendingActionStore.data = RoomDetailPendingAction.OpenRoom(replacementRoomId, closeCurrentRoom = true) @@ -107,6 +109,7 @@ class RoomProfileFragment @Inject constructor( headerViews = ViewStubRoomProfileHeaderBinding.bind(headerView) setupWaitingView() setupToolbar(views.matrixProfileToolbar) + .allowBack() setupRecyclerView() appBarStateChangeListener = MatrixItemAppBarStateChangeListener( headerView, diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasFragment.kt index 3128a590ce..e48ce54e6c 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasFragment.kt @@ -68,6 +68,7 @@ class RoomAliasFragment @Inject constructor( controller.callback = this setupToolbar(views.roomSettingsToolbar) + .allowBack() views.roomSettingsRecyclerView.configureWith(controller, hasFixedSize = true) views.waitingView.waitingStatusText.setText(R.string.please_wait) views.waitingView.waitingStatusText.isVisible = true diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListController.kt index 1e0572297b..cc332c0ba2 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListController.kt @@ -18,7 +18,6 @@ package im.vector.app.features.roomprofile.banned import com.airbnb.epoxy.TypedEpoxyController import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.epoxy.dividerItem import im.vector.app.core.epoxy.profiles.buildProfileSection import im.vector.app.core.epoxy.profiles.profileMatrixItemWithProgress @@ -27,6 +26,7 @@ import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.roomprofile.members.RoomMemberSummaryFilter +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListFragment.kt index c9fc889242..5cd4da009a 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListFragment.kt @@ -59,6 +59,7 @@ class RoomBannedMemberListFragment @Inject constructor( super.onViewCreated(view, savedInstanceState) roomMemberListController.callback = this setupToolbar(views.roomSettingsToolbar) + .allowBack() setupSearchView() views.roomSettingsRecyclerView.configureWith(roomMemberListController, hasFixedSize = true) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt index ba7e020ebe..d7a9ecf39b 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt @@ -58,6 +58,7 @@ class RoomMemberListFragment @Inject constructor( super.onViewCreated(view, savedInstanceState) roomMemberListController.callback = this setupToolbar(views.roomSettingGeneric.roomSettingsToolbar) + .allowBack() setupSearchView() setupInviteUsersButton() views.roomSettingGeneric.roomSettingsRecyclerView.configureWith(roomMemberListController, hasFixedSize = true) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/notifications/RoomNotificationSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/notifications/RoomNotificationSettingsFragment.kt index ce0fde32c6..320fdfd833 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/notifications/RoomNotificationSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/notifications/RoomNotificationSettingsFragment.kt @@ -50,6 +50,7 @@ class RoomNotificationSettingsFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupToolbar(views.roomSettingsToolbar) + .allowBack() roomNotificationSettingsController.callback = this views.roomSettingsRecyclerView.configureWith(roomNotificationSettingsController, hasFixedSize = true) setupWaitingView() diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/EditablePermission.kt b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/EditablePermission.kt index b083209f16..22d74ff7a3 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/EditablePermission.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/EditablePermission.kt @@ -99,7 +99,7 @@ sealed class EditablePermission(@StringRes val labelResId: Int, @StringRes val s class ChangeSettings : EditablePermission(R.string.room_permissions_change_settings) // Updates `content.kick` - class KickUsers : EditablePermission(R.string.room_permissions_kick_users) + class KickUsers : EditablePermission(R.string.room_permissions_remove_users) // Updates `content.ban` class BanUsers : EditablePermission(R.string.room_permissions_ban_users) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsFragment.kt index acf01321c9..0d5ac7dea8 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsFragment.kt @@ -58,6 +58,7 @@ class RoomPermissionsFragment @Inject constructor( controller.callback = this setupToolbar(views.roomSettingsToolbar) + .allowBack() views.roomSettingsRecyclerView.configureWith(controller, hasFixedSize = true) views.waitingView.waitingStatusText.setText(R.string.please_wait) views.waitingView.waitingStatusText.isVisible = true diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsViewModel.kt index 011c4ea8ae..7e8a66d12a 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsViewModel.kt @@ -124,7 +124,7 @@ class RoomPermissionsViewModel @AssistedInject constructor(@Assisted initialStat } ) } - room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, null, newPowerLevelsContent.toContent()) + room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, stateKey = "", newPowerLevelsContent.toContent()) setState { copy( isLoading = false diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt index 3fa0bbaa5b..51f6b247d4 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt @@ -85,6 +85,7 @@ class RoomSettingsFragment @Inject constructor( setupRoomJoinRuleSharedActionViewModel() controller.callback = this setupToolbar(views.roomSettingsToolbar) + .allowBack() views.roomSettingsRecyclerView.configureWith(controller, hasFixedSize = true) views.waitingView.waitingStatusText.setText(R.string.please_wait) views.waitingView.waitingStatusText.isVisible = true diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleAdvancedController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleAdvancedController.kt index 5e963b4cbc..caf4b1843a 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleAdvancedController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleAdvancedController.kt @@ -18,7 +18,6 @@ package im.vector.app.features.roomprofile.settings.joinrule import com.airbnb.epoxy.TypedEpoxyController import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.ItemStyle @@ -26,6 +25,7 @@ import im.vector.app.core.ui.list.genericButtonItem import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedState +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import timber.log.Timber import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/advanced/ChooseRestrictedController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/advanced/ChooseRestrictedController.kt index b301b8c947..bbec3ca4a5 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/advanced/ChooseRestrictedController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/advanced/ChooseRestrictedController.kt @@ -22,13 +22,13 @@ import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.epoxy.noResultItem import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.spaces.manage.roomSelectionItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.util.MatrixItem import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt index 3716d9682c..3c1a763072 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt @@ -34,6 +34,7 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.saveMedia import im.vector.app.core.utils.shareMedia import im.vector.app.databinding.FragmentRoomUploadsBinding +import im.vector.app.features.analytics.plan.Screen import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.roomprofile.RoomProfileArgs @@ -54,6 +55,11 @@ class RoomUploadsFragment @Inject constructor( return FragmentRoomUploadsBinding.inflate(inflater, container, false) } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + analyticsScreenName = Screen.ScreenName.RoomUploads + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -68,6 +74,7 @@ class RoomUploadsFragment @Inject constructor( }.attach() setupToolbar(views.roomUploadsToolbar) + .allowBack() viewModel.observeViewEvents { when (it) { diff --git a/vector/src/main/java/im/vector/app/features/session/SessionListener.kt b/vector/src/main/java/im/vector/app/features/session/SessionListener.kt index c1ee0b527e..37db55959f 100644 --- a/vector/src/main/java/im/vector/app/features/session/SessionListener.kt +++ b/vector/src/main/java/im/vector/app/features/session/SessionListener.kt @@ -20,16 +20,21 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import im.vector.app.core.extensions.postLiveEvent import im.vector.app.core.utils.LiveEvent +import im.vector.app.features.analytics.AnalyticsTracker +import im.vector.app.features.analytics.extensions.toListOfPerformanceTimer import im.vector.app.features.call.vectorCallService import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.launch import org.matrix.android.sdk.api.failure.GlobalError import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.statistics.StatisticEvent import javax.inject.Inject import javax.inject.Singleton @Singleton -class SessionListener @Inject constructor() : Session.Listener { +class SessionListener @Inject constructor( + private val analyticsTracker: AnalyticsTracker +) : Session.Listener { private val _globalErrorLiveData = MutableLiveData>() val globalErrorLiveData: LiveData> @@ -45,6 +50,12 @@ class SessionListener @Inject constructor() : Session.Listener { } } + override fun onStatisticsEvent(session: Session, statisticEvent: StatisticEvent) { + statisticEvent.toListOfPerformanceTimer().forEach { + analyticsTracker.capture(it) + } + } + override fun onSessionStopped(session: Session) { session.coroutineScope.coroutineContext.cancelChildren() } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index f46ab86c7c..eb620f8e5c 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -187,6 +187,9 @@ class VectorPreferences @Inject constructor(private val context: Context) { private const val DID_ASK_TO_ENABLE_SESSION_PUSH = "DID_ASK_TO_ENABLE_SESSION_PUSH" private const val DID_PROMOTE_NEW_RESTRICTED_JOIN_RULE = "DID_PROMOTE_NEW_RESTRICTED_JOIN_RULE" + // Location Sharing + const val SETTINGS_PREF_ENABLE_LOCATION_SHARING = "SETTINGS_PREF_ENABLE_LOCATION_SHARING" + private const val MEDIA_SAVING_3_DAYS = 0 private const val MEDIA_SAVING_1_WEEK = 1 private const val MEDIA_SAVING_1_MONTH = 2 @@ -196,7 +199,7 @@ class VectorPreferences @Inject constructor(private val context: Context) { private const val TAKE_PHOTO_VIDEO_MODE = "TAKE_PHOTO_VIDEO_MODE" - private const val SETTINGS_LABS_ENABLE_POLLS = "SETTINGS_LABS_ENABLE_POLLS" + private const val SETTINGS_LABS_RENDER_LOCATIONS_IN_TIMELINE = "SETTINGS_LABS_RENDER_LOCATIONS_IN_TIMELINE" // Possible values for TAKE_PHOTO_VIDEO_MODE const val TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK = 0 @@ -992,7 +995,11 @@ class VectorPreferences @Inject constructor(private val context: Context) { } } - fun labsEnablePolls(): Boolean { - return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_POLLS, false) + fun isLocationSharingEnabled(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_PREF_ENABLE_LOCATION_SHARING, false) && BuildConfig.enableLocationSharing + } + + fun labsRenderLocationsInTimeline(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_LABS_RENDER_LOCATIONS_IN_TIMELINE, true) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsActivity.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsActivity.kt index f502da24ff..725c4ceabc 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsActivity.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsActivity.kt @@ -63,7 +63,8 @@ class VectorSettingsActivity : VectorBaseActivity @Inject lateinit var session: Session override fun initUiAndData() { - configureToolbar(views.settingsToolbar) + setupToolbar(views.settingsToolbar) + .allowBack() if (isFirstCreation()) { // display the fragment diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt index 08d67067ec..7cefd20269 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt @@ -29,7 +29,9 @@ import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.toast -import im.vector.app.features.analytics.VectorAnalytics +import im.vector.app.features.analytics.AnalyticsTracker +import im.vector.app.features.analytics.plan.Screen +import im.vector.app.features.analytics.screen.ScreenEvent import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.session.Session @@ -37,6 +39,18 @@ import reactivecircus.flowbinding.android.view.clicks import timber.log.Timber abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), MavericksView { + /* ========================================================================================== + * Analytics + * ========================================================================================== */ + + protected var analyticsScreenName: Screen.ScreenName? = null + private var screenEvent: ScreenEvent? = null + + protected lateinit var analyticsTracker: AnalyticsTracker + + /* ========================================================================================== + * Activity + * ========================================================================================== */ val vectorActivity: VectorBaseActivity<*> by lazy { activity as VectorBaseActivity<*> @@ -47,7 +61,6 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), Maverick // members protected lateinit var session: Session protected lateinit var errorFormatter: ErrorFormatter - protected lateinit var analytics: VectorAnalytics /* ========================================================================================== * Views @@ -72,17 +85,23 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), Maverick super.onAttach(context) session = singletonEntryPoint.activeSessionHolder().getActiveSession() errorFormatter = singletonEntryPoint.errorFormatter() - analytics = singletonEntryPoint.analytics() + analyticsTracker = singletonEntryPoint.analyticsTracker() } override fun onResume() { super.onResume() Timber.i("onResume Fragment ${javaClass.simpleName}") + screenEvent = analyticsScreenName?.let { ScreenEvent(it) } vectorActivity.supportActionBar?.setTitle(titleRes) // find the view from parent activity mLoadingView = vectorActivity.findViewById(R.id.vector_settings_spinner_views) } + override fun onPause() { + super.onPause() + screenEvent?.send(analyticsTracker) + } + abstract fun bindPref() abstract var titleRes: Int diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt index 2e2fab06a3..50e32ae453 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt @@ -22,6 +22,7 @@ import android.widget.CheckedTextView import androidx.core.view.children import androidx.preference.Preference import com.google.android.material.dialog.MaterialAlertDialogBuilder +import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.dialogs.PhotoOrVideoDialog import im.vector.app.core.extensions.restart @@ -149,6 +150,8 @@ class VectorSettingsPreferencesFragment @Inject constructor( }) true } + + findPreference(VectorPreferences.SETTINGS_PREF_ENABLE_LOCATION_SHARING)?.isVisible = BuildConfig.enableLocationSharing } private fun updateTakePhotoOrVideoPreferenceSummary() { diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsRootFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsRootFragment.kt index 79eb0216ee..fb5d83239b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsRootFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsRootFragment.kt @@ -16,8 +16,10 @@ package im.vector.app.features.settings +import android.os.Bundle import im.vector.app.R import im.vector.app.core.preference.VectorPreference +import im.vector.app.features.analytics.plan.Screen import javax.inject.Inject class VectorSettingsRootFragment @Inject constructor() : VectorSettingsBaseFragment() { @@ -25,6 +27,11 @@ class VectorSettingsRootFragment @Inject constructor() : VectorSettingsBaseFragm override var titleRes: Int = R.string.title_activity_settings override val preferenceXmlRes = R.xml.vector_settings_root + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + analyticsScreenName = Screen.ScreenName.Settings + } + override fun bindPref() { tintIcons() } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt index 279499b7e9..31fce00f3c 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt @@ -51,6 +51,7 @@ import im.vector.app.core.utils.copyToClipboard import im.vector.app.core.utils.openFileSelection import im.vector.app.core.utils.toast import im.vector.app.databinding.DialogImportE2eKeysBinding +import im.vector.app.features.analytics.plan.Screen import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewActions import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewModel import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewState @@ -91,6 +92,11 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( private val analyticsConsentViewModel: AnalyticsConsentViewModel by fragmentViewModel() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + analyticsScreenName = Screen.ScreenName.SettingsSecurity + } + // cryptography private val mCryptographyCategory by lazy { findPreference(VectorPreferences.SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY)!! diff --git a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt index 5729e773b7..867526c009 100644 --- a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt @@ -31,6 +31,7 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentDeactivateAccountBinding import im.vector.app.features.MainActivity import im.vector.app.features.MainActivityArgs +import im.vector.app.features.analytics.plan.Screen import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.settings.VectorSettingsActivity import org.matrix.android.sdk.api.auth.data.LoginFlowTypes @@ -47,7 +48,7 @@ class DeactivateAccountFragment @Inject constructor() : VectorBaseFragment if (activityResult.resultCode == Activity.RESULT_OK) { when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) { - LoginFlowTypes.SSO -> { + LoginFlowTypes.SSO -> { viewModel.handle(DeactivateAccountAction.SsoAuthDone) } LoginFlowTypes.PASSWORD -> { @@ -63,6 +64,11 @@ class DeactivateAccountFragment @Inject constructor() : VectorBaseFragment("SETTINGS_DEFAULT")!! diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsKeywordAndMentionsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsKeywordAndMentionsNotificationPreferenceFragment.kt index fb1a357c30..b7cf7f6bbe 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsKeywordAndMentionsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsKeywordAndMentionsNotificationPreferenceFragment.kt @@ -25,6 +25,7 @@ import im.vector.app.core.preference.KeywordPreference import im.vector.app.core.preference.VectorCheckboxPreference import im.vector.app.core.preference.VectorPreference import im.vector.app.core.preference.VectorPreferenceCategory +import im.vector.app.features.analytics.plan.Screen import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -34,7 +35,7 @@ import org.matrix.android.sdk.api.pushrules.rest.PushRule import org.matrix.android.sdk.api.pushrules.toJson class VectorSettingsKeywordAndMentionsNotificationPreferenceFragment : - VectorSettingsPushRuleNotificationPreferenceFragment() { + VectorSettingsPushRuleNotificationPreferenceFragment() { override var titleRes: Int = R.string.settings_notification_mentions_and_keywords @@ -42,6 +43,11 @@ class VectorSettingsKeywordAndMentionsNotificationPreferenceFragment : private var keywordsHasFocus = false + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + analyticsScreenName = Screen.ScreenName.SettingsMentionsAndKeywords + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) session.getKeywords().observe(viewLifecycleOwner, this::updateWithKeywords) @@ -71,7 +77,7 @@ class VectorSettingsKeywordAndMentionsNotificationPreferenceFragment : val keywords = editKeywordPreference.keywords val newChecked = newValue as Boolean displayLoadingView() - updateKeywordPushRules(keywords, newChecked) { result -> + updateKeywordPushRules(keywords, newChecked) { result -> hideLoadingView() if (!isAdded) { return@updateKeywordPushRules @@ -88,7 +94,7 @@ class VectorSettingsKeywordAndMentionsNotificationPreferenceFragment : false } - editKeywordPreference.listener = object : KeywordPreference.Listener { + editKeywordPreference.listener = object : KeywordPreference.Listener { override fun onFocusDidChange(hasFocus: Boolean) { keywordsHasFocus = true } @@ -174,8 +180,8 @@ class VectorSettingsKeywordAndMentionsNotificationPreferenceFragment : } override val prefKeyToPushRuleId = mapOf( - "SETTINGS_PUSH_RULE_CONTAINING_MY_DISPLAY_NAME_PREFERENCE_KEY" to RuleIds.RULE_ID_CONTAIN_DISPLAY_NAME, - "SETTINGS_PUSH_RULE_CONTAINING_MY_USER_NAME_PREFERENCE_KEY" to RuleIds.RULE_ID_CONTAIN_USER_NAME, - "SETTINGS_PUSH_RULE_MESSAGES_CONTAINING_AT_ROOM_PREFERENCE_KEY" to RuleIds.RULE_ID_ROOM_NOTIF - ) + "SETTINGS_PUSH_RULE_CONTAINING_MY_DISPLAY_NAME_PREFERENCE_KEY" to RuleIds.RULE_ID_CONTAIN_DISPLAY_NAME, + "SETTINGS_PUSH_RULE_CONTAINING_MY_USER_NAME_PREFERENCE_KEY" to RuleIds.RULE_ID_CONTAIN_USER_NAME, + "SETTINGS_PUSH_RULE_MESSAGES_CONTAINING_AT_ROOM_PREFERENCE_KEY" to RuleIds.RULE_ID_ROOM_NOTIF + ) } diff --git a/vector/src/main/java/im/vector/app/features/settings/push/PushGateWayController.kt b/vector/src/main/java/im/vector/app/features/settings/push/PushGateWayController.kt index 3daeace09f..68c3e960c7 100644 --- a/vector/src/main/java/im/vector/app/features/settings/push/PushGateWayController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/push/PushGateWayController.kt @@ -18,9 +18,9 @@ package im.vector.app.features.settings.push import com.airbnb.epoxy.TypedEpoxyController import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import javax.inject.Inject class PushGateWayController @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/settings/push/PushRulesController.kt b/vector/src/main/java/im/vector/app/features/settings/push/PushRulesController.kt index ed6a31d070..68f288ffd3 100644 --- a/vector/src/main/java/im/vector/app/features/settings/push/PushRulesController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/push/PushRulesController.kt @@ -18,9 +18,9 @@ package im.vector.app.features.settings.push import com.airbnb.epoxy.TypedEpoxyController import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import javax.inject.Inject class PushRulesController @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsController.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsController.kt index f3c0469d79..d374357396 100644 --- a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsController.kt @@ -22,7 +22,6 @@ import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.epoxy.noResultItem import im.vector.app.core.error.ErrorFormatter @@ -38,6 +37,7 @@ import im.vector.app.features.discovery.settingsEditTextItem import im.vector.app.features.discovery.settingsInfoItem import im.vector.app.features.discovery.settingsInformationItem import im.vector.app.features.discovery.settingsSectionTitleItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.session.identity.ThreePid diff --git a/vector/src/main/java/im/vector/app/features/share/IncomingShareActivity.kt b/vector/src/main/java/im/vector/app/features/share/IncomingShareActivity.kt index 294f1d4d91..439d9b64fa 100644 --- a/vector/src/main/java/im/vector/app/features/share/IncomingShareActivity.kt +++ b/vector/src/main/java/im/vector/app/features/share/IncomingShareActivity.kt @@ -16,15 +16,13 @@ package im.vector.app.features.share -import com.google.android.material.appbar.MaterialToolbar import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.addFragment -import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleBinding @AndroidEntryPoint -class IncomingShareActivity : VectorBaseActivity(), ToolbarConfigurable { +class IncomingShareActivity : VectorBaseActivity() { override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater) @@ -35,8 +33,4 @@ class IncomingShareActivity : VectorBaseActivity(), Toolb addFragment(views.simpleFragmentContainer, IncomingShareFragment::class.java) } } - - override fun configure(toolbar: MaterialToolbar) { - configureToolbar(toolbar, displayBack = false) - } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceExploreActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceExploreActivity.kt index 3361305c83..f4610805bc 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceExploreActivity.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceExploreActivity.kt @@ -16,6 +16,7 @@ package im.vector.app.features.spaces +import android.app.Activity import android.content.Context import android.content.Intent import android.os.Bundle @@ -25,13 +26,16 @@ import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R +import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleBinding import im.vector.app.features.matrixto.MatrixToBottomSheet import im.vector.app.features.navigation.Navigator +import im.vector.app.features.roomdirectory.createroom.CreateRoomActivity import im.vector.app.features.spaces.explore.SpaceDirectoryArgs import im.vector.app.features.spaces.explore.SpaceDirectoryFragment +import im.vector.app.features.spaces.explore.SpaceDirectoryViewAction import im.vector.app.features.spaces.explore.SpaceDirectoryViewEvents import im.vector.app.features.spaces.explore.SpaceDirectoryViewModel @@ -44,6 +48,15 @@ class SpaceExploreActivity : VectorBaseActivity(), Matrix val sharedViewModel: SpaceDirectoryViewModel by viewModel() + private val createRoomResultLauncher = registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK) { + CreateRoomActivity.getCreatedRoomId(activityResult.data)?.let { + // we want to refresh from API + sharedViewModel.handle(SpaceDirectoryViewAction.RefreshUntilFound(it)) + } + } + } + private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() { override fun onFragmentResumed(fm: FragmentManager, f: Fragment) { if (f is MatrixToBottomSheet) { @@ -84,6 +97,13 @@ class SpaceExploreActivity : VectorBaseActivity(), Matrix is SpaceDirectoryViewEvents.NavigateToMxToBottomSheet -> { MatrixToBottomSheet.withLink(it.link).show(supportFragmentManager, "ShowChild") } + is SpaceDirectoryViewEvents.NavigateToCreateNewRoom -> { + createRoomResultLauncher.launch(CreateRoomActivity.getIntent( + this, + openAfterCreate = false, + currentSpaceId = it.currentSpaceId + )) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewModel.kt index a762e13cba..02771abc95 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewModel.kt @@ -88,7 +88,7 @@ class SpaceListViewModel @AssistedInject constructor(@Assisted initialState: Spa observeSpaceSummaries() // observeSelectionState() - appStateHandler.selectedRoomGroupingObservable + appStateHandler.selectedRoomGroupingFlow .distinctUntilChanged() .setOnEach { copy( diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt index e8c2c4bde6..95e4405da5 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt @@ -19,7 +19,6 @@ package im.vector.app.features.spaces import com.airbnb.epoxy.EpoxyController import im.vector.app.R import im.vector.app.RoomGroupingMethod -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem @@ -30,6 +29,7 @@ import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.list.UnreadCounterBadgeView import im.vector.app.group import im.vector.app.space +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.group.model.GroupSummary import org.matrix.android.sdk.api.session.room.model.Membership diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceAdd3pidEpoxyController.kt b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceAdd3pidEpoxyController.kt index 816931a7c1..1193ecb496 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceAdd3pidEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceAdd3pidEpoxyController.kt @@ -20,7 +20,6 @@ import android.text.InputType import com.airbnb.epoxy.TypedEpoxyController import com.google.android.material.textfield.TextInputLayout import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.ItemStyle @@ -28,6 +27,7 @@ import im.vector.app.core.ui.list.genericButtonItem import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericPillItem import im.vector.app.features.form.formEditTextItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import javax.inject.Inject class SpaceAdd3pidEpoxyController @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt index 3353e66b3c..4ef469500b 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt @@ -19,12 +19,12 @@ package im.vector.app.features.spaces.create import com.airbnb.epoxy.TypedEpoxyController import com.google.android.material.textfield.TextInputLayout import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.ItemStyle import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.features.form.formEditTextItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import javax.inject.Inject class SpaceDefaultRoomEpoxyController @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt index a22256c3e1..b25ae4f2c7 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt @@ -20,7 +20,6 @@ import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.mvrx.Fail import im.vector.app.R import im.vector.app.core.epoxy.TextListener -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.features.form.formEditTextItem @@ -28,6 +27,7 @@ import im.vector.app.features.form.formEditableSquareAvatarItem import im.vector.app.features.form.formMultiLineEditTextItem import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.roomdirectory.createroom.RoomAliasErrorFormatter +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.MatrixConstants import org.matrix.android.sdk.api.session.room.alias.RoomAliasError import org.matrix.android.sdk.api.util.MatrixItem diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt index 6dadce30e0..a2c1380098 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt @@ -24,7 +24,6 @@ import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Uninitialized import im.vector.app.R import im.vector.app.core.epoxy.ClickListener -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.epoxy.errorWithRetryItem import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.error.ErrorFormatter @@ -35,6 +34,7 @@ import im.vector.app.core.ui.list.genericEmptyWithActionItem import im.vector.app.core.ui.list.genericPillItem import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.list.spaceChildInfoItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.span import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError.Companion.M_UNRECOGNIZED diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt index f630323790..bbf6ac79ca 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt @@ -26,6 +26,7 @@ import android.view.ViewGroup import androidx.core.text.toSpannable import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState @@ -83,13 +84,16 @@ class SpaceDirectoryFragment @Inject constructor( bundle.getString(SpaceAddRoomSpaceChooserBottomSheet.BUNDLE_KEY_ACTION)?.let { action -> val spaceId = withState(viewModel) { it.spaceId } when (action) { - SpaceAddRoomSpaceChooserBottomSheet.ACTION_ADD_ROOMS -> { + SpaceAddRoomSpaceChooserBottomSheet.ACTION_ADD_ROOMS -> { addExistingRoomActivityResult.launch(SpaceManageActivity.newIntent(requireContext(), spaceId, ManageType.AddRooms)) } - SpaceAddRoomSpaceChooserBottomSheet.ACTION_ADD_SPACES -> { + SpaceAddRoomSpaceChooserBottomSheet.ACTION_ADD_SPACES -> { addExistingRoomActivityResult.launch(SpaceManageActivity.newIntent(requireContext(), spaceId, ManageType.AddRoomsOnlySpaces)) } - else -> { + SpaceAddRoomSpaceChooserBottomSheet.ACTION_CREATE_ROOM -> { + viewModel.handle(SpaceDirectoryViewAction.CreateNewRoom) + } + else -> { // nop } } @@ -100,12 +104,9 @@ class SpaceDirectoryFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - vectorBaseActivity.setSupportActionBar(views.toolbar) + setupToolbar(views.toolbar) + .allowBack() - vectorBaseActivity.supportActionBar?.let { - it.setDisplayShowHomeEnabled(true) - it.setDisplayHomeAsUpEnabled(true) - } epoxyController.listener = this views.spaceDirectoryList.configureWith(epoxyController) epoxyVisibilityTracker.attach(views.spaceDirectoryList) @@ -114,8 +115,32 @@ class SpaceDirectoryFragment @Inject constructor( invalidateOptionsMenu() } + views.addOrCreateChatRoomButton.debouncedClicks { + withState(viewModel) { + addExistingRooms(it.spaceId) + } + } + views.spaceCard.matrixToCardMainButton.isVisible = false views.spaceCard.matrixToCardSecondaryButton.isVisible = false + + // Hide FAB when list is scrolling + views.spaceDirectoryList.addOnScrollListener( + object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + views.addOrCreateChatRoomButton.removeCallbacks(showFabRunnable) + + when (newState) { + RecyclerView.SCROLL_STATE_IDLE -> { + views.addOrCreateChatRoomButton.postDelayed(showFabRunnable, 250) + } + RecyclerView.SCROLL_STATE_DRAGGING, + RecyclerView.SCROLL_STATE_SETTLING -> { + views.addOrCreateChatRoomButton.hide() + } + } + } + }) } override fun onDestroyView() { @@ -125,6 +150,12 @@ class SpaceDirectoryFragment @Inject constructor( super.onDestroyView() } + private val showFabRunnable = Runnable { + if (isAdded) { + views.addOrCreateChatRoomButton.show() + } + } + override fun invalidate() = withState(viewModel) { state -> epoxyController.setData(state) @@ -132,16 +163,15 @@ class SpaceDirectoryFragment @Inject constructor( if (currentParentId == null) { // it's the root - val title = getString(R.string.space_explore_activity_title) - views.toolbar.title = title + toolbar?.setTitle(R.string.space_explore_activity_title) } else { - val title = state.currentRootSummary?.name + toolbar?.title = state.currentRootSummary?.name ?: state.currentRootSummary?.canonicalAlias ?: getString(R.string.space_explore_activity_title) - views.toolbar.title = title } spaceCardRenderer.render(state.currentRootSummary, emptyList(), this, views.spaceCard) + views.addOrCreateChatRoomButton.isVisible = state.canAddRooms } override fun onPrepareOptionsMenu(menu: Menu) = withState(viewModel) { state -> diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewAction.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewAction.kt index 3ced017d61..2166a7e306 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewAction.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewAction.kt @@ -24,7 +24,9 @@ sealed class SpaceDirectoryViewAction : VectorViewModelAction { data class JoinOrOpen(val spaceChildInfo: SpaceChildInfo) : SpaceDirectoryViewAction() data class ShowDetails(val spaceChildInfo: SpaceChildInfo) : SpaceDirectoryViewAction() data class NavigateToRoom(val roomId: String) : SpaceDirectoryViewAction() + object CreateNewRoom : SpaceDirectoryViewAction() object HandleBack : SpaceDirectoryViewAction() object Retry : SpaceDirectoryViewAction() + data class RefreshUntilFound(val roomIdToFind: String) : SpaceDirectoryViewAction() object LoadAdditionalItemsIfNeeded : SpaceDirectoryViewAction() } diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewEvents.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewEvents.kt index 3ac0426de9..6359eff68d 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewEvents.kt @@ -22,4 +22,5 @@ sealed class SpaceDirectoryViewEvents : VectorViewEvents { object Dismiss : SpaceDirectoryViewEvents() data class NavigateToRoom(val roomId: String) : SpaceDirectoryViewEvents() data class NavigateToMxToBottomSheet(val link: String) : SpaceDirectoryViewEvents() + data class NavigateToCreateNewRoom(val currentSpaceId: String) : SpaceDirectoryViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt index d7bdf4f511..abc70ccbc1 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt @@ -55,7 +55,9 @@ class SpaceDirectoryViewModel @AssistedInject constructor( override fun create(initialState: SpaceDirectoryState): SpaceDirectoryViewModel } - companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() { + private const val PAGE_LENGTH = 10 + } init { @@ -71,6 +73,27 @@ class SpaceDirectoryViewModel @AssistedInject constructor( observeJoinedRooms() observeMembershipChanges() observePermissions() + observeKnownSummaries() + } + + private fun observeKnownSummaries() { + // A we prefer to use known summaries to have better name resolution + // it's important to have them up to date. Particularly after creation where + // resolved name is sometimes just "New Room" + session.flow().liveRoomSummaries( + roomSummaryQueryParams { + memberships = listOf(Membership.JOIN) + includeType = null + } + ).execute { + val updatedRoomSummaries = it + copy( + knownRoomSummaries = this.knownRoomSummaries.map { rs -> + updatedRoomSummaries.invoke()?.firstOrNull { it.roomId == rs.roomId } + ?: rs + } + ) + } } private fun observePermissions() { @@ -103,7 +126,7 @@ class SpaceDirectoryViewModel @AssistedInject constructor( try { val query = session.spaceService().querySpaceChildren( spaceId, - limit = 10 + limit = PAGE_LENGTH ) val knownSummaries = query.children.mapNotNull { session.getRoomSummary(it.childRoomId) @@ -181,9 +204,17 @@ class SpaceDirectoryViewModel @AssistedInject constructor( SpaceDirectoryViewAction.Retry -> { handleRetry() } + is SpaceDirectoryViewAction.RefreshUntilFound -> { + handleRefreshUntilFound(action.roomIdToFind) + } SpaceDirectoryViewAction.LoadAdditionalItemsIfNeeded -> { loadAdditionalItemsIfNeeded() } + is SpaceDirectoryViewAction.CreateNewRoom -> { + withState { state -> + _viewEvents.post(SpaceDirectoryViewEvents.NavigateToCreateNewRoom(state.currentRootSummary?.roomId ?: initialState.spaceId)) + } + } } } @@ -207,6 +238,66 @@ class SpaceDirectoryViewModel @AssistedInject constructor( refreshFromApi(state.hierarchyStack.lastOrNull() ?: initialState.spaceId) } + private fun handleRefreshUntilFound(roomIdToFind: String?) = withState { state -> + val currentRootId = state.hierarchyStack.lastOrNull() ?: initialState.spaceId + + val mutablePaginationStatus = state.paginationStatus.toMutableMap().apply { + this[currentRootId] = Loading() + } + + // mark as paginating + setState { + copy( + paginationStatus = mutablePaginationStatus + ) + } + + viewModelScope.launch(Dispatchers.IO) { + var query = session.spaceService().querySpaceChildren( + currentRootId, + limit = PAGE_LENGTH + ) + + var knownSummaries = query.children.mapNotNull { + session.getRoomSummary(it.childRoomId) + ?.takeIf { it.membership == Membership.JOIN } // only take if joined because it will be up to date (synced) + }.distinctBy { it.roomId } + + while (!query.children.any { it.childRoomId == roomIdToFind } && query.nextToken != null) { + // continue to paginate until found + val paginate = session.spaceService().querySpaceChildren( + currentRootId, + limit = PAGE_LENGTH, + from = query.nextToken, + knownStateList = query.childrenState + ) + + knownSummaries = ( + knownSummaries + + (paginate.children.mapNotNull { + session.getRoomSummary(it.childRoomId) + ?.takeIf { it.membership == Membership.JOIN } // only take if joined because it will be up to date (synced) + }) + ).distinctBy { it.roomId } + + query = query.copy( + children = query.children + paginate.children, + nextToken = paginate.nextToken + ) + } + + setState { + copy( + apiResults = this.apiResults.toMutableMap().apply { + this[currentRootId] = Success(query) + }, + paginationStatus = this.paginationStatus.toMutableMap().apply { this[currentRootId] = Success(Unit) }.toMap(), + knownRoomSummaries = (state.knownRoomSummaries + knownSummaries).distinctBy { it.roomId }, + ) + } + } + } + private fun handleExploreSubSpace(action: SpaceDirectoryViewAction.ExploreSubSpace) = withState { state -> val newRootId = action.spaceChildInfo.childRoomId val curSum = RoomSummary( @@ -252,7 +343,9 @@ class SpaceDirectoryViewModel @AssistedInject constructor( if (mutablePaginationStatus[currentRootId] is Loading) return@withState setState { - copy(paginationStatus = mutablePaginationStatus.toMap()) + copy(paginationStatus = mutablePaginationStatus.apply { + this[currentRootId] = Loading() + }) } viewModelScope.launch(Dispatchers.IO) { @@ -268,7 +361,7 @@ class SpaceDirectoryViewModel @AssistedInject constructor( } val query = session.spaceService().querySpaceChildren( currentRootId, - limit = 10, + limit = PAGE_LENGTH, from = currentResponse.nextToken, knownStateList = currentResponse.childrenState ) diff --git a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedActivity.kt index 6076388289..a3ce8cea31 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedActivity.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedActivity.kt @@ -26,7 +26,6 @@ import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.Success import com.airbnb.mvrx.viewModel -import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R @@ -34,15 +33,13 @@ import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.extensions.setTextOrHide -import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleLoadingBinding import im.vector.app.features.spaces.SpaceBottomSheetSettingsArgs import javax.inject.Inject @AndroidEntryPoint -class SpaceLeaveAdvancedActivity : VectorBaseActivity(), - ToolbarConfigurable { +class SpaceLeaveAdvancedActivity : VectorBaseActivity() { override fun getBinding(): ActivitySimpleLoadingBinding = ActivitySimpleLoadingBinding.inflate(layoutInflater) @@ -113,8 +110,4 @@ class SpaceLeaveAdvancedActivity : VectorBaseActivity(), - ToolbarConfigurable { +class SpaceManageActivity : VectorBaseActivity() { private lateinit var sharedDirectoryActionViewModel: RoomDirectorySharedActionViewModel @@ -188,8 +185,4 @@ class SpaceManageActivity : VectorBaseActivity(), } } } - - override fun configure(toolbar: MaterialToolbar) { - configureToolbar(toolbar) - } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsController.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsController.kt index 67c9f83498..54f19ce297 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsController.kt @@ -21,13 +21,13 @@ import com.airbnb.epoxy.VisibilityState import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Incomplete import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.epoxy.errorWithRetryItem import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.features.home.AvatarRenderer +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsFragment.kt index 125686d200..bbfa97511c 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsFragment.kt @@ -66,8 +66,11 @@ class SpaceManageRoomsFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + setupToolbar(views.addRoomToSpaceToolbar) - views.appBarTitle.text = getString(R.string.space_manage_rooms_and_spaces) + .setTitle(R.string.space_manage_rooms_and_spaces) + .allowBack() + views.createNewRoom.isVisible = false epoxyController.listener = this views.roomList.configureWith(epoxyController, hasFixedSize = true, dividerDrawable = R.drawable.divider_horizontal) @@ -111,14 +114,15 @@ class SpaceManageRoomsFragment @Inject constructor( epoxyController.setData(state) state.spaceSummary.invoke()?.let { - views.appBarSpaceInfo.text = it.displayName + toolbar?.subtitle = it.displayName } + if (state.selectedRooms.isNotEmpty()) { if (currentActionMode == null) { views.addRoomToSpaceToolbar.isVisible = true vectorBaseActivity.startSupportActionMode(this) } else { - currentActionMode?.title = "${state.selectedRooms.size} selected" + toolbar?.title = resources.getQuantityString(R.plurals.room_details_selected, state.selectedRooms.size, state.selectedRooms.size) } // views.addRoomToSpaceToolbar.isVisible = false // views.addRoomToSpaceToolbar.startActionMode(this) @@ -167,10 +171,10 @@ class SpaceManageRoomsFragment @Inject constructor( override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { when (item?.itemId) { - R.id.action_delete -> { + R.id.action_delete -> { handleDeleteSelection() } - R.id.action_mark_as_suggested -> { + R.id.action_mark_as_suggested -> { viewModel.handle(SpaceManageRoomViewAction.MarkAllAsSuggested(true)) } R.id.action_mark_as_not_suggested -> { diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt index a3d0252c19..266d08fd12 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt @@ -85,6 +85,7 @@ class SpaceSettingsFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupToolbar(views.roomSettingsToolbar) + .allowBack() // roomProfileSharedActionViewModel = activityViewModelProvider.get(RoomProfileSharedActionViewModel::class.java) // setupRoomHistoryVisibilitySharedActionViewModel() setupRoomJoinRuleSharedActionViewModel() diff --git a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleFragment.kt index c5cfed6974..5b2d6bed7b 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleFragment.kt @@ -64,9 +64,9 @@ class SpacePeopleFragment @Inject constructor( } override fun invalidate() = withState(membersViewModel) { memberListState -> - views.appBarTitle.text = getString(R.string.bottom_action_people) val memberCount = (memberListState.roomSummary.invoke()?.otherMemberIds?.size ?: 0) + 1 - views.appBarSpaceInfo.text = resources.getQuantityString(R.plurals.room_title_members, memberCount, memberCount) + + toolbar?.subtitle = resources.getQuantityString(R.plurals.room_title_members, memberCount, memberCount) // views.listBuildingProgress.isVisible = true epoxyController.setData(memberListState) } @@ -78,17 +78,12 @@ class SpacePeopleFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + + setupToolbar(views.addRoomToSpaceToolbar) + .allowBack() setupRecyclerView() setupSearchView() - views.addRoomToSpaceToolbar.navigationIcon = drawableProvider.getDrawable( - R.drawable.ic_close_24dp, - colorProvider.getColorFromAttribute(R.attr.vctr_content_primary) - ) - views.addRoomToSpaceToolbar.setNavigationOnClickListener { - sharedActionViewModel.post(SpacePeopleSharedAction.Dismiss) - } - viewModel.observeViewEvents { handleViewEvents(it) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleListController.kt b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleListController.kt index 62bd866cb1..1fbe9bbbf9 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleListController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleListController.kt @@ -18,7 +18,6 @@ package im.vector.app.features.spaces.people import com.airbnb.epoxy.TypedEpoxyController import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.epoxy.dividerItem import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.epoxy.profiles.profileMatrixItemWithPowerLevel @@ -31,6 +30,7 @@ import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.roomprofile.members.RoomMemberListCategories import im.vector.app.features.roomprofile.members.RoomMemberListViewState import im.vector.app.features.roomprofile.members.RoomMemberSummaryFilter +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.span import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.util.toMatrixItem diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt index 4d0d301721..e97dab1d86 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt @@ -32,12 +32,12 @@ import com.airbnb.mvrx.withState import im.vector.app.R import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith -import im.vector.app.core.flow.throttleFirst import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentSpacePreviewBinding import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.spaces.SpacePreviewSharedAction import im.vector.app.features.spaces.SpacePreviewSharedActionViewModel +import im.vector.lib.core.utils.flow.throttleFirst import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.parcelize.Parcelize diff --git a/vector/src/main/java/im/vector/app/features/usercode/ScanUserCodeFragment.kt b/vector/src/main/java/im/vector/app/features/usercode/ScanUserCodeFragment.kt index 389256871e..a7d632bd7b 100644 --- a/vector/src/main/java/im/vector/app/features/usercode/ScanUserCodeFragment.kt +++ b/vector/src/main/java/im/vector/app/features/usercode/ScanUserCodeFragment.kt @@ -55,6 +55,10 @@ class ScanUserCodeFragment @Inject constructor() : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + + setupToolbar(views.qrScannerToolbar) + .allowBack(useCross = true) + views.userCodeMyCodeButton.debouncedClicks { sharedViewModel.handle(UserCodeActions.SwitchMode(UserCodeState.Mode.SHOW)) } @@ -62,10 +66,6 @@ class ScanUserCodeFragment @Inject constructor() : views.userCodeOpenGalleryButton.debouncedClicks { MultiPicker.get(MultiPicker.IMAGE).single().startWith(pickImageActivityResultLauncher) } - - views.userCodeClose.debouncedClicks { - requireActivity().onBackPressed() - } } private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted, _ -> diff --git a/vector/src/main/java/im/vector/app/features/usercode/ShowUserCodeFragment.kt b/vector/src/main/java/im/vector/app/features/usercode/ShowUserCodeFragment.kt index b794b23d0e..a31b0d3a25 100644 --- a/vector/src/main/java/im/vector/app/features/usercode/ShowUserCodeFragment.kt +++ b/vector/src/main/java/im/vector/app/features/usercode/ShowUserCodeFragment.kt @@ -52,9 +52,9 @@ class ShowUserCodeFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - views.showUserCodeClose.debouncedClicks { - sharedViewModel.handle(UserCodeActions.DismissAction) - } + setupToolbar(views.showUserCodeToolBar) + .allowBack(useCross = true) + views.showUserCodeScanButton.debouncedClicks { if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), openCameraActivityResultLauncher)) { doOpenQRCodeScanner() diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt index cb0ec7c36b..55858b2baf 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt @@ -22,7 +22,6 @@ import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.epoxy.errorWithRetryItem import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.epoxy.noResultItem @@ -32,6 +31,7 @@ import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericPillItem import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.span import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.identity.IdentityServiceError diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt index 721bce4af9..6ac6270fc7 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt @@ -72,9 +72,9 @@ class UserListFragment @Inject constructor( super.onViewCreated(view, savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(UserListSharedActionViewModel::class.java) if (args.showToolbar) { - views.userListTitle.text = args.title - vectorBaseActivity.setSupportActionBar(views.userListToolbar) - setupCloseView() + setupToolbar(views.userListToolbar) + .setTitle(args.title) + .allowBack(useCross = true) views.userListToolbar.isVisible = true } else { views.userListToolbar.isVisible = false @@ -153,12 +153,6 @@ class UserListFragment @Inject constructor( views.userListSearch.requestFocus() } - private fun setupCloseView() { - views.userListClose.debouncedClicks { - requireActivity().finish() - } - } - override fun invalidate() = withState(viewModel) { userListController.setData(it) } diff --git a/vector/src/main/java/im/vector/app/features/webview/VectorWebViewActivity.kt b/vector/src/main/java/im/vector/app/features/webview/VectorWebViewActivity.kt index ab7913a99c..fd26fff017 100644 --- a/vector/src/main/java/im/vector/app/features/webview/VectorWebViewActivity.kt +++ b/vector/src/main/java/im/vector/app/features/webview/VectorWebViewActivity.kt @@ -44,7 +44,8 @@ class VectorWebViewActivity : VectorBaseActivity() } override fun initUiAndData() { - configureToolbar(views.webviewToolbar) + setupToolbar(views.webviewToolbar) + .allowBack() waitingView = views.simpleWebviewLoader views.simpleWebview.settings.apply { diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt index 1e6d130c67..963bd9521c 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt @@ -22,11 +22,9 @@ import android.content.Intent import androidx.core.view.isVisible import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel -import com.google.android.material.appbar.MaterialToolbar import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.extensions.addFragment -import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityWidgetBinding import im.vector.app.features.widgets.permissions.RoomWidgetPermissionBottomSheet @@ -36,8 +34,7 @@ import org.matrix.android.sdk.api.session.events.model.Content import java.io.Serializable @AndroidEntryPoint -class WidgetActivity : VectorBaseActivity(), - ToolbarConfigurable { +class WidgetActivity : VectorBaseActivity() { companion object { private const val WIDGET_FRAGMENT_TAG = "WIDGET_FRAGMENT_TAG" @@ -77,7 +74,8 @@ class WidgetActivity : VectorBaseActivity(), finish() return } - configure(views.toolbar) + setupToolbar(views.toolbar) + .allowBack() views.toolbar.isVisible = widgetArgs.kind.nameRes != 0 viewModel.observeViewEvents { when (it) { @@ -129,8 +127,4 @@ class WidgetActivity : VectorBaseActivity(), } finish() } - - override fun configure(toolbar: MaterialToolbar) { - configureToolbar(toolbar) - } } diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt index 99b3595d11..cdffbd5411 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt @@ -319,7 +319,7 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo launchWidgetAPIAction(widgetPostAPIMediator, eventData) { room.sendStateEvent( eventType = EventType.PLUMBING, - stateKey = null, + stateKey = "", body = params ) } diff --git a/vector/src/main/res/drawable-hdpi/ic_splash_collaboration.webp b/vector/src/main/res/drawable-hdpi/ic_splash_collaboration.webp new file mode 100644 index 0000000000..7042e030d0 Binary files /dev/null and b/vector/src/main/res/drawable-hdpi/ic_splash_collaboration.webp differ diff --git a/vector/src/main/res/drawable-hdpi/ic_splash_collaboration_dark.webp b/vector/src/main/res/drawable-hdpi/ic_splash_collaboration_dark.webp new file mode 100644 index 0000000000..6e4297183a Binary files /dev/null and b/vector/src/main/res/drawable-hdpi/ic_splash_collaboration_dark.webp differ diff --git a/vector/src/main/res/drawable-hdpi/ic_splash_control.webp b/vector/src/main/res/drawable-hdpi/ic_splash_control.webp new file mode 100644 index 0000000000..82c04e402b Binary files /dev/null and b/vector/src/main/res/drawable-hdpi/ic_splash_control.webp differ diff --git a/vector/src/main/res/drawable-hdpi/ic_splash_control_dark.webp b/vector/src/main/res/drawable-hdpi/ic_splash_control_dark.webp new file mode 100644 index 0000000000..0d0c6ad78b Binary files /dev/null and b/vector/src/main/res/drawable-hdpi/ic_splash_control_dark.webp differ diff --git a/vector/src/main/res/drawable-hdpi/ic_splash_conversations.webp b/vector/src/main/res/drawable-hdpi/ic_splash_conversations.webp new file mode 100644 index 0000000000..ee9604c1f1 Binary files /dev/null and b/vector/src/main/res/drawable-hdpi/ic_splash_conversations.webp differ diff --git a/vector/src/main/res/drawable-hdpi/ic_splash_conversations_dark.webp b/vector/src/main/res/drawable-hdpi/ic_splash_conversations_dark.webp new file mode 100644 index 0000000000..c5cdf4e6fe Binary files /dev/null and b/vector/src/main/res/drawable-hdpi/ic_splash_conversations_dark.webp differ diff --git a/vector/src/main/res/drawable-hdpi/ic_splash_secure.webp b/vector/src/main/res/drawable-hdpi/ic_splash_secure.webp new file mode 100644 index 0000000000..a880031ada Binary files /dev/null and b/vector/src/main/res/drawable-hdpi/ic_splash_secure.webp differ diff --git a/vector/src/main/res/drawable-hdpi/ic_splash_secure_dark.webp b/vector/src/main/res/drawable-hdpi/ic_splash_secure_dark.webp new file mode 100644 index 0000000000..65ef9f35ff Binary files /dev/null and b/vector/src/main/res/drawable-hdpi/ic_splash_secure_dark.webp differ diff --git a/vector/src/main/res/drawable-xhdpi/ic_splash_collaboration.webp b/vector/src/main/res/drawable-xhdpi/ic_splash_collaboration.webp new file mode 100644 index 0000000000..d32d9f6026 Binary files /dev/null and b/vector/src/main/res/drawable-xhdpi/ic_splash_collaboration.webp differ diff --git a/vector/src/main/res/drawable-xhdpi/ic_splash_collaboration_dark.webp b/vector/src/main/res/drawable-xhdpi/ic_splash_collaboration_dark.webp new file mode 100644 index 0000000000..04af9e2db4 Binary files /dev/null and b/vector/src/main/res/drawable-xhdpi/ic_splash_collaboration_dark.webp differ diff --git a/vector/src/main/res/drawable-xhdpi/ic_splash_control.webp b/vector/src/main/res/drawable-xhdpi/ic_splash_control.webp new file mode 100644 index 0000000000..972d91d5d0 Binary files /dev/null and b/vector/src/main/res/drawable-xhdpi/ic_splash_control.webp differ diff --git a/vector/src/main/res/drawable-xhdpi/ic_splash_control_dark.webp b/vector/src/main/res/drawable-xhdpi/ic_splash_control_dark.webp new file mode 100644 index 0000000000..cbbea1ae87 Binary files /dev/null and b/vector/src/main/res/drawable-xhdpi/ic_splash_control_dark.webp differ diff --git a/vector/src/main/res/drawable-xhdpi/ic_splash_conversations.webp b/vector/src/main/res/drawable-xhdpi/ic_splash_conversations.webp new file mode 100644 index 0000000000..4057edfc66 Binary files /dev/null and b/vector/src/main/res/drawable-xhdpi/ic_splash_conversations.webp differ diff --git a/vector/src/main/res/drawable-xhdpi/ic_splash_conversations_dark.webp b/vector/src/main/res/drawable-xhdpi/ic_splash_conversations_dark.webp new file mode 100644 index 0000000000..e3b7f22c1a Binary files /dev/null and b/vector/src/main/res/drawable-xhdpi/ic_splash_conversations_dark.webp differ diff --git a/vector/src/main/res/drawable-xhdpi/ic_splash_secure.webp b/vector/src/main/res/drawable-xhdpi/ic_splash_secure.webp new file mode 100644 index 0000000000..b8c772bde2 Binary files /dev/null and b/vector/src/main/res/drawable-xhdpi/ic_splash_secure.webp differ diff --git a/vector/src/main/res/drawable-xhdpi/ic_splash_secure_dark.webp b/vector/src/main/res/drawable-xhdpi/ic_splash_secure_dark.webp new file mode 100644 index 0000000000..d4c1f97652 Binary files /dev/null and b/vector/src/main/res/drawable-xhdpi/ic_splash_secure_dark.webp differ diff --git a/vector/src/main/res/drawable-xhdpi/onboarding_carousel_connect.webp b/vector/src/main/res/drawable-xhdpi/onboarding_carousel_connect.webp deleted file mode 100644 index 50aa2bc0f0..0000000000 Binary files a/vector/src/main/res/drawable-xhdpi/onboarding_carousel_connect.webp and /dev/null differ diff --git a/vector/src/main/res/drawable-xhdpi/onboarding_carousel_conversations.webp b/vector/src/main/res/drawable-xhdpi/onboarding_carousel_conversations.webp deleted file mode 100644 index cb1038db14..0000000000 Binary files a/vector/src/main/res/drawable-xhdpi/onboarding_carousel_conversations.webp and /dev/null differ diff --git a/vector/src/main/res/drawable-xhdpi/onboarding_carousel_ems.webp b/vector/src/main/res/drawable-xhdpi/onboarding_carousel_ems.webp deleted file mode 100644 index 4f1754b300..0000000000 Binary files a/vector/src/main/res/drawable-xhdpi/onboarding_carousel_ems.webp and /dev/null differ diff --git a/vector/src/main/res/drawable-xhdpi/onboarding_carousel_universal.webp b/vector/src/main/res/drawable-xhdpi/onboarding_carousel_universal.webp deleted file mode 100644 index de4c5f18cf..0000000000 Binary files a/vector/src/main/res/drawable-xhdpi/onboarding_carousel_universal.webp and /dev/null differ diff --git a/vector/src/main/res/drawable-xxhdpi/ic_splash_collaboration.webp b/vector/src/main/res/drawable-xxhdpi/ic_splash_collaboration.webp new file mode 100644 index 0000000000..8feed1f9f9 Binary files /dev/null and b/vector/src/main/res/drawable-xxhdpi/ic_splash_collaboration.webp differ diff --git a/vector/src/main/res/drawable-xxhdpi/ic_splash_collaboration_dark.webp b/vector/src/main/res/drawable-xxhdpi/ic_splash_collaboration_dark.webp new file mode 100644 index 0000000000..02e44fbf44 Binary files /dev/null and b/vector/src/main/res/drawable-xxhdpi/ic_splash_collaboration_dark.webp differ diff --git a/vector/src/main/res/drawable-xxhdpi/ic_splash_control.webp b/vector/src/main/res/drawable-xxhdpi/ic_splash_control.webp new file mode 100644 index 0000000000..99d4c4049d Binary files /dev/null and b/vector/src/main/res/drawable-xxhdpi/ic_splash_control.webp differ diff --git a/vector/src/main/res/drawable-xxhdpi/ic_splash_control_dark.webp b/vector/src/main/res/drawable-xxhdpi/ic_splash_control_dark.webp new file mode 100644 index 0000000000..9afa384f27 Binary files /dev/null and b/vector/src/main/res/drawable-xxhdpi/ic_splash_control_dark.webp differ diff --git a/vector/src/main/res/drawable-xxhdpi/ic_splash_conversations.webp b/vector/src/main/res/drawable-xxhdpi/ic_splash_conversations.webp new file mode 100644 index 0000000000..99a4c0c6f5 Binary files /dev/null and b/vector/src/main/res/drawable-xxhdpi/ic_splash_conversations.webp differ diff --git a/vector/src/main/res/drawable-xxhdpi/ic_splash_conversations_dark.webp b/vector/src/main/res/drawable-xxhdpi/ic_splash_conversations_dark.webp new file mode 100644 index 0000000000..361981eec7 Binary files /dev/null and b/vector/src/main/res/drawable-xxhdpi/ic_splash_conversations_dark.webp differ diff --git a/vector/src/main/res/drawable-xxhdpi/ic_splash_secure.webp b/vector/src/main/res/drawable-xxhdpi/ic_splash_secure.webp new file mode 100644 index 0000000000..114421453e Binary files /dev/null and b/vector/src/main/res/drawable-xxhdpi/ic_splash_secure.webp differ diff --git a/vector/src/main/res/drawable-xxhdpi/ic_splash_secure_dark.webp b/vector/src/main/res/drawable-xxhdpi/ic_splash_secure_dark.webp new file mode 100644 index 0000000000..737bcbdf17 Binary files /dev/null and b/vector/src/main/res/drawable-xxhdpi/ic_splash_secure_dark.webp differ diff --git a/vector/src/main/res/drawable-xxxhdpi/ic_splash_collaboration.webp b/vector/src/main/res/drawable-xxxhdpi/ic_splash_collaboration.webp new file mode 100644 index 0000000000..1dc31f6447 Binary files /dev/null and b/vector/src/main/res/drawable-xxxhdpi/ic_splash_collaboration.webp differ diff --git a/vector/src/main/res/drawable-xxxhdpi/ic_splash_collaboration_dark.webp b/vector/src/main/res/drawable-xxxhdpi/ic_splash_collaboration_dark.webp new file mode 100644 index 0000000000..943f2b9ba8 Binary files /dev/null and b/vector/src/main/res/drawable-xxxhdpi/ic_splash_collaboration_dark.webp differ diff --git a/vector/src/main/res/drawable-xxxhdpi/ic_splash_control.webp b/vector/src/main/res/drawable-xxxhdpi/ic_splash_control.webp new file mode 100644 index 0000000000..9375475513 Binary files /dev/null and b/vector/src/main/res/drawable-xxxhdpi/ic_splash_control.webp differ diff --git a/vector/src/main/res/drawable-xxxhdpi/ic_splash_control_dark.webp b/vector/src/main/res/drawable-xxxhdpi/ic_splash_control_dark.webp new file mode 100644 index 0000000000..905851dc26 Binary files /dev/null and b/vector/src/main/res/drawable-xxxhdpi/ic_splash_control_dark.webp differ diff --git a/vector/src/main/res/drawable-xxxhdpi/ic_splash_conversations.webp b/vector/src/main/res/drawable-xxxhdpi/ic_splash_conversations.webp new file mode 100644 index 0000000000..0d669312f5 Binary files /dev/null and b/vector/src/main/res/drawable-xxxhdpi/ic_splash_conversations.webp differ diff --git a/vector/src/main/res/drawable-xxxhdpi/ic_splash_conversations_dark.webp b/vector/src/main/res/drawable-xxxhdpi/ic_splash_conversations_dark.webp new file mode 100644 index 0000000000..c5c4b2ccdd Binary files /dev/null and b/vector/src/main/res/drawable-xxxhdpi/ic_splash_conversations_dark.webp differ diff --git a/vector/src/main/res/drawable-xxxhdpi/ic_splash_secure.webp b/vector/src/main/res/drawable-xxxhdpi/ic_splash_secure.webp new file mode 100644 index 0000000000..6a2a3fda56 Binary files /dev/null and b/vector/src/main/res/drawable-xxxhdpi/ic_splash_secure.webp differ diff --git a/vector/src/main/res/drawable-xxxhdpi/ic_splash_secure_dark.webp b/vector/src/main/res/drawable-xxxhdpi/ic_splash_secure_dark.webp new file mode 100644 index 0000000000..b792cb16ea Binary files /dev/null and b/vector/src/main/res/drawable-xxxhdpi/ic_splash_secure_dark.webp differ diff --git a/vector/src/main/res/drawable/bg_feature_icon.xml b/vector/src/main/res/drawable/bg_feature_icon.xml new file mode 100644 index 0000000000..299f1a4a2e --- /dev/null +++ b/vector/src/main/res/drawable/bg_feature_icon.xml @@ -0,0 +1,9 @@ + + + diff --git a/vector/src/main/res/drawable/bg_map_user_pin.xml b/vector/src/main/res/drawable/bg_map_user_pin.xml new file mode 100644 index 0000000000..148d3cfa29 --- /dev/null +++ b/vector/src/main/res/drawable/bg_map_user_pin.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/drawable/ic_attachment_location_white.xml b/vector/src/main/res/drawable/ic_attachment_location_white.xml new file mode 100644 index 0000000000..865362312b --- /dev/null +++ b/vector/src/main/res/drawable/ic_attachment_location_white.xml @@ -0,0 +1,9 @@ + + + diff --git a/vector/src/main/res/drawable/ic_onboarding_use_case_icon.xml b/vector/src/main/res/drawable/ic_onboarding_use_case_icon.xml new file mode 100644 index 0000000000..35b45aa69a --- /dev/null +++ b/vector/src/main/res/drawable/ic_onboarding_use_case_icon.xml @@ -0,0 +1,14 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_share_external.xml b/vector/src/main/res/drawable/ic_share_external.xml new file mode 100644 index 0000000000..c4b78c8a83 --- /dev/null +++ b/vector/src/main/res/drawable/ic_share_external.xml @@ -0,0 +1,5 @@ + + + diff --git a/vector/src/main/res/drawable/ic_use_case_communities.xml b/vector/src/main/res/drawable/ic_use_case_communities.xml new file mode 100644 index 0000000000..a511da6d2e --- /dev/null +++ b/vector/src/main/res/drawable/ic_use_case_communities.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/drawable/ic_use_case_friends.xml b/vector/src/main/res/drawable/ic_use_case_friends.xml new file mode 100644 index 0000000000..e4dea36774 --- /dev/null +++ b/vector/src/main/res/drawable/ic_use_case_friends.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/drawable/ic_use_case_teams.xml b/vector/src/main/res/drawable/ic_use_case_teams.xml new file mode 100644 index 0000000000..29b3def8cb --- /dev/null +++ b/vector/src/main/res/drawable/ic_use_case_teams.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/layout/activity_call.xml b/vector/src/main/res/layout/activity_call.xml index 2238f96c9f..5734e5f92a 100644 --- a/vector/src/main/res/layout/activity_call.xml +++ b/vector/src/main/res/layout/activity_call.xml @@ -41,10 +41,10 @@ android:layout_width="@dimen/call_pip_width" android:layout_height="@dimen/call_pip_height" android:layout_marginEnd="16dp" - app:layout_goneMarginEnd="0dp" app:cardCornerRadius="@dimen/call_pip_radius" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent"> + app:layout_constraintEnd_toEndOf="parent" + app:layout_goneMarginEnd="0dp"> + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/bottom_sheet_add_rooms_or_spaces_to_space.xml b/vector/src/main/res/layout/bottom_sheet_add_rooms_or_spaces_to_space.xml index 25c2d1c3e5..f17bcd16f4 100644 --- a/vector/src/main/res/layout/bottom_sheet_add_rooms_or_spaces_to_space.xml +++ b/vector/src/main/res/layout/bottom_sheet_add_rooms_or_spaces_to_space.xml @@ -7,24 +7,34 @@ android:background="?android:colorBackground" android:orientation="vertical"> - + + + + + + + + + + + + + + app:actionTitle="@string/create_new_room" + app:leftIcon="@drawable/ic_fab_add" + app:tint="?vctr_content_primary" + app:titleTextColor="?vctr_content_primary" + tools:actionDescription="" /> - - - - - - - - - - + android:layout_height="?actionBarSize" + app:title="@string/contacts_book_title"/> - - - - - - - - - - + android:layout_height="?actionBarSize" + app:title="@string/direct_chats_header" /> - - - - - - - - - - + app:title="@string/create_poll_title" /> diff --git a/vector/src/main/res/layout/fragment_create_room.xml b/vector/src/main/res/layout/fragment_create_room.xml index 5607f2be69..7deb1786e1 100644 --- a/vector/src/main/res/layout/fragment_create_room.xml +++ b/vector/src/main/res/layout/fragment_create_room.xml @@ -19,46 +19,8 @@ - - - - - - - - - - + android:layout_height="?actionBarSize" + app:title="@string/direct_chats_header" /> diff --git a/vector/src/main/res/layout/fragment_ftue_auth_splash.xml b/vector/src/main/res/layout/fragment_ftue_auth_splash.xml index 39c0ad3007..ef3d015bdb 100644 --- a/vector/src/main/res/layout/fragment_ftue_auth_splash.xml +++ b/vector/src/main/res/layout/fragment_ftue_auth_splash.xml @@ -182,15 +182,15 @@ style="@style/Widget.Vector.Button.Login" android:layout_width="0dp" android:layout_height="wrap_content" - android:text="@string/login_splash_submit" android:textAllCaps="true" android:transitionName="loginSubmitTransition" app:layout_constraintBottom_toTopOf="@id/loginSplashSpace5" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/loginSplashSpace4" /> + app:layout_constraintTop_toBottomOf="@id/loginSplashSpace4" + tools:text="@string/login_splash_create_account" /> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + +