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/quality.yml b/.github/workflows/quality.yml index 483e8819f3..7eff52cc9f 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -74,7 +74,7 @@ jobs: edit-mode: replace - name: Delete comment if needed if: always() && steps.fc.outputs.comment-id != '' && steps.ktlint-results.outputs.add_comment == 'false' - uses: actions/github-script@v5.1.0 + uses: actions/github-script@v3 with: script: | github.issues.deleteComment({ 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 e0dd3298d8..cf885d5cd5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,67 @@ +Changes in Element v1.3.15 (2022-01-18) +======================================= + +Bugfixes 🐛 +---------- + - Fix crash when viewing source which contains an emoji ([#4796](https://github.com/vector-im/element-android/issues/4796)) + - Prevent crash in Timeline and add more logs. ([#4959](https://github.com/vector-im/element-android/issues/4959)) + - Fix crash on API <24 and make sure this error will not occur again. ([#4962](https://github.com/vector-im/element-android/issues/4962)) + - Fixes sign in/up crash when selecting ems and other server types which use SSO ([#4969](https://github.com/vector-im/element-android/issues/4969)) + + +Changes in Element v1.3.14 (2022-01-12) +======================================= + +Bugfixes 🐛 +---------- + - Fix sending events in encrypted rooms broken, and incremental sync broken in 1.3.13 ([#4924](https://github.com/vector-im/element-android/issues/4924)) + + +Changes in Element v1.3.13 (2022-01-11) +======================================= + +Features ✨ +---------- + - Updates onboarding splash screen to have a dedicated sign in button and removes the dual purpose sign in/up stage ([#4382](https://github.com/vector-im/element-android/issues/4382)) + - Display Analytics opt-in screen at first start-up of the app ([#4892](https://github.com/vector-im/element-android/issues/4892)) + - New attachment picker UI ([#3444](https://github.com/vector-im/element-android/issues/3444)) + - Add labs support for rendering LaTeX maths (MSC2191) ([#2133](https://github.com/vector-im/element-android/issues/2133)) + - Allow changing nick colors from the member detail screen ([#2614](https://github.com/vector-im/element-android/issues/2614)) + - Analytics: Track Errors ([#4719](https://github.com/vector-im/element-android/issues/4719)) + - Change internal timeline management. ([#4405](https://github.com/vector-im/element-android/issues/4405)) + - Translate the error observed when the user is not allowed to join a room ([#4847](https://github.com/vector-im/element-android/issues/4847)) + +Bugfixes 🐛 +---------- + - Stop using CharSequence as EpoxyAttribute because it can lead to crash if the CharSequence mutates during rendering. ([#4837](https://github.com/vector-im/element-android/issues/4837)) + - Better handling of misconfigured room encryption ([#4711](https://github.com/vector-im/element-android/issues/4711)) + - Fix message replies/quotes to respect newlines. ([#4540](https://github.com/vector-im/element-android/issues/4540)) + - Polls: unable to create a poll with more than 10 answers ([#4735](https://github.com/vector-im/element-android/issues/4735)) + - Fix for broken unread message indicator on the room list when there are no messages in the room. ([#4749](https://github.com/vector-im/element-android/issues/4749)) + - Fixes newer emojis rendering strangely when inserting from the system keyboard ([#4756](https://github.com/vector-im/element-android/issues/4756)) + - Fixing unable to change change avatar in some scenarios ([#4767](https://github.com/vector-im/element-android/issues/4767)) + - Tentative fix for the speaker being used instead of earpiece for the outgoing call ringtone on lineage os ([#4781](https://github.com/vector-im/element-android/issues/4781)) + - Fixing crashes when quickly scrolling or restoring the room timeline ([#4789](https://github.com/vector-im/element-android/issues/4789)) + - Fixing encrypted non message events showing up as notification messages (eg when a participant joins, mutes or leaves a voice call) ([#4804](https://github.com/vector-im/element-android/issues/4804)) + +SDK API changes ⚠️ +------------------ + - Introduce method onStateUpdated on Timeline.Callback ([#4405](https://github.com/vector-im/element-android/issues/4405)) + - Support tagged events in Room Account Data (MSC2437) ([#4753](https://github.com/vector-im/element-android/issues/4753)) + +Other changes +------------- + - Workaround to fetch all the pending toDevice events from a Synapse homeserver ([#4612](https://github.com/vector-im/element-android/issues/4612)) + - Toolbar is added to a views with QR code scan ([#4644](https://github.com/vector-im/element-android/issues/4644)) + - Open share UI provides by the system when sharing media or text. ([#4745](https://github.com/vector-im/element-android/issues/4745)) + - Cleaning rendering of state events in timeline ([#4747](https://github.com/vector-im/element-android/issues/4747)) + - Enabling new FTUE Auth onboarding base, includes the "I already have an account" button in the splash ([#4872](https://github.com/vector-im/element-android/issues/4872)) + - Olm lib is now hosted in MavenCentral - upgrade to 3.2.10 ([#4882](https://github.com/vector-im/element-android/issues/4882)) + - Remove deprecated experimental restricted space lab option ([#4889](https://github.com/vector-im/element-android/issues/4889)) + - Add ktlint results on github as a comment only on fail ([#4888](https://github.com/vector-im/element-android/issues/4888)) + - Fix github actions ktlint reports and publish results on PR as comment ([#4864](https://github.com/vector-im/element-android/issues/4864)) + + Changes in Element v1.3.12 (2021-12-20) ======================================= diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dbc0ce9b72..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. @@ -139,7 +140,7 @@ If a string is not used anymore, it should be removed from the resource, but ple Instead, please comment the original string with: ```xml - + ``` The string will be removed during the next sync with Weblate. 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/2133.feature b/changelog.d/2133.feature deleted file mode 100644 index 5649ca4cc6..0000000000 --- a/changelog.d/2133.feature +++ /dev/null @@ -1 +0,0 @@ -Add labs support for rendering LaTeX maths (MSC2191) diff --git a/changelog.d/2614.feature b/changelog.d/2614.feature deleted file mode 100644 index 7feceeabba..0000000000 --- a/changelog.d/2614.feature +++ /dev/null @@ -1 +0,0 @@ -Allow changing nick colors from the member detail screen \ No newline at end of file diff --git a/changelog.d/3444.feature b/changelog.d/3444.feature deleted file mode 100644 index 8b9fad18b1..0000000000 --- a/changelog.d/3444.feature +++ /dev/null @@ -1 +0,0 @@ -New attachment picker UI \ No newline at end of file diff --git a/changelog.d/3839.bugfix b/changelog.d/3839.bugfix new file mode 100644 index 0000000000..a2a4dc6876 --- /dev/null +++ b/changelog.d/3839.bugfix @@ -0,0 +1 @@ + Notification does not take me to the room when another space was last viewed \ No newline at end of file diff --git a/changelog.d/3932.bugfix b/changelog.d/3932.bugfix new file mode 100644 index 0000000000..76e90f2bfd --- /dev/null +++ b/changelog.d/3932.bugfix @@ -0,0 +1 @@ +Explore Rooms overflow menu - content update include "Create room" \ No newline at end of file diff --git a/changelog.d/4382.feature b/changelog.d/4382.feature deleted file mode 100644 index 64230dccb1..0000000000 --- a/changelog.d/4382.feature +++ /dev/null @@ -1 +0,0 @@ -Updates onboarding splash screen to have a dedicated sign in button and removes the dual purpose sign in/up stage \ No newline at end of file diff --git a/changelog.d/4405.feature b/changelog.d/4405.feature deleted file mode 100644 index 9a840a9d12..0000000000 --- a/changelog.d/4405.feature +++ /dev/null @@ -1 +0,0 @@ -Change internal timeline management. \ No newline at end of file diff --git a/changelog.d/4405.removal b/changelog.d/4405.removal deleted file mode 100644 index 2d1543cb2b..0000000000 --- a/changelog.d/4405.removal +++ /dev/null @@ -1 +0,0 @@ -Introduce method onStateUpdated on Timeline.Callback \ No newline at end of file diff --git a/changelog.d/4540.bugfix b/changelog.d/4540.bugfix deleted file mode 100644 index 9c962f91f7..0000000000 --- a/changelog.d/4540.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix message replies/quotes to respect newlines. \ No newline at end of file diff --git a/changelog.d/4584.feature b/changelog.d/4584.feature new file mode 100644 index 0000000000..6c0de3b34f --- /dev/null +++ b/changelog.d/4584.feature @@ -0,0 +1 @@ +Enables the FTUE splash carousel \ No newline at end of file diff --git a/changelog.d/4612.misc b/changelog.d/4612.misc deleted file mode 100644 index 43b5007b7e..0000000000 --- a/changelog.d/4612.misc +++ /dev/null @@ -1 +0,0 @@ -Workaround to fetch all the pending toDevice events from a Synapse homeserver \ No newline at end of file diff --git a/changelog.d/4644.misc b/changelog.d/4644.misc deleted file mode 100644 index faf3346f66..0000000000 --- a/changelog.d/4644.misc +++ /dev/null @@ -1 +0,0 @@ -Toolbar is added to a views with QR code scan \ No newline at end of file diff --git a/changelog.d/4669.bugfix b/changelog.d/4669.bugfix new file mode 100644 index 0000000000..8e38becdd9 --- /dev/null +++ b/changelog.d/4669.bugfix @@ -0,0 +1 @@ +Fix sync timeout after returning from background diff --git a/changelog.d/4719.feature b/changelog.d/4719.feature deleted file mode 100644 index b926613d03..0000000000 --- a/changelog.d/4719.feature +++ /dev/null @@ -1 +0,0 @@ -Analytics: Track Errors \ No newline at end of file diff --git a/changelog.d/4734.misc b/changelog.d/4734.misc new file mode 100644 index 0000000000..46748df141 --- /dev/null +++ b/changelog.d/4734.misc @@ -0,0 +1 @@ +Analytics: send more Events \ No newline at end of file diff --git a/changelog.d/4735.bugfix b/changelog.d/4735.bugfix deleted file mode 100644 index cf6cca33fd..0000000000 --- a/changelog.d/4735.bugfix +++ /dev/null @@ -1 +0,0 @@ -Polls: unable to create a poll with more than 10 answers \ No newline at end of file diff --git a/changelog.d/4745.misc b/changelog.d/4745.misc deleted file mode 100644 index 458422d112..0000000000 --- a/changelog.d/4745.misc +++ /dev/null @@ -1 +0,0 @@ -Open share UI provides by the system when sharing media or text. \ No newline at end of file diff --git a/changelog.d/4747.misc b/changelog.d/4747.misc deleted file mode 100644 index 37a960671c..0000000000 --- a/changelog.d/4747.misc +++ /dev/null @@ -1 +0,0 @@ -Cleaning rendering of state events in timeline \ No newline at end of file diff --git a/changelog.d/4749.bugfix b/changelog.d/4749.bugfix deleted file mode 100644 index 5ea29f66a0..0000000000 --- a/changelog.d/4749.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix for broken unread message indicator on the room list when there are no messages in the room. \ No newline at end of file diff --git a/changelog.d/4753.removal b/changelog.d/4753.removal deleted file mode 100644 index 1a474f0f5f..0000000000 --- a/changelog.d/4753.removal +++ /dev/null @@ -1 +0,0 @@ -Support tagged events in Room Account Data (MSC2437) diff --git a/changelog.d/4756.bugfix b/changelog.d/4756.bugfix deleted file mode 100644 index 8e0c373557..0000000000 --- a/changelog.d/4756.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixes newer emojis rendering strangely when inserting from the system keyboard \ No newline at end of file diff --git a/changelog.d/4767.bugfix b/changelog.d/4767.bugfix deleted file mode 100644 index 172e9d80ca..0000000000 --- a/changelog.d/4767.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixing unable to change change avatar in some scenarios \ No newline at end of file diff --git a/changelog.d/4781.bugfix b/changelog.d/4781.bugfix deleted file mode 100644 index 7ac6e62448..0000000000 --- a/changelog.d/4781.bugfix +++ /dev/null @@ -1 +0,0 @@ -Tentative fix for the speaker being used instead of earpiece for the outgoing call ringtone on lineage os \ No newline at end of file diff --git a/changelog.d/4789.bugfix b/changelog.d/4789.bugfix deleted file mode 100644 index 4612dd1ecd..0000000000 --- a/changelog.d/4789.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixing crashes when quickly scrolling or restoring the room timeline \ No newline at end of file diff --git a/changelog.d/4804.bugfix b/changelog.d/4804.bugfix deleted file mode 100644 index 8f845662ab..0000000000 --- a/changelog.d/4804.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixing encrypted non message events showing up as notification messages (eg when a participant joins, mutes or leaves a voice call) \ No newline at end of file diff --git a/changelog.d/4811.feature b/changelog.d/4811.feature new file mode 100644 index 0000000000..e113078f21 --- /dev/null +++ b/changelog.d/4811.feature @@ -0,0 +1 @@ +Enabling native support for window resizing \ No newline at end of file diff --git a/changelog.d/4837.bugfix b/changelog.d/4837.bugfix deleted file mode 100644 index d1eae295f5..0000000000 --- a/changelog.d/4837.bugfix +++ /dev/null @@ -1 +0,0 @@ -Stop using CharSequence as EpoxyAttribute because it can lead to crash if the CharSequence mutates during rendering. \ No newline at end of file diff --git a/changelog.d/4842.misc b/changelog.d/4842.misc new file mode 100644 index 0000000000..ebeb54084e --- /dev/null +++ b/changelog.d/4842.misc @@ -0,0 +1 @@ +Fix integration tests and add a comment with results (still not perfect due to github actions resource limitations) diff --git a/changelog.d/4847.bugfix b/changelog.d/4847.bugfix deleted file mode 100644 index d311882934..0000000000 --- a/changelog.d/4847.bugfix +++ /dev/null @@ -1 +0,0 @@ -Translate the error observed when the user is not allowed to join a room \ No newline at end of file diff --git a/changelog.d/4864.misc b/changelog.d/4864.misc deleted file mode 100644 index 05890ae1c6..0000000000 --- a/changelog.d/4864.misc +++ /dev/null @@ -1 +0,0 @@ -Fix github actions ktlint reports and publish results on PR as comment diff --git a/changelog.d/4865.misc b/changelog.d/4865.misc new file mode 100644 index 0000000000..291c4a099f --- /dev/null +++ b/changelog.d/4865.misc @@ -0,0 +1 @@ +"/kick" command is replaced with "/remove". Also replaced all occurrences in string resources diff --git a/changelog.d/4872.misc b/changelog.d/4872.misc deleted file mode 100644 index 6a29e55494..0000000000 --- a/changelog.d/4872.misc +++ /dev/null @@ -1 +0,0 @@ -Enabling new FTUE Auth onboarding base, includes the "I already have an account" button in the splash \ No newline at end of file diff --git a/changelog.d/4880.wip b/changelog.d/4880.wip new file mode 100644 index 0000000000..e74c56cdc4 --- /dev/null +++ b/changelog.d/4880.wip @@ -0,0 +1 @@ +Updates the onboarding carousel images, copy and improves the handling of different device sizes \ No newline at end of file diff --git a/changelog.d/4882.misc b/changelog.d/4882.misc deleted file mode 100644 index 46c5ec0bb7..0000000000 --- a/changelog.d/4882.misc +++ /dev/null @@ -1 +0,0 @@ - Olm lib is now hosted in MavenCentral - upgrade to 3.2.10 \ No newline at end of file diff --git a/changelog.d/4884.misc b/changelog.d/4884.misc new file mode 100644 index 0000000000..3315aeba0c --- /dev/null +++ b/changelog.d/4884.misc @@ -0,0 +1,3 @@ +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. \ No newline at end of file diff --git a/changelog.d/4888.misc b/changelog.d/4888.misc deleted file mode 100644 index 3b948c9857..0000000000 --- a/changelog.d/4888.misc +++ /dev/null @@ -1 +0,0 @@ -Add ktlint results on github as a comment only on fail diff --git a/changelog.d/4889.removal b/changelog.d/4889.removal deleted file mode 100644 index 2b631deb69..0000000000 --- a/changelog.d/4889.removal +++ /dev/null @@ -1 +0,0 @@ -Remove deprecated experimental restricted space lab option \ No newline at end of file diff --git a/changelog.d/4892.feature b/changelog.d/4892.feature deleted file mode 100644 index 1d66367999..0000000000 --- a/changelog.d/4892.feature +++ /dev/null @@ -1 +0,0 @@ -Display Analytics opt-in screen at first start-up of the app \ No newline at end of file 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/4914.wip b/changelog.d/4914.wip new file mode 100644 index 0000000000..9d471d40ac --- /dev/null +++ b/changelog.d/4914.wip @@ -0,0 +1 @@ +Disabling onboarding automatic carousel transitions on user interaction \ No newline at end of file diff --git a/changelog.d/4918.wip b/changelog.d/4918.wip new file mode 100644 index 0000000000..ed5a273e16 --- /dev/null +++ b/changelog.d/4918.wip @@ -0,0 +1 @@ +Locking phones to portrait during the FTUE onboarding \ No newline at end of file diff --git a/changelog.d/4926.misc b/changelog.d/4926.misc new file mode 100644 index 0000000000..c84ff0d9eb --- /dev/null +++ b/changelog.d/4926.misc @@ -0,0 +1 @@ +Add signing config for the release buildType. No secret added \ No newline at end of file diff --git a/changelog.d/4927.wip b/changelog.d/4927.wip new file mode 100644 index 0000000000..86e9abad26 --- /dev/null +++ b/changelog.d/4927.wip @@ -0,0 +1 @@ +Adds a messaging use case screen to the FTUE onboarding \ No newline at end of file diff --git a/changelog.d/4935.bugfix b/changelog.d/4935.bugfix new file mode 100644 index 0000000000..18967ed4a5 --- /dev/null +++ b/changelog.d/4935.bugfix @@ -0,0 +1 @@ +Fix a wrong network error issue in the Legals screen \ No newline at end of file diff --git a/changelog.d/4942.misc b/changelog.d/4942.misc new file mode 100644 index 0000000000..29fb8cee55 --- /dev/null +++ b/changelog.d/4942.misc @@ -0,0 +1 @@ +Remove unused module matrix-sdk-android-rx and do some cleanup \ No newline at end of file diff --git a/changelog.d/4948.bugfix b/changelog.d/4948.bugfix new file mode 100644 index 0000000000..da4b0a2500 --- /dev/null +++ b/changelog.d/4948.bugfix @@ -0,0 +1 @@ +Prevent Alerts to be displayed in the automatically displayed analytics opt-in screen \ No newline at end of file diff --git a/changelog.d/4949.misc b/changelog.d/4949.misc new file mode 100644 index 0000000000..8f23c7a05e --- /dev/null +++ b/changelog.d/4949.misc @@ -0,0 +1 @@ +Sync issue automation with element-web diff --git a/changelog.d/4960.misc b/changelog.d/4960.misc new file mode 100644 index 0000000000..0829d74b61 --- /dev/null +++ b/changelog.d/4960.misc @@ -0,0 +1 @@ +Improves local echo blinking when non room events received diff --git a/changelog.d/4975.misc b/changelog.d/4975.misc new file mode 100644 index 0000000000..12d1fc163f --- /dev/null +++ b/changelog.d/4975.misc @@ -0,0 +1 @@ +Including onboarding server options in the all screen sanity test suite \ No newline at end of file diff --git a/changelog.d/4988.misc b/changelog.d/4988.misc new file mode 100644 index 0000000000..9fbeb781d5 --- /dev/null +++ b/changelog.d/4988.misc @@ -0,0 +1 @@ +Exclude dependabot upgrade for @github-script@v3 diff --git a/changelog.d/4991.bugfix b/changelog.d/4991.bugfix new file mode 100644 index 0000000000..cd534374c9 --- /dev/null +++ b/changelog.d/4991.bugfix @@ -0,0 +1 @@ +EmojiPopupDismissListener not being triggered after dismissing the EmojiPopup \ 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/changelog.d/4997.bugfix b/changelog.d/4997.bugfix new file mode 100644 index 0000000000..0bdfbede25 --- /dev/null +++ b/changelog.d/4997.bugfix @@ -0,0 +1 @@ +Fix an error in string resource \ No newline at end of file diff --git a/changelog.d/4998.misc b/changelog.d/4998.misc new file mode 100644 index 0000000000..1283b33b1c --- /dev/null +++ b/changelog.d/4998.misc @@ -0,0 +1 @@ + Small iteration on command parser and unit test it. \ No newline at end of file diff --git a/changelog.d/5003.bugfix b/changelog.d/5003.bugfix new file mode 100644 index 0000000000..299cf15cd8 --- /dev/null +++ b/changelog.d/5003.bugfix @@ -0,0 +1 @@ +Fixing missing notifications in FDroid variants using `optimised for battery` background sync mode \ No newline at end of file diff --git a/changelog.d/5036.feature b/changelog.d/5036.feature new file mode 100644 index 0000000000..1b72644a1f --- /dev/null +++ b/changelog.d/5036.feature @@ -0,0 +1 @@ +Allow editing polls \ No newline at end of file diff --git a/changelog.d/5037.feature b/changelog.d/5037.feature new file mode 100644 index 0000000000..45b27d4375 --- /dev/null +++ b/changelog.d/5037.feature @@ -0,0 +1 @@ +Support undisclosed polls \ No newline at end of file diff --git a/changelog.d/516.bugfix b/changelog.d/516.bugfix new file mode 100644 index 0000000000..7a5f745c32 --- /dev/null +++ b/changelog.d/516.bugfix @@ -0,0 +1 @@ +Fix for stuck local event messages at the bottom of the screen diff --git a/dependencies.gradle b/dependencies.gradle index 6cb5fac64c..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", @@ -71,6 +71,7 @@ ext.libs = [ 'espressoIntents' : "androidx.test.espresso:espresso-intents:$espresso" ], google : [ + // TODO There is 1.6.0? 'material' : "com.google.android.material:material:1.4.0" ], dagger : [ @@ -86,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 9cc5684255..7de8100469 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -4,7 +4,6 @@ ext.groups = [ ], group: [ 'com.github.Armen101', - 'com.github.BillCarsonFr', 'com.github.chrisbanes', 'com.github.hyuwah', 'com.github.jetradarmobile', @@ -155,6 +154,7 @@ ext.groups = [ 'org.jetbrains.intellij.deps', 'org.jetbrains.kotlin', 'org.jetbrains.kotlinx', + 'org.json', 'org.jsoup', 'org.junit', 'org.junit.jupiter', diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40101150.txt b/fastlane/metadata/android/cs-CZ/changelogs/40101150.txt index e82655d352..93093cb1a7 100644 --- a/fastlane/metadata/android/cs-CZ/changelogs/40101150.txt +++ b/fastlane/metadata/android/cs-CZ/changelogs/40101150.txt @@ -1,2 +1,2 @@ -Hlavní změny v této verzi: implementace hlasových zpráv dosupných v rámci laboratoře. +Hlavní změny v této verzi: implementace hlasových zpráv dosupných v experimentálních funkcích. Úplný seznam změn: https://github.com/vector-im/element-android/releases/tag/v1.1.15 diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40103100.txt b/fastlane/metadata/android/cs-CZ/changelogs/40103100.txt new file mode 100644 index 0000000000..02eb5b59ef --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40103100.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Přidání podpory pro hlasování (v experimentálních funkcích). Nový design náhledu URL. +Úplný seznam změn: https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40103110.txt b/fastlane/metadata/android/cs-CZ/changelogs/40103110.txt new file mode 100644 index 0000000000..e765e1667d --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40103110.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Opravy chyb! +Úplný seznam změn: https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40103120.txt b/fastlane/metadata/android/cs-CZ/changelogs/40103120.txt new file mode 100644 index 0000000000..81437c716b --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40103120.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Opravy chyb! +Úplný seznam změn: https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/en-US/changelogs/40103130.txt b/fastlane/metadata/android/en-US/changelogs/40103130.txt new file mode 100644 index 0000000000..1c0b5da2ee --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40103130.txt @@ -0,0 +1,2 @@ +Main changes in this version: First change in onboarding screens, including Analytics opt-in. Support for Events with Math added in the labs. +Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.13 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/40103140.txt b/fastlane/metadata/android/en-US/changelogs/40103140.txt new file mode 100644 index 0000000000..c8467d68fe --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40103140.txt @@ -0,0 +1,2 @@ +Main changes in this version: First change in onboarding screens, including Analytics opt-in. Support for Events with Math added in the labs. +Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.14 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/40103150.txt b/fastlane/metadata/android/en-US/changelogs/40103150.txt new file mode 100644 index 0000000000..2b5fbe76ca --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40103150.txt @@ -0,0 +1,2 @@ +Main changes in this version: First change in onboarding screens, including Analytics opt-in. Support for Events with Math added in the labs. +Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.15 \ No newline at end of file diff --git a/fastlane/metadata/android/et/changelogs/40103100.txt b/fastlane/metadata/android/et/changelogs/40103100.txt new file mode 100644 index 0000000000..2cb2ae0d88 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40103100.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: katseline küsitluste tugi ja linkide eelvaate uus visuaal. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/et/changelogs/40103110.txt b/fastlane/metadata/android/et/changelogs/40103110.txt new file mode 100644 index 0000000000..6271372e2b --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40103110.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: pinu veaparandusi! +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/et/changelogs/40103120.txt b/fastlane/metadata/android/et/changelogs/40103120.txt new file mode 100644 index 0000000000..c1cc3ff696 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40103120.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: pinu veaparandusi! +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/fa/changelogs/40103100.txt b/fastlane/metadata/android/fa/changelogs/40103100.txt new file mode 100644 index 0000000000..99c4e3faec --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40103100.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش:‌ افزودن پشتیبانی نظرسنجی‌ها (در آزمایشگاه‌ها). طرّاحی جدید پیش‌نمای نشانی. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/fa/changelogs/40103110.txt b/fastlane/metadata/android/fa/changelogs/40103110.txt new file mode 100644 index 0000000000..56d8ba6b91 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40103110.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش:‌ تعمیر مشکلات! +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/fa/changelogs/40103120.txt b/fastlane/metadata/android/fa/changelogs/40103120.txt new file mode 100644 index 0000000000..67976a2024 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40103120.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش:‌ تعمیر مشکلات! +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/hu-HU/changelogs/40103100.txt b/fastlane/metadata/android/hu-HU/changelogs/40103100.txt new file mode 100644 index 0000000000..9e3cb21611 --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40103100.txt @@ -0,0 +1,2 @@ +Fő változás ebben a verzióban: Szavazások támogatása (a laborok között). Új URL előnézet. +Teljes változásnapló: https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/hu-HU/changelogs/40103110.txt b/fastlane/metadata/android/hu-HU/changelogs/40103110.txt new file mode 100644 index 0000000000..86cb418a6c --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40103110.txt @@ -0,0 +1,2 @@ +Fő változás ebben a verzióban: Hibajavítások! +Teljes változásnapló: https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/hu-HU/changelogs/40103120.txt b/fastlane/metadata/android/hu-HU/changelogs/40103120.txt new file mode 100644 index 0000000000..33fa44248d --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40103120.txt @@ -0,0 +1,2 @@ +Fő változás ebben a verzióban: Hibajavítások! +Teljes változásnapló: https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/id/changelogs/40103100.txt b/fastlane/metadata/android/id/changelogs/40103100.txt new file mode 100644 index 0000000000..39d127cd93 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40103100.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Dukungan untuk fitur poll (dalam Uji Coba), dan desain tampilan URL baru. +Changelog lanjutan: https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/id/changelogs/40103110.txt b/fastlane/metadata/android/id/changelogs/40103110.txt new file mode 100644 index 0000000000..725e58d957 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40103110.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Perbaikan bug! +Changelog lanjutan: https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/id/changelogs/40103120.txt b/fastlane/metadata/android/id/changelogs/40103120.txt new file mode 100644 index 0000000000..9a5dc8026c --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40103120.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Perbaikan bug! +Changelog lanjutan: https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/it-IT/changelogs/40103100.txt b/fastlane/metadata/android/it-IT/changelogs/40103100.txt new file mode 100644 index 0000000000..d6036ff048 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40103100.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: aggiunto supporto per i sondaggi (in labs). Nuovo design anteprime URL. +Cronologia completa: https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/it-IT/changelogs/40103110.txt b/fastlane/metadata/android/it-IT/changelogs/40103110.txt new file mode 100644 index 0000000000..2db15676dc --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40103110.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: correzioni di errori! +Cronologia completa: https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/it-IT/changelogs/40103120.txt b/fastlane/metadata/android/it-IT/changelogs/40103120.txt new file mode 100644 index 0000000000..7756f8f186 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40103120.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: correzioni di errori! +Cronologia completa: https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/nl/changelogs/40103070.txt b/fastlane/metadata/android/nl/changelogs/40103070.txt new file mode 100644 index 0000000000..c2496fa34d --- /dev/null +++ b/fastlane/metadata/android/nl/changelogs/40103070.txt @@ -0,0 +1,2 @@ +Belangrijkste wijzigingen in deze versie: Bugfixes voornamelijk met betrekking tot de meldingen. +Volledige changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.7-RC2 diff --git a/fastlane/metadata/android/nl/changelogs/40103080.txt b/fastlane/metadata/android/nl/changelogs/40103080.txt new file mode 100644 index 0000000000..8ed093a460 --- /dev/null +++ b/fastlane/metadata/android/nl/changelogs/40103080.txt @@ -0,0 +1,2 @@ +Belangrijkste wijzigingen in deze versie: Bugfixes! +Volledige changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.8 diff --git a/fastlane/metadata/android/nl/changelogs/40103090.txt b/fastlane/metadata/android/nl/changelogs/40103090.txt new file mode 100644 index 0000000000..e4a7f63089 --- /dev/null +++ b/fastlane/metadata/android/nl/changelogs/40103090.txt @@ -0,0 +1,2 @@ +Belangrijkste wijzigingen in deze versie: Ondersteuning toevoegen voor spraakberichtconcept. Veel bugfixes! +Volledige changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.9 diff --git a/fastlane/metadata/android/nl/changelogs/40103100.txt b/fastlane/metadata/android/nl/changelogs/40103100.txt new file mode 100644 index 0000000000..883c656577 --- /dev/null +++ b/fastlane/metadata/android/nl/changelogs/40103100.txt @@ -0,0 +1,2 @@ +Belangrijkste wijzigingen in deze versie: Ondersteuning toevoegen voor polls (in labs). Nieuw URL-voorbeeldontwerp. +Volledige changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/nl/changelogs/40103110.txt b/fastlane/metadata/android/nl/changelogs/40103110.txt new file mode 100644 index 0000000000..ae1685270b --- /dev/null +++ b/fastlane/metadata/android/nl/changelogs/40103110.txt @@ -0,0 +1,2 @@ +Belangrijkste wijzigingen in deze versie: Bugfixes! +Volledige changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/nl/changelogs/40103120.txt b/fastlane/metadata/android/nl/changelogs/40103120.txt new file mode 100644 index 0000000000..39d3f5fb43 --- /dev/null +++ b/fastlane/metadata/android/nl/changelogs/40103120.txt @@ -0,0 +1,2 @@ +Belangrijkste wijzigingen in deze versie: Bugfixes! +Volledige changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/nl/short_description.txt b/fastlane/metadata/android/nl/short_description.txt new file mode 100644 index 0000000000..107e30f48d --- /dev/null +++ b/fastlane/metadata/android/nl/short_description.txt @@ -0,0 +1 @@ +Groepsberichten - versleutelde berichten, groepschat en videogesprekken diff --git a/fastlane/metadata/android/nl/title.txt b/fastlane/metadata/android/nl/title.txt new file mode 100644 index 0000000000..7b5a887213 --- /dev/null +++ b/fastlane/metadata/android/nl/title.txt @@ -0,0 +1 @@ +Element - Veilige Berichten diff --git a/fastlane/metadata/android/pt-BR/changelogs/40103100.txt b/fastlane/metadata/android/pt-BR/changelogs/40103100.txt new file mode 100644 index 0000000000..9912e2ccf1 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40103100.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: Adicionar suporte para sondagens (em labs). Novo design de previsualização de URL. +Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/pt-BR/changelogs/40103110.txt b/fastlane/metadata/android/pt-BR/changelogs/40103110.txt new file mode 100644 index 0000000000..a1f4d11acf --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40103110.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: Consertos de bugs! +Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/pt-BR/changelogs/40103120.txt b/fastlane/metadata/android/pt-BR/changelogs/40103120.txt new file mode 100644 index 0000000000..b511348152 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40103120.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: Consertos de bugs! +Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/sk/changelogs/40101000.txt b/fastlane/metadata/android/sk/changelogs/40101000.txt new file mode 100644 index 0000000000..5d267bd7dc --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101000.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Vylepšenie VoIP (audio a video hovory v priamych správach) a opravy chýb! +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.0 diff --git a/fastlane/metadata/android/sk/changelogs/40101010.txt b/fastlane/metadata/android/sk/changelogs/40101010.txt new file mode 100644 index 0000000000..164166fba8 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101010.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: zlepšenie výkonu a opravy chýb! +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.1 diff --git a/fastlane/metadata/android/sk/changelogs/40101020.txt b/fastlane/metadata/android/sk/changelogs/40101020.txt new file mode 100644 index 0000000000..379db42cca --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101020.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: zlepšenie výkonu a opravy chýb! +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.2 diff --git a/fastlane/metadata/android/sk/changelogs/40101030.txt b/fastlane/metadata/android/sk/changelogs/40101030.txt new file mode 100644 index 0000000000..b99ebb9e99 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101030.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: zlepšenie výkonu a opravy chýb! +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.3 diff --git a/fastlane/metadata/android/sk/changelogs/40101040.txt b/fastlane/metadata/android/sk/changelogs/40101040.txt new file mode 100644 index 0000000000..884305c478 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101040.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: zlepšenie výkonu a opravy chýb! +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.4 diff --git a/fastlane/metadata/android/sk/changelogs/40101050.txt b/fastlane/metadata/android/sk/changelogs/40101050.txt new file mode 100644 index 0000000000..22dc095371 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101050.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: rýchle opravy pre verziu 1.1.4 +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.5 diff --git a/fastlane/metadata/android/sk/changelogs/40101060.txt b/fastlane/metadata/android/sk/changelogs/40101060.txt new file mode 100644 index 0000000000..70ac5cad57 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101060.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: rýchle opravy pre verziu 1.1.5 +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.6 diff --git a/fastlane/metadata/android/sk/changelogs/40101070.txt b/fastlane/metadata/android/sk/changelogs/40101070.txt new file mode 100644 index 0000000000..87eccd8f45 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101070.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: beta podpora pre priestory Spaces. Kompresia videa pred odoslaním. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.7 diff --git a/fastlane/metadata/android/sk/changelogs/40101080.txt b/fastlane/metadata/android/sk/changelogs/40101080.txt new file mode 100644 index 0000000000..9484062b47 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101080.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: vylepšenie pre Priestory. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.8 diff --git a/fastlane/metadata/android/sk/changelogs/40101090.txt b/fastlane/metadata/android/sk/changelogs/40101090.txt new file mode 100644 index 0000000000..5778db23a9 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101090.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: pridanie podpory pre sieť gitter.im. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.9 diff --git a/fastlane/metadata/android/sk/changelogs/40101100.txt b/fastlane/metadata/android/sk/changelogs/40101100.txt new file mode 100644 index 0000000000..a23198c88b --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101100.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: aktualizácia témy a štýlu a nové funkcie pre priestory. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.10 diff --git a/fastlane/metadata/android/sk/changelogs/40101110.txt b/fastlane/metadata/android/sk/changelogs/40101110.txt new file mode 100644 index 0000000000..45a095f0a8 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101110.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: aktualizácia témy a štýlu a nové funkcie pre priestory (oprava chyby pre verziu 1.1.10) +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.11 diff --git a/fastlane/metadata/android/sk/changelogs/40101120.txt b/fastlane/metadata/android/sk/changelogs/40101120.txt new file mode 100644 index 0000000000..ba345c3150 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101120.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: aktualizácia témy a štýlu a oprava pádu po videohovore +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.12 diff --git a/fastlane/metadata/android/sk/changelogs/40101130.txt b/fastlane/metadata/android/sk/changelogs/40101130.txt new file mode 100644 index 0000000000..6daf3e789b --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101130.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: hlavne aktualizácia stability a opravy chýb. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.13 diff --git a/fastlane/metadata/android/sk/changelogs/40101140.txt b/fastlane/metadata/android/sk/changelogs/40101140.txt new file mode 100644 index 0000000000..c93fe1bb15 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101140.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: oprava problému so šifrovanými správami. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.14 diff --git a/fastlane/metadata/android/sk/changelogs/40101150.txt b/fastlane/metadata/android/sk/changelogs/40101150.txt new file mode 100644 index 0000000000..87256269ab --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101150.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: implementácia hlasových správ v rámci nastavení laboratórií. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.15 diff --git a/fastlane/metadata/android/sk/changelogs/40101160.txt b/fastlane/metadata/android/sk/changelogs/40101160.txt new file mode 100644 index 0000000000..5e12aab282 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101160.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Oprava chyby pri odosielaní zašifrovanej správy, ak sa niekto v miestnosti odhlási. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/sk/changelogs/40102000.txt b/fastlane/metadata/android/sk/changelogs/40102000.txt new file mode 100644 index 0000000000..4d0093469b --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40102000.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Hlasová správa je predvolene povolená. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/sk/changelogs/40102010.txt b/fastlane/metadata/android/sk/changelogs/40102010.txt new file mode 100644 index 0000000000..ac1cbc4509 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40102010.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Mnohé vylepšenia v oblasti VoIP a Priestorov (stále v beta verzii). +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/sk/changelogs/40103000.txt b/fastlane/metadata/android/sk/changelogs/40103000.txt new file mode 100644 index 0000000000..2a669aa744 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40103000.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Usporiadajte svoje miestnosti pomocou Priestorov! +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.3.0 diff --git a/fastlane/metadata/android/sk/changelogs/40103010.txt b/fastlane/metadata/android/sk/changelogs/40103010.txt new file mode 100644 index 0000000000..3a12a5910e --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40103010.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Usporiadajte svoje miestnosti pomocou Priestorov! Verzia v1.3.1 opravuje pád, ktorý sa môže vyskytnúť vo verzii v1.3.0. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.3.1 diff --git a/fastlane/metadata/android/sk/changelogs/40103020.txt b/fastlane/metadata/android/sk/changelogs/40103020.txt new file mode 100644 index 0000000000..96cefe73ed --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40103020.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Pridanie podpory pre Android Auto. Množstvo opráv chýb! +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.3.2 diff --git a/fastlane/metadata/android/sk/changelogs/40103030.txt b/fastlane/metadata/android/sk/changelogs/40103030.txt new file mode 100644 index 0000000000..a14dba2c9d --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40103030.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Zviditeľnite zásad servera totožností v nastaveniach. Dočasne odstránenie podpory Android Auto. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.3.3 diff --git a/fastlane/metadata/android/sk/changelogs/40103040.txt b/fastlane/metadata/android/sk/changelogs/40103040.txt new file mode 100644 index 0000000000..e2e6a98b07 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40103040.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Pridanie podpory prítomnosti pre miestnosť s priamymi správami (poznámka: prítomnosť je na matrix.org vypnutá). Opätovné pridanie podpory Android Auto. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.3.4 diff --git a/fastlane/metadata/android/sk/changelogs/40103050.txt b/fastlane/metadata/android/sk/changelogs/40103050.txt new file mode 100644 index 0000000000..f5cc73a4e2 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40103050.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Pridanie podpory prítomnosti pre miestnosť s priamymi správami (poznámka: prítomnosť je na matrix.org vypnutá). Opätovné pridanie podpory Android Auto. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.3.5 diff --git a/fastlane/metadata/android/sk/changelogs/40103100.txt b/fastlane/metadata/android/sk/changelogs/40103100.txt new file mode 100644 index 0000000000..14a667c78d --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40103100.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Pridanie podpory pre ankety (v laboratóriách). Nový dizajn náhľadu URL. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/sk/changelogs/40103110.txt b/fastlane/metadata/android/sk/changelogs/40103110.txt new file mode 100644 index 0000000000..2c2ee1aa6d --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40103110.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Opravy chýb! +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/sk/changelogs/40103120.txt b/fastlane/metadata/android/sk/changelogs/40103120.txt new file mode 100644 index 0000000000..363e4aef24 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40103120.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Opravy chýb! +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/sv-SE/changelogs/40103100.txt b/fastlane/metadata/android/sv-SE/changelogs/40103100.txt new file mode 100644 index 0000000000..d2ea16da98 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40103100.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Lägg till stöd för omröstningar (i experiment). Ny design för URL-förhandsgranskning. +Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/sv-SE/changelogs/40103110.txt b/fastlane/metadata/android/sv-SE/changelogs/40103110.txt new file mode 100644 index 0000000000..ae1fcddda9 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40103110.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Buggfixar! +Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/sv-SE/changelogs/40103120.txt b/fastlane/metadata/android/sv-SE/changelogs/40103120.txt new file mode 100644 index 0000000000..b9d73b692b --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40103120.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Buggfixar! +Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/uk/changelogs/40103100.txt b/fastlane/metadata/android/uk/changelogs/40103100.txt new file mode 100644 index 0000000000..99e4be65eb --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40103100.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: Додано підтримку опитувань (в експериментальних). Новий вигляд попереднього перегляду посилань. +Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/uk/changelogs/40103110.txt b/fastlane/metadata/android/uk/changelogs/40103110.txt new file mode 100644 index 0000000000..cc5af09cda --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40103110.txt @@ -0,0 +1,2 @@ +Основні зміни у цій версії: Виправлення помилок! +Повний перелік змін: https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/uk/changelogs/40103120.txt b/fastlane/metadata/android/uk/changelogs/40103120.txt new file mode 100644 index 0000000000..a37498b4f1 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40103120.txt @@ -0,0 +1,2 @@ +Основні зміни у цій версії: Виправлення помилок! +Повний перелік змін: https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103100.txt b/fastlane/metadata/android/zh-TW/changelogs/40103100.txt new file mode 100644 index 0000000000..70d93e833d --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40103100.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:新增對投票(在實驗室中)的支援。新的 URL 預覽設計。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103110.txt b/fastlane/metadata/android/zh-TW/changelogs/40103110.txt new file mode 100644 index 0000000000..d5450f4c6a --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40103110.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:臭蟲修復! +完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103120.txt b/fastlane/metadata/android/zh-TW/changelogs/40103120.txt new file mode 100644 index 0000000000..0ee60318c1 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40103120.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:臭蟲修復! +完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.12 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/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/SecretsSynchronisationInfo.kt b/library/core-utils/src/main/java/im/vector/lib/core/utils/compat/MutableCollectionCompat.kt similarity index 57% rename from matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/SecretsSynchronisationInfo.kt rename to library/core-utils/src/main/java/im/vector/lib/core/utils/compat/MutableCollectionCompat.kt index 6da3217070..332ed27ca3 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/SecretsSynchronisationInfo.kt +++ b/library/core-utils/src/main/java/im/vector/lib/core/utils/compat/MutableCollectionCompat.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * 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. @@ -14,14 +14,14 @@ * limitations under the License. */ -package org.matrix.android.sdk.rx +package im.vector.lib.core.utils.compat -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 android.os.Build + +fun MutableCollection.removeIfCompat(predicate: (E) -> Boolean) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + removeIf(predicate) + } else { + removeAll(filter(predicate).toSet()) + } +} 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/matrix-sdk-android-rx/.gitignore b/library/jsonviewer/.gitignore similarity index 100% rename from matrix-sdk-android-rx/.gitignore rename to library/jsonviewer/.gitignore diff --git a/library/jsonviewer/build.gradle b/library/jsonviewer/build.gradle new file mode 100644 index 0000000000..15f46754b3 --- /dev/null +++ b/library/jsonviewer/build.gradle @@ -0,0 +1,66 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-parcelize' +apply plugin: 'kotlin-kapt' +apply plugin: 'com.jakewharton.butterknife' + +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3' + } +} + +android { + compileSdk versions.compileSdk + + defaultConfig { + minSdk versions.minSdk + targetSdk versions.targetSdk + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility versions.sourceCompat + targetCompatibility versions.targetCompat + } + + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + implementation project(":library:core-utils") + + implementation libs.androidx.appCompat + implementation libs.androidx.core + + implementation libs.airbnb.epoxy + kapt libs.airbnb.epoxyProcessor + + implementation libs.airbnb.mavericks + // Span utils + implementation 'me.gujun.android:span:1.7' + + implementation libs.google.material + + implementation libs.jetbrains.coroutinesCore + implementation libs.jetbrains.coroutinesAndroid + + testImplementation 'org.json:json:20211205' + testImplementation libs.tests.junit + androidTestImplementation libs.androidx.junit + androidTestImplementation libs.androidx.espressoCore +} diff --git a/library/jsonviewer/src/main/AndroidManifest.xml b/library/jsonviewer/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..73322c2fdb --- /dev/null +++ b/library/jsonviewer/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerDialog.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerDialog.kt new file mode 100644 index 0000000000..a8d9cac849 --- /dev/null +++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerDialog.kt @@ -0,0 +1,77 @@ +/* + * 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 org.billcarsonfr.jsonviewer + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import androidx.fragment.app.DialogFragment +import com.airbnb.mvrx.Mavericks + +class JSonViewerDialog : DialogFragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_dialog_jv, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val args: JSonViewerFragmentArgs = arguments?.getParcelable(Mavericks.KEY_ARG) ?: return + if (savedInstanceState == null) { + childFragmentManager.beginTransaction() + .replace( + R.id.fragmentContainer, JSonViewerFragment.newInstance( + args.jsonString, + args.defaultOpenDepth, + true, + args.styleProvider + ) + ) + .commitNow() + } + } + + override fun onResume() { + super.onResume() + // Get existing layout params for the window + val params = dialog?.window?.attributes + // Assign window properties to fill the parent + params?.width = WindowManager.LayoutParams.MATCH_PARENT + params?.height = WindowManager.LayoutParams.MATCH_PARENT + dialog?.window?.attributes = params + } + + companion object { + fun newInstance( + jsonString: String, + initialOpenDepth: Int = -1, + styleProvider: JSonViewerStyleProvider? = null + ): JSonViewerDialog { + val args = Bundle() + val parcelableArgs = + JSonViewerFragmentArgs(jsonString, initialOpenDepth, false, styleProvider) + args.putParcelable(Mavericks.KEY_ARG, parcelableArgs) + return JSonViewerDialog().apply { arguments = args } + } + } +} diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerEpoxyController.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerEpoxyController.kt new file mode 100644 index 0000000000..96b5a9c997 --- /dev/null +++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerEpoxyController.kt @@ -0,0 +1,261 @@ +/* + * 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 org.billcarsonfr.jsonviewer + +import android.content.Context +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 + +internal class JSonViewerEpoxyController(private val context: Context) : + TypedEpoxyController() { + + private var styleProvider: JSonViewerStyleProvider = JSonViewerStyleProvider.default(context) + + fun setStyle(styleProvider: JSonViewerStyleProvider?) { + this.styleProvider = styleProvider ?: JSonViewerStyleProvider.default(context) + } + + override fun buildModels(data: JSonViewerState?) { + val async = data?.root ?: return + + when (async) { + is Fail -> { + valueItem { + id("fail") + text(async.error.localizedMessage?.toEpoxyCharSequence()) + } + } + is Success -> { + val model = data.root.invoke() + + model?.let { + buildRec(it, 0, "") + } + } + } + } + + private fun buildRec( + model: JSonViewerModel, + depth: Int, + idBase: String + ) { + val host = this + val id = "$idBase/${model.key ?: model.index}_${model.isExpanded}}" + when (model) { + is JSonViewerObject -> { + if (model.isExpanded) { + open(id, model.key, model.index, depth, true, model) + model.keys.forEach { + buildRec(it.value, depth + 1, id) + } + close(id, depth, true) + } else { + valueItem { + id(id + "_sum") + depth(depth) + text( + span { + if (model.key != null) { + span("\"${model.key}\"") { + textColor = host.styleProvider.keyColor + } + span(" : ") { + textColor = host.styleProvider.baseColor + } + } + if (model.index != null) { + span("${model.index}") { + textColor = host.styleProvider.secondaryColor + } + span(" : ") { + textColor = host.styleProvider.baseColor + } + } + span { + +"{+${model.keys.size}}" + textColor = host.styleProvider.baseColor + } + }.toEpoxyCharSequence() + ) + itemClickListener(View.OnClickListener { host.itemClicked(model) }) + } + } + } + is JSonViewerArray -> { + if (model.isExpanded) { + open(id, model.key, model.index, depth, false, model) + model.items.forEach { + buildRec(it, depth + 1, id) + } + close(id, depth, false) + } else { + valueItem { + id(id + "_sum") + depth(depth) + text( + span { + if (model.key != null) { + span("\"${model.key}\"") { + textColor = host.styleProvider.keyColor + } + span(" : ") { + textColor = host.styleProvider.baseColor + } + } + if (model.index != null) { + span("${model.index}") { + textColor = host.styleProvider.secondaryColor + } + span(" : ") { + textColor = host.styleProvider.baseColor + } + } + span { + +"[+${model.items.size}]" + textColor = host.styleProvider.baseColor + } + }.toEpoxyCharSequence() + ) + itemClickListener(View.OnClickListener { host.itemClicked(model) }) + } + } + } + is JSonViewerLeaf -> { + valueItem { + id(id) + depth(depth) + text( + span { + if (model.key != null) { + span("\"${model.key}\"") { + textColor = host.styleProvider.keyColor + } + span(" : ") { + textColor = host.styleProvider.baseColor + } + } + + if (model.index != null) { + span("${model.index}") { + textColor = host.styleProvider.secondaryColor + } + span(" : ") { + textColor = host.styleProvider.baseColor + } + } + append(host.valueToSpan(model)) + }.toEpoxyCharSequence() + ) + copyValue(model.stringRes) + } + } + } + } + + private fun valueToSpan(leaf: JSonViewerLeaf): Span { + val host = this + return when (leaf.type) { + JSONType.STRING -> { + span("\"${leaf.stringRes}\"") { + textColor = host.styleProvider.stringColor + } + } + JSONType.NUMBER -> { + span(leaf.stringRes) { + textColor = host.styleProvider.numberColor + } + } + JSONType.BOOLEAN -> { + span(leaf.stringRes) { + textColor = host.styleProvider.booleanColor + } + } + JSONType.NULL -> { + span("null") { + textColor = host.styleProvider.booleanColor + } + } + } + } + + private fun open( + id: String, + key: String?, + index: Int?, + depth: Int, + isObject: Boolean = true, + composed: JSonViewerModel + ) { + val host = this + valueItem { + id("${id}_Open") + depth(depth) + text( + span { + if (key != null) { + span("\"$key\"") { + textColor = host.styleProvider.keyColor + } + span(" : ") { + textColor = host.styleProvider.baseColor + } + } + if (index != null) { + span("$index") { + textColor = host.styleProvider.secondaryColor + } + span(" : ") { + textColor = host.styleProvider.baseColor + } + } + span("- ") { + textColor = host.styleProvider.secondaryColor + } + span("{".takeIf { isObject } ?: "[") { + textColor = host.styleProvider.baseColor + } + }.toEpoxyCharSequence() + ) + itemClickListener(View.OnClickListener { host.itemClicked(composed) }) + } + } + + private fun itemClicked(model: JSonViewerModel) { + model.isExpanded = !model.isExpanded + setData(currentData) + } + + private fun close(id: String, depth: Int, isObject: Boolean = true) { + val host = this + valueItem { + id("${id}_Close") + depth(depth) + text( + span { + text = "}".takeIf { isObject } ?: "]" + textColor = host.styleProvider.baseColor + }.toEpoxyCharSequence() + ) + } + } +} diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerFragment.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerFragment.kt new file mode 100644 index 0000000000..51e2797958 --- /dev/null +++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerFragment.kt @@ -0,0 +1,102 @@ +/* + * 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 org.billcarsonfr.jsonviewer + +import android.os.Bundle +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import com.airbnb.epoxy.EpoxyRecyclerView +import com.airbnb.mvrx.Mavericks +import com.airbnb.mvrx.MavericksView +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import kotlinx.parcelize.Parcelize + +@Parcelize +internal data class JSonViewerFragmentArgs( + val jsonString: String, + val defaultOpenDepth: Int, + val wrap: Boolean, + val styleProvider: JSonViewerStyleProvider? +) : Parcelable + +class JSonViewerFragment : Fragment(), MavericksView { + + private val viewModel: JSonViewerViewModel by fragmentViewModel() + + private val epoxyController by lazy { + JSonViewerEpoxyController(requireContext()) + } + + private lateinit var recyclerView: EpoxyRecyclerView + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val args: JSonViewerFragmentArgs? = arguments?.getParcelable(Mavericks.KEY_ARG) + val inflate = + if (args?.wrap == true) { + inflater.inflate(R.layout.fragment_jv_recycler_view_wrap, container, false) + } else { + inflater.inflate(R.layout.fragment_jv_recycler_view, container, false) + } + recyclerView = inflate.findViewById(R.id.jvRecyclerView) + recyclerView.layoutManager = + LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) + recyclerView.setController(epoxyController) + epoxyController.setStyle(args?.styleProvider) + registerForContextMenu(recyclerView) + return inflate + } + + fun showJson(jsonString: String, initialOpenDepth: Int) { + viewModel.setJsonSource(jsonString, initialOpenDepth) + } + + override fun invalidate() = withState(viewModel) { state -> + epoxyController.setData(state) + } + + companion object { + fun newInstance( + jsonString: String, + initialOpenDepth: Int = -1, + wrap: Boolean = false, + styleProvider: JSonViewerStyleProvider? = null + ): JSonViewerFragment { + return JSonViewerFragment().apply { + arguments = Bundle().apply { + putParcelable( + Mavericks.KEY_ARG, + JSonViewerFragmentArgs( + jsonString, + initialOpenDepth, + wrap, + styleProvider + ) + ) + } + } + } + } +} diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerModel.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerModel.kt new file mode 100644 index 0000000000..3d1f8dd3e2 --- /dev/null +++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerModel.kt @@ -0,0 +1,122 @@ +/* + * 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 org.billcarsonfr.jsonviewer + +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject + +internal open class JSonViewerModel(var key: String?, var index: Int?, val jObject: Any) { + var depth = 0 + var isExpanded = false +} + +internal interface Composed { + fun addChild(model: JSonViewerModel) +} + +internal class JSonViewerObject(key: String?, index: Int?, jObject: JSONObject) : + JSonViewerModel(key, index, jObject), + Composed { + + var keys = LinkedHashMap() + + override fun addChild(model: JSonViewerModel) { + keys[model.key!!] = model + } +} + +internal class JSonViewerArray(key: String?, index: Int?, jObject: JSONArray) : + JSonViewerModel(key, index, jObject), Composed { + var items = ArrayList() + + override fun addChild(model: JSonViewerModel) { + items.add(model) + } +} + +internal class JSonViewerLeaf(key: String?, index: Int?, val stringRes: String, val type: JSONType) : + JSonViewerModel(key, index, stringRes) + +internal enum class JSONType { + STRING, + NUMBER, + BOOLEAN, + NULL +} + +internal object ModelParser { + + @Throws(JSONException::class) + fun fromJsonString(jsonString: String, initialOpenDepth: Int = -1): JSonViewerObject { + val jobj = JSONObject(jsonString.trim()) + val root = JSonViewerObject(null, null, jobj).apply { isExpanded = true } + jobj.keys().forEach { + eval(root, it, null, jobj.get(it), 1, initialOpenDepth) + } + return root + } + + private fun eval(parent: Composed, key: String?, index: Int?, obj: Any, depth: Int, initialOpenDepth: Int) { + when (obj) { + is JSONObject -> { + val objectComposed = JSonViewerObject(key, index, obj) + .apply { isExpanded = initialOpenDepth == -1 || depth <= initialOpenDepth } + objectComposed.depth = depth + obj.keys().forEach { + eval(objectComposed, it, null, obj.get(it), depth + 1, initialOpenDepth) + } + parent.addChild(objectComposed) + } + is JSONArray -> { + val objectComposed = JSonViewerArray(key, index, obj) + .apply { isExpanded = initialOpenDepth == -1 || depth <= initialOpenDepth } + objectComposed.depth = depth + for (i in 0 until obj.length()) { + eval(objectComposed, null, i, obj[i], depth + 1, initialOpenDepth) + } + parent.addChild(objectComposed) + } + is String -> { + JSonViewerLeaf(key, index, obj, JSONType.STRING).let { + it.depth = depth + parent.addChild(it) + } + } + is Number -> { + JSonViewerLeaf(key, index, obj.toString(), JSONType.NUMBER).let { + it.depth = depth + parent.addChild(it) + } + } + is Boolean -> { + JSonViewerLeaf(key, index, obj.toString(), JSONType.BOOLEAN).let { + it.depth = depth + parent.addChild(it) + } + } + else -> { + if (obj == JSONObject.NULL) { + JSonViewerLeaf(key, index, "null", JSONType.NULL).let { + it.depth = depth + parent.addChild(it) + } + } + } + } + } +} diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerStyleProvider.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerStyleProvider.kt new file mode 100644 index 0000000000..4fc04c91e4 --- /dev/null +++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerStyleProvider.kt @@ -0,0 +1,45 @@ +/* + * 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 org.billcarsonfr.jsonviewer + +import android.content.Context +import android.os.Parcelable +import androidx.annotation.ColorInt +import androidx.core.content.ContextCompat +import kotlinx.parcelize.Parcelize + +@Parcelize +data class JSonViewerStyleProvider( + @ColorInt val keyColor: Int, + @ColorInt val stringColor: Int, + @ColorInt val booleanColor: Int, + @ColorInt val numberColor: Int, + @ColorInt val baseColor: Int, + @ColorInt val secondaryColor: Int +) : Parcelable { + + companion object { + fun default(context: Context) = JSonViewerStyleProvider( + keyColor = ContextCompat.getColor(context, R.color.key_color), + stringColor = ContextCompat.getColor(context, R.color.string_color), + booleanColor = ContextCompat.getColor(context, R.color.bool_color), + numberColor = ContextCompat.getColor(context, R.color.number_color), + baseColor = ContextCompat.getColor(context, R.color.base_color), + secondaryColor = ContextCompat.getColor(context, R.color.secondary_color) + ) + } +} diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerViewModel.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerViewModel.kt new file mode 100644 index 0000000000..bc3f022cfa --- /dev/null +++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerViewModel.kt @@ -0,0 +1,74 @@ +/* + * 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 org.billcarsonfr.jsonviewer + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModel +import com.airbnb.mvrx.MavericksViewModelFactory +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.ViewModelContext +import kotlinx.coroutines.launch + +internal data class JSonViewerState( + val root: Async = Uninitialized +) : MavericksState + +internal class JSonViewerViewModel(initialState: JSonViewerState) : + MavericksViewModel(initialState) { + + fun setJsonSource(json: String, initialOpenDepth: Int) { + setState { + copy(root = Loading()) + } + viewModelScope.launch { + try { + ModelParser.fromJsonString(json, initialOpenDepth).let { + setState { + copy( + root = Success(it) + ) + } + } + } catch (error: Throwable) { + setState { + copy( + root = Fail(error) + ) + } + } + } + } + + companion object : MavericksViewModelFactory { + + @JvmStatic + override fun initialState(viewModelContext: ViewModelContext): JSonViewerState? { + val arg: JSonViewerFragmentArgs = viewModelContext.args() + return try { + JSonViewerState( + Success(ModelParser.fromJsonString(arg.jsonString, arg.defaultOpenDepth)) + ) + } catch (failure: Throwable) { + JSonViewerState(Fail(failure)) + } + } + } +} diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/OptionalRx.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/Utils.kt similarity index 57% rename from matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/OptionalRx.kt rename to library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/Utils.kt index 936bd824e7..efb2bfd855 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/OptionalRx.kt +++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/Utils.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * 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. @@ -14,17 +14,17 @@ * limitations under the License. */ -package org.matrix.android.sdk.rx +package org.billcarsonfr.jsonviewer -import io.reactivex.Observable -import org.matrix.android.sdk.api.util.Optional +import android.content.Context +import android.util.TypedValue -fun Observable>.unwrap(): Observable { - return filter { it.hasValue() }.map { it.get() } -} - -fun Observable>.mapOptional(fn: (T) -> U?): Observable> { - return map { - it.map(fn) +internal object Utils { + fun dpToPx(dp: Int, context: Context): Int { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + dp.toFloat(), + context.resources.displayMetrics + ).toInt() } } diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt new file mode 100644 index 0000000000..227ac2a71d --- /dev/null +++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt @@ -0,0 +1,93 @@ +/* + * 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 org.billcarsonfr.jsonviewer + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.view.ContextMenu +import android.view.Menu +import android.view.View +import android.widget.LinearLayout +import android.widget.TextView +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: EpoxyCharSequence? = null + + @EpoxyAttribute + var depth: Int = 0 + + @EpoxyAttribute + var copyValue: String? = null + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var itemClickListener: View.OnClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.textView.text = text?.charSequence + holder.baseView.setPadding(Utils.dpToPx(16 * depth, holder.baseView.context), 0, 0, 0) + itemClickListener?.let { holder.baseView.setOnClickListener(it) } + holder.copyValue = copyValue + } + + override fun unbind(holder: Holder) { + super.unbind(holder) + holder.baseView.setOnClickListener(null) + holder.copyValue = null + } + + class Holder : EpoxyHolder(), View.OnCreateContextMenuListener { + + lateinit var textView: TextView + lateinit var baseView: LinearLayout + var copyValue: String? = null + + override fun bindView(itemView: View) { + baseView = itemView.findViewById(R.id.jvBaseLayout) + textView = itemView.findViewById(R.id.jvValueText) + itemView.setOnCreateContextMenuListener(this) + } + + override fun onCreateContextMenu( + menu: ContextMenu?, + v: View?, + menuInfo: ContextMenu.ContextMenuInfo? + ) { + if (copyValue != null) { + val menuItem = menu?.add( + Menu.NONE, R.id.copy_value, + Menu.NONE, R.string.copy_value + ) + val clipService = + v?.context?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager + menuItem?.setOnMenuItemClickListener { + clipService?.setPrimaryClip(ClipData.newPlainText("", copyValue)) + true + } + } + } + } +} diff --git a/library/jsonviewer/src/main/res/layout/fragment_dialog_jv.xml b/library/jsonviewer/src/main/res/layout/fragment_dialog_jv.xml new file mode 100644 index 0000000000..fb9e6d38c5 --- /dev/null +++ b/library/jsonviewer/src/main/res/layout/fragment_dialog_jv.xml @@ -0,0 +1,5 @@ + + diff --git a/library/jsonviewer/src/main/res/layout/fragment_jv_recycler_view.xml b/library/jsonviewer/src/main/res/layout/fragment_jv_recycler_view.xml new file mode 100644 index 0000000000..20822191e6 --- /dev/null +++ b/library/jsonviewer/src/main/res/layout/fragment_jv_recycler_view.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/library/jsonviewer/src/main/res/layout/fragment_jv_recycler_view_wrap.xml b/library/jsonviewer/src/main/res/layout/fragment_jv_recycler_view_wrap.xml new file mode 100644 index 0000000000..8b61b13111 --- /dev/null +++ b/library/jsonviewer/src/main/res/layout/fragment_jv_recycler_view_wrap.xml @@ -0,0 +1,10 @@ + + diff --git a/library/jsonviewer/src/main/res/layout/item_jv_base_value.xml b/library/jsonviewer/src/main/res/layout/item_jv_base_value.xml new file mode 100644 index 0000000000..b7dee1221b --- /dev/null +++ b/library/jsonviewer/src/main/res/layout/item_jv_base_value.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/library/jsonviewer/src/main/res/menu/jv_menu_item.xml b/library/jsonviewer/src/main/res/menu/jv_menu_item.xml new file mode 100644 index 0000000000..4da69b5117 --- /dev/null +++ b/library/jsonviewer/src/main/res/menu/jv_menu_item.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/library/jsonviewer/src/main/res/values/colors.xml b/library/jsonviewer/src/main/res/values/colors.xml new file mode 100644 index 0000000000..7b92899918 --- /dev/null +++ b/library/jsonviewer/src/main/res/values/colors.xml @@ -0,0 +1,11 @@ + + + + #FF006700 + #FF040091 + #FF980000 + #FF1700FF + #FF000000 + #FFAAAAAA + + diff --git a/library/jsonviewer/src/main/res/values/strings.xml b/library/jsonviewer/src/main/res/values/strings.xml new file mode 100644 index 0000000000..cc4b8726b4 --- /dev/null +++ b/library/jsonviewer/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Copy Value + diff --git a/library/jsonviewer/src/test/java/org/billcarsonfr/jsonviewer/ModelParseTest.kt b/library/jsonviewer/src/test/java/org/billcarsonfr/jsonviewer/ModelParseTest.kt new file mode 100644 index 0000000000..350bcdf289 --- /dev/null +++ b/library/jsonviewer/src/test/java/org/billcarsonfr/jsonviewer/ModelParseTest.kt @@ -0,0 +1,92 @@ +/* + * 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 org.billcarsonfr.jsonviewer + +import org.junit.Assert +import org.junit.Test + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ModelParseTest { + @Test + fun parsing_isCorrect() { + val string = """ + { + "glossary": { + "title": "example glossary", + "GlossDiv": { + "title": "S", + "GlossList": { + "GlossEntry": { + "ID": "SGML", + "SortAs": "SGML", + "GlossTerm": "Standard Generalized Markup Language", + "Acronym": "SGML", + "Abbrev": "ISO 8879:1986", + "GlossDef": { + "para": "A meta-markup language, used to create markup languages such as DocBook.", + "GlossSeeAlso": ["GML", "XML"] + }, + "GlossSee": "markup" + } + } + } + } + } + """.trim() + + val model = ModelParser.fromJsonString(string) + + Assert.assertEquals(0, model.depth) + Assert.assertEquals(1, model.keys.size) + Assert.assertTrue(model.keys.containsKey("glossary")) + Assert.assertTrue(model.keys["glossary"] is JSonViewerObject) + + val glossary = model.keys["glossary"] as JSonViewerObject + Assert.assertEquals(2, glossary.keys.size) + Assert.assertTrue(glossary.keys.containsKey("title")) + Assert.assertTrue(glossary.keys.containsKey("GlossDiv")) + + Assert.assertTrue(glossary.keys["title"] is JSonViewerLeaf) + (glossary.keys["title"] as JSonViewerLeaf).let { + Assert.assertEquals(JSONType.STRING, it.type) + } + + Assert.assertTrue(glossary.keys["GlossDiv"] is JSonViewerObject) + val glossDiv = glossary.keys["GlossDiv"] as JSonViewerObject + + Assert.assertTrue(glossDiv.keys["GlossList"] is JSonViewerObject) + val glossList = glossDiv.keys["GlossList"] as JSonViewerObject + + Assert.assertTrue(glossList.keys["GlossEntry"] is JSonViewerObject) + val glossEntry = glossList.keys["GlossEntry"] as JSonViewerObject + + Assert.assertTrue(glossEntry.keys["GlossDef"] is JSonViewerObject) + val glossDef = glossEntry.keys["GlossDef"] as JSonViewerObject + + Assert.assertTrue(glossDef.keys["GlossSeeAlso"] is JSonViewerArray) + val glossSeeAlso = glossDef.keys["GlossSeeAlso"] as JSonViewerArray + + Assert.assertEquals(2, glossSeeAlso.items.size) + Assert.assertEquals(0, glossSeeAlso.items.first().index) + Assert.assertNull(glossSeeAlso.items.first().key) + Assert.assertEquals("GML", (glossSeeAlso.items.first() as JSonViewerLeaf).stringRes) + } +} 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_1.xml b/library/ui-styles/src/main/res/drawable/bg_carousel_page_1.xml index bff828fb22..fa3aea4cab 100644 --- a/library/ui-styles/src/main/res/drawable/bg_carousel_page_1.xml +++ b/library/ui-styles/src/main/res/drawable/bg_carousel_page_1.xml @@ -1,6 +1,7 @@ \ No newline at end of file diff --git a/library/ui-styles/src/main/res/drawable/bg_carousel_page_2.xml b/library/ui-styles/src/main/res/drawable/bg_carousel_page_2.xml index 54e5286ded..f696823a6e 100644 --- a/library/ui-styles/src/main/res/drawable/bg_carousel_page_2.xml +++ b/library/ui-styles/src/main/res/drawable/bg_carousel_page_2.xml @@ -1,6 +1,7 @@ \ No newline at end of file diff --git a/library/ui-styles/src/main/res/drawable/bg_carousel_page_3.xml b/library/ui-styles/src/main/res/drawable/bg_carousel_page_3.xml index c31c70c078..b114f9c804 100644 --- a/library/ui-styles/src/main/res/drawable/bg_carousel_page_3.xml +++ b/library/ui-styles/src/main/res/drawable/bg_carousel_page_3.xml @@ -1,6 +1,7 @@ \ No newline at end of file diff --git a/library/ui-styles/src/main/res/drawable/bg_carousel_page_4.xml b/library/ui-styles/src/main/res/drawable/bg_carousel_page_4.xml index 56989688af..e8ee364431 100644 --- a/library/ui-styles/src/main/res/drawable/bg_carousel_page_4.xml +++ b/library/ui-styles/src/main/res/drawable/bg_carousel_page_4.xml @@ -1,6 +1,7 @@ \ No newline at end of file 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/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 936609c1d7..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.13\"" + 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/crypto/RoomEncryptionTrustLevel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/RoomEncryptionTrustLevel.kt index f381ae8455..8ba99ad70b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/RoomEncryptionTrustLevel.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/RoomEncryptionTrustLevel.kt @@ -27,5 +27,8 @@ enum class RoomEncryptionTrustLevel { Warning, // All devices in the room are verified -> the app should display a green shield - Trusted + Trusted, + + // e2e is active but with an unsupported algorithm + E2EWithUnsupportedAlgorithm } 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/session/room/crypto/RoomCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt index 6581247b90..445d16b72b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt @@ -27,9 +27,12 @@ interface RoomCryptoService { fun shouldEncryptForInvitedMembers(): Boolean /** - * Enable encryption of the room + * Enable encryption of the room. + * @param Use force to ensure that this algorithm will be used. Otherwise this call + * will throw if encryption is already setup or if the algorithm is not supported. Only to + * be used by admins to fix misconfigured encryption. */ - suspend fun enableEncryption(algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM) + suspend fun enableEncryption(algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM, force: Boolean = false) /** * Ensures all members of the room are loaded and outbound session keys are shared. 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/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomEncryptionAlgorithm.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomEncryptionAlgorithm.kt new file mode 100644 index 0000000000..f681216929 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomEncryptionAlgorithm.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2021 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 + +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM + +sealed class RoomEncryptionAlgorithm { + + abstract class SupportedAlgorithm(val alg: String) : RoomEncryptionAlgorithm() + + object Megolm : SupportedAlgorithm(MXCRYPTO_ALGORITHM_MEGOLM) + + data class UnsupportedAlgorithm(val name: String?) : RoomEncryptionAlgorithm() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt index 10cad026bc..c793a04f9d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt @@ -62,7 +62,8 @@ data class RoomSummary( val roomType: String? = null, val spaceParents: List? = null, val spaceChildren: List? = null, - val flattenParentIds: List = emptyList() + val flattenParentIds: List = emptyList(), + val roomEncryptionAlgorithm: RoomEncryptionAlgorithm? = null ) { val isVersioned: Boolean 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 51030de305..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. 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/internal/crypto/CryptoSessionInfoProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt index 5338e7e92f..82eced4371 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt @@ -37,7 +37,6 @@ internal class CryptoSessionInfoProvider @Inject constructor( fun isRoomEncrypted(roomId: String): Boolean { val encryptionEvent = monarchy.fetchCopied { realm -> EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION) - .contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"") .isEmpty(EventEntityFields.STATE_KEY) .findFirst() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index 00cd278921..0646e4d2b8 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -177,7 +177,7 @@ internal class DefaultCryptoService @Inject constructor( private val isStarted = AtomicBoolean(false) fun onStateEvent(roomId: String, event: Event) { - when (event.getClearType()) { + when (event.type) { EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) @@ -185,10 +185,13 @@ internal class DefaultCryptoService @Inject constructor( } fun onLiveEvent(roomId: String, event: Event) { - when (event.getClearType()) { - EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) - EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) - EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) + // handle state events + if (event.isStateEvent()) { + when (event.type) { + EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) + EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) + EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) + } } } @@ -575,26 +578,31 @@ internal class DefaultCryptoService @Inject constructor( // (for now at least. Maybe we should alert the user somehow?) val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId) - if (!existingAlgorithm.isNullOrEmpty() && existingAlgorithm != algorithm) { - Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId") + if (existingAlgorithm == algorithm && roomEncryptorsStore.get(roomId) != null) { + // ignore + Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Ignoring m.room.encryption for same alg ($algorithm) in $roomId") return false } val encryptingClass = MXCryptoAlgorithms.hasEncryptorClassForAlgorithm(algorithm) + // Always store even if not supported + cryptoStore.storeRoomAlgorithm(roomId, algorithm) + if (!encryptingClass) { Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Unable to encrypt room $roomId with $algorithm") return false } - cryptoStore.storeRoomAlgorithm(roomId, algorithm!!) - - val alg: IMXEncrypting = when (algorithm) { + val alg: IMXEncrypting? = when (algorithm) { MXCRYPTO_ALGORITHM_MEGOLM -> megolmEncryptionFactory.create(roomId) - else -> olmEncryptionFactory.create(roomId) + MXCRYPTO_ALGORITHM_OLM -> olmEncryptionFactory.create(roomId) + else -> null } - roomEncryptorsStore.put(roomId, alg) + if (alg != null) { + roomEncryptorsStore.put(roomId, alg) + } // if encryption was not previously enabled in this room, we will have been // ignoring new device events for these users so far. We may well have @@ -927,6 +935,7 @@ internal class DefaultCryptoService @Inject constructor( } private fun onRoomHistoryVisibilityEvent(roomId: String, event: Event) { + if (!event.isStateEvent()) return val eventContent = event.content.toModel() eventContent?.historyVisibility?.let { cryptoStore.setShouldEncryptForInvitedMembers(roomId, it != RoomHistoryVisibility.JOINED) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptionEventContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptionEventContent.kt index b64cd97ff6..dd76ae1d8e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptionEventContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptionEventContent.kt @@ -27,7 +27,7 @@ data class EncryptionEventContent( * Required. The encryption algorithm to be used to encrypt messages sent in this room. Must be 'm.megolm.v1.aes-sha2'. */ @Json(name = "algorithm") - val algorithm: String, + val algorithm: String?, /** * How long the session should be used before changing it. 604800000 (a week) is the recommended default. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt index 9b75f88f91..82fb565377 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt @@ -230,7 +230,7 @@ internal interface IMXCryptoStore { * @param roomId the id of the room. * @param algorithm the algorithm. */ - fun storeRoomAlgorithm(roomId: String, algorithm: String) + fun storeRoomAlgorithm(roomId: String, algorithm: String?) /** * Provides the algorithm used in a dedicated room. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt index 40678a6ce6..33578ba06a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt @@ -629,7 +629,7 @@ internal class RealmCryptoStore @Inject constructor( } } - override fun storeRoomAlgorithm(roomId: String, algorithm: String) { + override fun storeRoomAlgorithm(roomId: String, algorithm: String?) { doRealmTransaction(realmConfiguration) { CryptoRoomEntity.getOrCreate(it, roomId).algorithm = algorithm } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 508af250c2..1f45ac2a75 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent import org.matrix.android.sdk.api.session.room.model.VersioningState import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent import org.matrix.android.sdk.api.session.room.model.tag.RoomTag +import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent import org.matrix.android.sdk.internal.database.model.ChunkEntityFields import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntityFields @@ -55,7 +56,7 @@ internal class RealmSessionStoreMigration @Inject constructor( ) : RealmMigration { companion object { - const val SESSION_STORE_SCHEMA_VERSION = 20L + const val SESSION_STORE_SCHEMA_VERSION = 21L } /** @@ -88,6 +89,7 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion <= 17) migrateTo18(realm) if (oldVersion <= 18) migrateTo19(realm) if (oldVersion <= 19) migrateTo20(realm) + if (oldVersion <= 20) migrateTo21(realm) } private fun migrateTo1(realm: DynamicRealm) { @@ -395,6 +397,7 @@ internal class RealmSessionStoreMigration @Inject constructor( private fun migrateTo20(realm: DynamicRealm) { Timber.d("Step 19 -> 20") + realm.schema.get("ChunkEntity")?.apply { if (hasField("numberOfTimelineEvents")) { removeField("numberOfTimelineEvents") @@ -414,4 +417,32 @@ internal class RealmSessionStoreMigration @Inject constructor( } } } + + private fun migrateTo21(realm: DynamicRealm) { + Timber.d("Step 20 -> 21") + + realm.schema.get("RoomSummaryEntity") + ?.addField(RoomSummaryEntityFields.E2E_ALGORITHM, String::class.java) + ?.transform { obj -> + + val encryptionContentAdapter = MoshiProvider.providesMoshi().adapter(EncryptionEventContent::class.java) + + val encryptionEvent = realm.where("CurrentStateEventEntity") + .equalTo(CurrentStateEventEntityFields.ROOM_ID, obj.getString(RoomSummaryEntityFields.ROOM_ID)) + .equalTo(CurrentStateEventEntityFields.TYPE, EventType.STATE_ROOM_ENCRYPTION) + .findFirst() + + val encryptionEventRoot = encryptionEvent?.getObject(CurrentStateEventEntityFields.ROOT.`$`) + val algorithm = encryptionEventRoot + ?.getString(EventEntityFields.CONTENT)?.let { + encryptionContentAdapter.fromJson(it)?.algorithm + } + + obj.setString(RoomSummaryEntityFields.E2E_ALGORITHM, algorithm) + obj.setBoolean(RoomSummaryEntityFields.IS_ENCRYPTED, encryptionEvent != null) + encryptionEventRoot?.getLong(EventEntityFields.ORIGIN_SERVER_TS)?.let { + obj.setLong(RoomSummaryEntityFields.ENCRYPTION_EVENT_TS, it) + } + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/TimelineEventEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/TimelineEventEntityHelper.kt index 1d2cbcad51..ea508731b1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/TimelineEventEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/TimelineEventEntityHelper.kt @@ -30,8 +30,8 @@ internal fun TimelineEventEntity.Companion.nextId(realm: Realm): Long { } internal fun TimelineEventEntity.isMoreRecentThan(eventToCheck: TimelineEventEntity): Boolean { - val currentChunk = this.chunk?.first() ?: return false - val chunkToCheck = eventToCheck.chunk?.firstOrNull() ?: return false + val currentChunk = this.chunk?.first(null) ?: return false + val chunkToCheck = eventToCheck.chunk?.first(null) ?: return false return if (currentChunk == chunkToCheck) { this.displayIndex >= eventToCheck.displayIndex } else { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt index 3a15e0acf0..63b326096a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt @@ -16,12 +16,15 @@ package org.matrix.android.sdk.internal.database.mapper +import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.session.room.model.RoomEncryptionAlgorithm import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import org.matrix.android.sdk.api.session.room.model.SpaceParentInfo import org.matrix.android.sdk.api.session.room.model.tag.RoomTag import org.matrix.android.sdk.api.session.typing.TypingUsersTracker +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.presence.toUserPresence import javax.inject.Inject @@ -68,7 +71,9 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa isEncrypted = roomSummaryEntity.isEncrypted, encryptionEventTs = roomSummaryEntity.encryptionEventTs, breadcrumbsIndex = roomSummaryEntity.breadcrumbsIndex, - roomEncryptionTrustLevel = roomSummaryEntity.roomEncryptionTrustLevel, + roomEncryptionTrustLevel = if (roomSummaryEntity.isEncrypted && roomSummaryEntity.e2eAlgorithm != MXCRYPTO_ALGORITHM_MEGOLM) { + RoomEncryptionTrustLevel.E2EWithUnsupportedAlgorithm + } else roomSummaryEntity.roomEncryptionTrustLevel, inviterId = roomSummaryEntity.inviterId, hasFailedSending = roomSummaryEntity.hasFailedSending, roomType = roomSummaryEntity.roomType, @@ -99,7 +104,13 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa worldReadable = it.childSummaryEntity?.joinRules == RoomJoinRules.PUBLIC ) }, - flattenParentIds = roomSummaryEntity.flattenParentIds?.split("|") ?: emptyList() + flattenParentIds = roomSummaryEntity.flattenParentIds?.split("|") ?: emptyList(), + roomEncryptionAlgorithm = when (val alg = roomSummaryEntity.e2eAlgorithm) { + // I should probably use #hasEncryptorClassForAlgorithm but it says it supports + // OLM which is some legacy? Now only megolm allowed in rooms + MXCRYPTO_ALGORITHM_MEGOLM -> RoomEncryptionAlgorithm.Megolm + else -> RoomEncryptionAlgorithm.UnsupportedAlgorithm(alg) + } ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt index 67672f03ad..febedc3456 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt @@ -205,6 +205,11 @@ internal open class RoomSummaryEntity( if (value != field) field = value } + var e2eAlgorithm: String? = null + set(value) { + if (value != field) field = value + } + var encryptionEventTs: Long? = 0 set(value) { if (value != field) field = value 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 cb4bcdb606..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 @@ -119,18 +119,18 @@ internal class DefaultRoom(override val roomId: String, } } - override suspend fun enableEncryption(algorithm: String) { + override suspend fun enableEncryption(algorithm: String, force: Boolean) { when { - isEncrypted() -> { + (!force && isEncrypted() && encryptionAlgorithm() == MXCRYPTO_ALGORITHM_MEGOLM) -> { throw IllegalStateException("Encryption is already enabled for this room") } - algorithm != MXCRYPTO_ALGORITHM_MEGOLM -> { + (!force && algorithm != MXCRYPTO_ALGORITHM_MEGOLM) -> { throw InvalidParameterException("Only MXCRYPTO_ALGORITHM_MEGOLM algorithm is supported") } - else -> { + 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/RoomGetter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomGetter.kt index eb9cd9fcba..e3f4732cc1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomGetter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomGetter.kt @@ -51,7 +51,7 @@ internal class DefaultRoomGetter @Inject constructor( .equalTo(RoomSummaryEntityFields.IS_DIRECT, true) .equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) .findAll() - .firstOrNull { dm -> dm.otherMemberIds.size == 1 && dm.otherMemberIds.first() == otherUserId } + .firstOrNull { dm -> dm.otherMemberIds.size == 1 && dm.otherMemberIds.first(null) == otherUserId } ?.roomId } } 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 09fb85d3e6..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) } } 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 0cd33cd2d9..2582a0a03d 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 @@ -52,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 @@ -66,6 +67,7 @@ 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.concurrent.TimeUnit +import java.util.UUID import javax.inject.Inject /** @@ -129,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 { @@ -154,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, 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/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt index 70562de998..a7887d77f8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt @@ -38,13 +38,11 @@ import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.sync.model.RoomSyncSummary import org.matrix.android.sdk.api.session.sync.model.RoomSyncUnreadNotifications import org.matrix.android.sdk.internal.crypto.EventDecryptor -import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService +import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity -import org.matrix.android.sdk.internal.database.model.EventEntity -import org.matrix.android.sdk.internal.database.model.EventEntityFields import org.matrix.android.sdk.internal.database.model.GroupSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity @@ -57,7 +55,6 @@ import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.getOrNull import org.matrix.android.sdk.internal.database.query.isEventRead import org.matrix.android.sdk.internal.database.query.where -import org.matrix.android.sdk.internal.database.query.whereType import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.extensions.clearWith import org.matrix.android.sdk.internal.query.process @@ -123,10 +120,8 @@ internal class RoomSummaryUpdater @Inject constructor( Timber.v("## Space: Updating summary room [$roomId] roomType: [$roomType]") // Don't use current state for this one as we are only interested in having MXCRYPTO_ALGORITHM_MEGOLM event in the room - val encryptionEvent = EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION) - .contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"") - .isNotNull(EventEntityFields.STATE_KEY) - .findFirst() + val encryptionEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ENCRYPTION, stateKey = "")?.root + Timber.v("## CRYPTO: currentEncryptionEvent is $encryptionEvent") val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) @@ -152,6 +147,11 @@ internal class RoomSummaryUpdater @Inject constructor( .orEmpty() roomSummaryEntity.updateAliases(roomAliases) roomSummaryEntity.isEncrypted = encryptionEvent != null + + roomSummaryEntity.e2eAlgorithm = ContentMapper.map(encryptionEvent?.content) + ?.toModel() + ?.algorithm + roomSummaryEntity.encryptionEventTs = encryptionEvent?.originServerTs if (roomSummaryEntity.membership == Membership.INVITE && inviterId != null) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt index 14cba2a4b8..6af03a858a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt @@ -426,9 +426,9 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity, if (initialEventId != null) { frozenTimelineEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, initialEventId).findFirst()?.displayIndex } else if (direction == Timeline.Direction.BACKWARDS) { - frozenTimelineEvents.first()?.displayIndex + frozenTimelineEvents.first(null)?.displayIndex } else { - frozenTimelineEvents.last()?.displayIndex + frozenTimelineEvents.last(null)?.displayIndex } } else if (direction == Timeline.Direction.FORWARDS) { builtEvents.first().displayIndex + 1 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 3fdfb473db..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 @@ -221,6 +221,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle } val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType) + Timber.v("## received state event ${event.type} and key ${event.stateKey}") CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { // Timber.v("## Space state event: $eventEntity") eventId = event.eventId @@ -393,6 +394,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle roomMemberEventHandler.handle(realm, roomEntity.roomId, event.stateKey, fixedContent, aggregator) } } + roomMemberContentsByUser.getOrPut(event.senderId) { // If we don't have any new state on this user, get it from db val rootStateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root @@ -424,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 @@ -476,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/SyncWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt index 2f1241f4d8..bdccb59221 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 @@ -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 e3b84b4733..d3b217c517 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,8 +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/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 73ec675be0..0151950c84 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -15,7 +15,10 @@ kapt { // Note: 2 digits max for each value ext.versionMajor = 1 ext.versionMinor = 3 -ext.versionPatch = 13 +// 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 = 16 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' @@ -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 } } @@ -326,7 +335,9 @@ dependencies { implementation project(":diff-match-patch") implementation project(":multipicker") 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 @@ -362,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 @@ -458,7 +469,6 @@ dependencies { gplayImplementation 'com.google.android.gms:play-services-oss-licenses:17.0.0' implementation "androidx.emoji2:emoji2:1.0.1" - implementation('com.github.BillCarsonFr:JsonViewer:0.7') // WebRTC // org.webrtc:google-webrtc is for development purposes only diff --git a/vector/lint.xml b/vector/lint.xml index 818349da24..f02090489c 100644 --- a/vector/lint.xml +++ b/vector/lint.xml @@ -40,6 +40,7 @@ + @@ -82,10 +83,6 @@ - - - - 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..638cf9840e 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) 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 87d7e36ed5..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,8 +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", + 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 602ecef3c1..fdec5337ba 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -80,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" @@ -405,7 +406,8 @@ android:value="androidx.startup" tools:node="remove" /> - 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/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 04fe6f4723..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 @@ -106,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 @@ -451,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) 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 beb52d3082..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,7 +26,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.features.displayname.getBestName @@ -37,6 +36,7 @@ import im.vector.app.features.home.room.detail.timeline.tools.findPillsAndProces 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 /** 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/Integer.kt b/vector/src/main/java/im/vector/app/core/extensions/Integer.kt new file mode 100644 index 0000000000..2940c16fa2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/extensions/Integer.kt @@ -0,0 +1,26 @@ +/* + * 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.core.extensions + +fun Int.incrementByOneAndWrap(max: Int, min: Int = 0): Int { + val incrementedValue = this + 1 + return if (incrementedValue > max) { + min + } else { + incrementedValue + } +} 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..048dddf9e5 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 @@ -82,7 +82,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 +101,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) diff --git a/vector/src/main/java/im/vector/app/core/extensions/ViewPager2.kt b/vector/src/main/java/im/vector/app/core/extensions/ViewPager2.kt new file mode 100644 index 0000000000..ff3f02e55c --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/extensions/ViewPager2.kt @@ -0,0 +1,65 @@ +/* + * 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.core.extensions + +import android.animation.Animator +import android.animation.TimeInterpolator +import android.animation.ValueAnimator +import android.view.View +import android.view.animation.AccelerateDecelerateInterpolator +import androidx.viewpager2.widget.ViewPager2 + +fun ViewPager2.setCurrentItem( + item: Int, + duration: Long, + interpolator: TimeInterpolator = AccelerateDecelerateInterpolator(), + pagePxWidth: Int = width, +) { + val pxToDrag: Int = pagePxWidth * (item - currentItem) + val animator = ValueAnimator.ofInt(0, pxToDrag) + var previousValue = 0 + val isRtl = this.layoutDirection == View.LAYOUT_DIRECTION_RTL + + animator.addUpdateListener { valueAnimator -> + val currentValue = valueAnimator.animatedValue as Int + val currentPxToDrag = (currentValue - previousValue).toFloat() + kotlin.runCatching { + when { + isRtl -> fakeDragBy(currentPxToDrag) + else -> fakeDragBy(-currentPxToDrag) + } + previousValue = currentValue + }.onFailure { animator.cancel() } + } + animator.addListener(object : Animator.AnimatorListener { + override fun onAnimationStart(animation: Animator?) { + isUserInputEnabled = false + beginFakeDrag() + } + + override fun onAnimationEnd(animation: Animator?) { + isUserInputEnabled = true + endFakeDrag() + } + + override fun onAnimationCancel(animation: Animator?) = Unit + override fun onAnimationRepeat(animation: Animator?) = Unit + }) + animator.interpolator = interpolator + animator.duration = duration + animator.start() +} 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 323c21b76b..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 @@ -27,6 +27,7 @@ import im.vector.app.core.epoxy.VectorEpoxyModel 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. @@ -38,7 +39,7 @@ import im.vector.app.features.themes.ThemeUtils abstract class GenericFooterItem : VectorEpoxyModel() { @EpoxyAttribute - var text: String? = null + var text: EpoxyCharSequence? = null @EpoxyAttribute var style: ItemStyle = ItemStyle.NORMAL_TEXT @@ -56,7 +57,7 @@ abstract class GenericFooterItem : VectorEpoxyModel() override fun bind(holder: Holder) { super.bind(holder) - holder.text.setTextOrHide(text) + holder.text.setTextOrHide(text?.charSequence) holder.text.typeface = style.toTypeFace() holder.text.textSize = style.toTextSize() holder.text.gravity = if (centered) Gravity.CENTER_HORIZONTAL else Gravity.START 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/ui/views/NotificationAreaView.kt b/vector/src/main/java/im/vector/app/core/ui/views/NotificationAreaView.kt index 94809d2981..1615e77902 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/NotificationAreaView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/NotificationAreaView.kt @@ -25,6 +25,7 @@ import android.widget.LinearLayout import androidx.core.content.ContextCompat import androidx.core.text.italic import im.vector.app.R +import im.vector.app.core.epoxy.onClick import im.vector.app.core.error.ResourceLimitErrorFormatter import im.vector.app.core.extensions.exhaustive import im.vector.app.core.utils.DimensionConverter @@ -73,6 +74,7 @@ class NotificationAreaView @JvmOverloads constructor( is State.Default -> renderDefault() is State.Hidden -> renderHidden() is State.NoPermissionToPost -> renderNoPermissionToPost() + is State.UnsupportedAlgorithm -> renderUnsupportedAlgorithm(newState) is State.Tombstone -> renderTombstone() is State.ResourceLimitExceededError -> renderResourceLimitExceededError(newState) }.exhaustive @@ -106,6 +108,24 @@ class NotificationAreaView @JvmOverloads constructor( views.roomNotificationMessage.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_content_secondary)) } + private fun renderUnsupportedAlgorithm(e2eState: State.UnsupportedAlgorithm) { + visibility = View.VISIBLE + views.roomNotificationIcon.setImageResource(R.drawable.ic_warning_badge) + val text = if (e2eState.canRestore) { + R.string.room_unsupported_e2e_algorithm_as_admin + } else R.string.room_unsupported_e2e_algorithm + val message = span { + italic { + +resources.getString(text) + } + } + views.roomNotificationMessage.onClick { + delegate?.onMisconfiguredEncryptionClicked() + } + views.roomNotificationMessage.text = message + views.roomNotificationMessage.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_content_secondary)) + } + private fun renderResourceLimitExceededError(state: State.ResourceLimitExceededError) { visibility = View.VISIBLE val resourceLimitErrorFormatter = ResourceLimitErrorFormatter(context) @@ -163,6 +183,7 @@ class NotificationAreaView @JvmOverloads constructor( // User can't post messages to room because his power level doesn't allow it. object NoPermissionToPost : State() + data class UnsupportedAlgorithm(val canRestore: Boolean) : State() // View will be Gone object Hidden : State() @@ -179,5 +200,6 @@ class NotificationAreaView @JvmOverloads constructor( */ interface Delegate { fun onTombstoneEventClicked() + fun onMisconfiguredEncryptionClicked() } } diff --git a/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt b/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt index 44724f7954..ac0b4408b2 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt @@ -61,6 +61,10 @@ class ShieldImageView @JvmOverloads constructor( else R.drawable.ic_shield_trusted ) } + RoomEncryptionTrustLevel.E2EWithUnsupportedAlgorithm -> { + contentDescription = context.getString(R.string.a11y_trust_level_trusted) + setImageResource(R.drawable.ic_warning_badge) + } } } } @@ -71,5 +75,6 @@ fun RoomEncryptionTrustLevel.toDrawableRes(): Int { RoomEncryptionTrustLevel.Default -> R.drawable.ic_shield_black RoomEncryptionTrustLevel.Warning -> R.drawable.ic_shield_warning RoomEncryptionTrustLevel.Trusted -> R.drawable.ic_shield_trusted + RoomEncryptionTrustLevel.E2EWithUnsupportedAlgorithm -> R.drawable.ic_warning_badge } } 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..60b4686a32 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/utils/ToolbarConfig.kt @@ -0,0 +1,82 @@ +/* + * 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 [toolbar.title] + * */ + var title: CharSequence? by toolbar::title + + /** + * Delegating property for [toolbar.subtitle] + * */ + var subtitle: CharSequence? by toolbar::subtitle + + /** + * Sets toolbar's title text + * */ + fun setTitle(title: CharSequence?) = apply { toolbar.title = title } + + /** + * Sets toolbar's title text using provided string resource + * */ + fun setTitle(@StringRes titleRes: Int) = apply { toolbar.setTitle(titleRes) } + + /** + * Sets toolbar's subtitle text + * */ + fun setSubtitle(subtitle: String?) = apply { toolbar.subtitle = subtitle } + + /** + * Sets toolbar's title text using provided string resource + * */ + fun setSubtitle(@StringRes subtitleRes: Int) = apply { toolbar.subtitle = activity.getString(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 cd98356445..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,9 +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.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 @@ -48,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 ) { @@ -89,7 +90,7 @@ class DecryptionFailureTracker @Inject constructor( fun onTimeLineDisposed(roomId: String) { scope.launch(Dispatchers.Default) { synchronized(failures) { - failures.removeIf { it.roomId == roomId } + failures.removeIfCompat { it.roomId == roomId } } } } @@ -105,7 +106,7 @@ class DecryptionFailureTracker @Inject constructor( private fun removeFailureForEventId(eventId: String) { synchronized(failures) { - failures.removeIf { it.failedEventId == eventId } + failures.removeIfCompat { it.failedEventId == eventId } } } @@ -135,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/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/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..f7390282f2 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_kick_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 75fcf43292..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 @@ -22,6 +22,7 @@ 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( @@ -36,7 +37,7 @@ class RoomDevToolSendFormController @Inject constructor( genericFooterItem { id("topSpace") - text("") + text("".toEpoxyCharSequence()) } formEditTextItem { id("event_type") 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 ce04b6812d..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 @@ -43,6 +43,7 @@ sealed class RoomDetailAction : VectorViewModelAction { object MarkAllAsRead : RoomDetailAction() data class DownloadOrOpen(val eventId: String, val senderId: String?, val messageFileContent: MessageWithAttachmentContent) : RoomDetailAction() object JoinAndOpenReplacementRoom : RoomDetailAction() + object OnClickMisconfiguredEncryption : RoomDetailAction() object AcceptInvite : RoomDetailAction() object RejectInvite : 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 e0003a038c..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 @@ -117,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 @@ -134,12 +136,14 @@ import im.vector.app.features.command.Command import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.composer.CanSendStatus import im.vector.app.features.home.room.detail.composer.MessageComposerAction import im.vector.app.features.home.room.detail.composer.MessageComposerView import im.vector.app.features.home.room.detail.composer.MessageComposerViewEvents import im.vector.app.features.home.room.detail.composer.MessageComposerViewModel import im.vector.app.features.home.room.detail.composer.MessageComposerViewState import im.vector.app.features.home.room.detail.composer.SendMode +import im.vector.app.features.home.room.detail.composer.boolean import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.RecordingUiState import im.vector.app.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet @@ -175,6 +179,7 @@ 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 @@ -202,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 @@ -239,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( @@ -337,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)) @@ -363,6 +371,7 @@ class RoomDetailFragment @Inject constructor( keyboardStateUtils = KeyboardStateUtils(requireActivity()) lazyLoadedViews.bind(views) setupToolbar(views.roomToolbar) + .allowBack() setupRecyclerView() setupComposer() setupNotificationView() @@ -396,7 +405,7 @@ class RoomDetailFragment @Inject constructor( } messageComposerViewModel.onEach(MessageComposerViewState::sendMode, MessageComposerViewState::canSendMessage) { mode, canSend -> - if (!canSend) { + if (!canSend.boolean()) { return@onEach } when (mode) { @@ -463,7 +472,8 @@ class RoomDetailFragment @Inject constructor( is RoomDetailViewEvents.OpenRoom -> handleOpenRoom(it) RoomDetailViewEvents.OpenInvitePeople -> navigator.openInviteUsersToRoom(requireContext(), roomDetailArgs.roomId) RoomDetailViewEvents.OpenSetRoomAvatarDialog -> galleryOrCameraDialogHelper.show() - RoomDetailViewEvents.OpenRoomSettings -> handleOpenRoomSettings() + RoomDetailViewEvents.OpenRoomSettings -> handleOpenRoomSettings(RoomProfileActivity.EXTRA_DIRECT_ACCESS_ROOM_SETTINGS) + RoomDetailViewEvents.OpenRoomProfile -> handleOpenRoomSettings() is RoomDetailViewEvents.ShowRoomAvatarFullScreen -> it.matrixItem?.let { item -> navigator.openBigImageViewer(requireActivity(), it.view, item) } @@ -588,11 +598,11 @@ class RoomDetailFragment @Inject constructor( ) } - private fun handleOpenRoomSettings() { + private fun handleOpenRoomSettings(directAccess: Int? = null) { navigator.openRoomProfile( requireContext(), roomDetailArgs.roomId, - RoomProfileActivity.EXTRA_DIRECT_ACCESS_ROOM_SETTINGS + directAccess ) } @@ -688,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() } } @@ -963,6 +973,10 @@ class RoomDetailFragment @Inject constructor( override fun onTombstoneEventClicked() { roomDetailViewModel.handle(RoomDetailAction.JoinAndOpenReplacementRoom) } + + override fun onMisconfiguredEncryptionClicked() { + roomDetailViewModel.handle(RoomDetailAction.OnClickMisconfiguredEncryption) + } } } @@ -1284,7 +1298,7 @@ class RoomDetailFragment @Inject constructor( val canSendMessage = withState(messageComposerViewModel) { it.canSendMessage } - if (!canSendMessage) { + if (!canSendMessage.boolean()) { return false } return when (model) { @@ -1375,7 +1389,6 @@ 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) @@ -1405,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 @@ -1463,10 +1477,18 @@ class RoomDetailFragment @Inject constructor( views.voiceMessageRecorderView.render(messageComposerState.voiceRecordingUiState) views.composerLayout.setRoomEncrypted(summary.isEncrypted) // views.composerLayout.alwaysShowSendButton = false - if (messageComposerState.canSendMessage) { - views.notificationAreaView.render(NotificationAreaView.State.Hidden) - } else { - views.notificationAreaView.render(NotificationAreaView.State.NoPermissionToPost) + when (messageComposerState.canSendMessage) { + CanSendStatus.Allowed -> { + NotificationAreaView.State.Hidden + } + CanSendStatus.NoPermission -> { + NotificationAreaView.State.NoPermissionToPost + } + is CanSendStatus.UnSupportedE2eAlgorithm -> { + NotificationAreaView.State.UnsupportedAlgorithm(mainState.isAllowedToSetupEncryption) + } + }.let { + views.notificationAreaView.render(it) } } else { views.hideComposerViews() @@ -1511,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)) @@ -2022,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) @@ -2121,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 @@ -2234,7 +2258,7 @@ class RoomDetailFragment @Inject constructor( 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.POLL -> navigator.openCreatePoll(requireContext(), roomDetailArgs.roomId, null, PollMode.CREATE) AttachmentTypeSelectorView.Type.LOCATION -> { navigator .openLocationSharing( 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 de1110dfcb..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 @@ -49,6 +49,7 @@ sealed class RoomDetailViewEvents : VectorViewEvents { object OpenInvitePeople : RoomDetailViewEvents() object OpenSetRoomAvatarDialog : RoomDetailViewEvents() object OpenRoomSettings : RoomDetailViewEvents() + object OpenRoomProfile : RoomDetailViewEvents() data class ShowRoomAvatarFullScreen(val matrixItem: MatrixItem?, val view: View?) : RoomDetailViewEvents() object ShowWaitingView : 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 0fe67b3882..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 @@ -56,6 +58,8 @@ 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 @@ -113,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 { @@ -180,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() { @@ -212,11 +236,13 @@ class RoomDetailViewModel @AssistedInject constructor( val canInvite = PowerLevelsHelper(it).isUserAbleToInvite(session.myUserId) val isAllowedToManageWidgets = session.widgetService().hasPermissionsToHandleWidgets(room.roomId) val isAllowedToStartWebRTCCall = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.CALL_INVITE) + val isAllowedToSetupEncryption = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_ENCRYPTION) setState { copy( canInvite = canInvite, isAllowedToManageWidgets = isAllowedToManageWidgets, - isAllowedToStartWebRTCCall = isAllowedToStartWebRTCCall + isAllowedToStartWebRTCCall = isAllowedToStartWebRTCCall, + isAllowedToSetupEncryption = isAllowedToSetupEncryption ) } }.launchIn(viewModelScope) @@ -310,6 +336,7 @@ class RoomDetailViewModel @AssistedInject constructor( is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action) is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action) is RoomDetailAction.JoinAndOpenReplacementRoom -> handleJoinAndOpenReplacementRoom() + is RoomDetailAction.OnClickMisconfiguredEncryption -> handleClickMisconfiguredE2E() is RoomDetailAction.ResendMessage -> handleResendEvent(action) is RoomDetailAction.RemoveFailedEcho -> handleRemove(action) is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead() @@ -620,6 +647,12 @@ class RoomDetailViewModel @AssistedInject constructor( } } + private fun handleClickMisconfiguredE2E() = withState { state -> + if (state.isAllowedToSetupEncryption) { + _viewEvents.post(RoomDetailViewEvents.OpenRoomProfile) + } + } + private fun isIntegrationEnabled() = session.integrationManagerService().isIntegrationEnabled() fun isMenuItemVisible(@IdRes itemId: Int): Boolean = com.airbnb.mvrx.withState(this) { state -> @@ -706,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 042a415b47..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 @@ -64,15 +64,18 @@ data class RoomDetailViewState( val canInvite: Boolean = true, val isAllowedToManageWidgets: Boolean = false, 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 a63a06928a..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 @@ -38,6 +40,7 @@ import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.voice.VoicePlayerHelper import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session @@ -47,6 +50,7 @@ import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.RoomAvatarContent +import org.matrix.android.sdk.api.session.room.model.RoomEncryptionAlgorithm import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper @@ -55,6 +59,8 @@ import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent import org.matrix.android.sdk.api.session.room.timeline.getRelationContent import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent import org.matrix.android.sdk.api.session.space.CreateSpaceParams +import org.matrix.android.sdk.flow.flow +import org.matrix.android.sdk.flow.unwrap import timber.log.Timber class MessageComposerViewModel @AssistedInject constructor( @@ -62,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) { @@ -74,7 +82,7 @@ class MessageComposerViewModel @AssistedInject constructor( init { loadDraftIfAny() - observePowerLevel() + observePowerLevelAndEncryption() subscribeToStateInternal() } @@ -137,12 +145,30 @@ class MessageComposerViewModel @AssistedInject constructor( } } - private fun observePowerLevel() { - PowerLevelsFlowFactory(room).createFlow() - .setOnEach { - val canSendMessage = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.MESSAGE) - copy(canSendMessage = canSendMessage) + private fun observePowerLevelAndEncryption() { + combine( + PowerLevelsFlowFactory(room).createFlow(), + room.flow().liveRoomSummary().unwrap() + ) { pl, sum -> + val canSendMessage = PowerLevelsHelper(pl).isUserAllowedToSend(session.myUserId, false, EventType.MESSAGE) + if (canSendMessage) { + val isE2E = sum.isEncrypted + if (isE2E) { + val roomEncryptionAlgorithm = sum.roomEncryptionAlgorithm + if (roomEncryptionAlgorithm is RoomEncryptionAlgorithm.UnsupportedAlgorithm) { + CanSendStatus.UnSupportedE2eAlgorithm(roomEncryptionAlgorithm.name) + } else { + CanSendStatus.Allowed + } + } else { + CanSendStatus.Allowed } + } else { + CanSendStatus.NoPermission + } + }.setOnEach { + copy(canSendMessage = it) + } } private fun handleEnterQuoteMode(action: MessageComposerAction.EnterQuoteMode) { @@ -161,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) @@ -217,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) @@ -498,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)) @@ -552,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) } } @@ -576,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) } } @@ -619,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/MessageComposerViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt index 578de9bd89..915e1b3338 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt @@ -43,9 +43,23 @@ sealed interface SendMode { data class Voice(val text: String) : SendMode } +sealed interface CanSendStatus { + object Allowed : CanSendStatus + object NoPermission : CanSendStatus + data class UnSupportedE2eAlgorithm(val algorithm: String?) : CanSendStatus +} + +fun CanSendStatus.boolean(): Boolean { + return when (this) { + CanSendStatus.Allowed -> true + CanSendStatus.NoPermission -> false + is CanSendStatus.UnSupportedE2eAlgorithm -> false + } +} + data class MessageComposerViewState( val roomId: String, - val canSendMessage: Boolean = true, + val canSendMessage: CanSendStatus = CanSendStatus.Allowed, val isSendButtonVisible: Boolean = false, val sendMode: SendMode = SendMode.Regular("", false), val voiceRecordingUiState: VoiceMessageRecorderView.RecordingUiState = VoiceMessageRecorderView.RecordingUiState.Idle @@ -60,8 +74,8 @@ data class MessageComposerViewState( val isVoiceMessageIdle = !isVoiceRecording - val isComposerVisible = canSendMessage && !isVoiceRecording - val isVoiceMessageRecorderVisible = canSendMessage && !isSendButtonVisible + val isComposerVisible = canSendMessage.boolean() && !isVoiceRecording + val isVoiceMessageRecorderVisible = canSendMessage.boolean() && !isSendButtonVisible @Suppress("UNUSED") // needed by mavericks constructor(args: RoomDetailArgs) : this(roomId = args.roomId) 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/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index 241ccb7428..4a9a03789f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -72,6 +72,7 @@ import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import timber.log.Timber import javax.inject.Inject +import kotlin.math.min import kotlin.system.measureTimeMillis class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter, @@ -185,6 +186,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec override fun onChanged(position: Int, count: Int, payload: Any?) { synchronized(modelCache) { assertUpdateCallbacksAllowed() + Timber.v("listUpdateCallback.onChanged(position: $position, count: $count). " + + "\ncurrentSnapshot has size of ${currentSnapshot.size} items") (position until position + count).forEach { // Invalidate cache modelCache[it] = null @@ -192,10 +195,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec // Also invalidate the first previous displayable event if // it's sent by the same user so we are sure we have up to date information. val invalidatedSenderId: String? = currentSnapshot.getOrNull(position)?.senderInfo?.userId - val prevDisplayableEventIndex = currentSnapshot.subList(0, position).indexOfLast { + // In some cases onChanged will be called before onRemoved and onInserted so position will be bigger than currentSnapshot.size. + val prevList = currentSnapshot.subList(0, min(position, currentSnapshot.size)) + val prevDisplayableEventIndex = prevList.indexOfLast { timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId) } - if (prevDisplayableEventIndex != -1 && currentSnapshot[prevDisplayableEventIndex].senderInfo.userId == invalidatedSenderId) { + if (prevDisplayableEventIndex != -1 && currentSnapshot.getOrNull(prevDisplayableEventIndex)?.senderInfo?.userId == invalidatedSenderId) { modelCache[prevDisplayableEventIndex] = null } requestModelBuild() @@ -205,6 +210,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec override fun onMoved(fromPosition: Int, toPosition: Int) { synchronized(modelCache) { assertUpdateCallbacksAllowed() + Timber.v("listUpdateCallback.onMoved(fromPosition: $fromPosition, toPosition: $toPosition). " + + "\ncurrentSnapshot has size of ${currentSnapshot.size} items") val model = modelCache.removeAt(fromPosition) modelCache.add(toPosition, model) requestModelBuild() @@ -214,6 +221,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec override fun onInserted(position: Int, count: Int) { synchronized(modelCache) { assertUpdateCallbacksAllowed() + Timber.v("listUpdateCallback.onInserted(position: $position, count: $count). " + + "\ncurrentSnapshot has size of ${currentSnapshot.size} items") repeat(count) { modelCache.add(position, null) } @@ -224,6 +233,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec override fun onRemoved(position: Int, count: Int) { synchronized(modelCache) { assertUpdateCallbacksAllowed() + Timber.v("listUpdateCallback.onRemoved(position: $position, count: $count). " + + "\ncurrentSnapshot has size of ${currentSnapshot.size} items") repeat(count) { modelCache.removeAt(position) } @@ -306,6 +317,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec inSubmitList = true val diffCallback = TimelineEventDiffUtilCallback(currentSnapshot, newSnapshot) currentSnapshot = newSnapshot + Timber.v("Submit a new snapshot of ${currentSnapshot.size} items.") val diffResult = DiffUtil.calculateDiff(diffCallback) diffResult.dispatchUpdatesTo(listUpdateCallback) requestDelayedModelBuild(0) 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 479459c4bc..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,7 +27,6 @@ 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 @@ -42,6 +41,7 @@ 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 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 2b3d391634..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)) { @@ -467,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) ) } @@ -517,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 96d139f11d..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 @@ -62,7 +62,7 @@ class ViewEditHistoryEpoxyController @Inject constructor( is Fail -> { genericFooterItem { id("failure") - text(host.stringProvider.getString(R.string.unknown_error)) + text(host.stringProvider.getString(R.string.unknown_error).toEpoxyCharSequence()) } } is Success -> { 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/EncryptionItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt index 14d9cce28a..0ff786d504 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt @@ -63,9 +63,9 @@ class EncryptionItemFactory @Inject constructor( ) shield = StatusTileTimelineItem.ShieldUIState.BLACK } else { - title = stringProvider.getString(R.string.encryption_not_enabled) + title = stringProvider.getString(R.string.encryption_misconfigured) description = stringProvider.getString(R.string.encryption_unknown_algorithm_tile_description) - shield = StatusTileTimelineItem.ShieldUIState.RED + shield = StatusTileTimelineItem.ShieldUIState.ERROR } return StatusTileTimelineItem_() .attributes( 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 17eb076b09..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,7 +28,6 @@ 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 @@ -76,6 +75,7 @@ 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 @@ -96,6 +96,7 @@ 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 @@ -228,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) + } } } @@ -252,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) @@ -262,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/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/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/item/StatusTileTimelineItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/StatusTileTimelineItem.kt index 6531efb82d..a3d9d3995c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/StatusTileTimelineItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/StatusTileTimelineItem.kt @@ -57,6 +57,7 @@ abstract class StatusTileTimelineItem : AbsBaseMessageItem R.drawable.ic_shield_trusted ShieldUIState.BLACK -> R.drawable.ic_shield_black ShieldUIState.RED -> R.drawable.ic_shield_warning + ShieldUIState.ERROR -> R.drawable.ic_warning_badge } holder.titleView.setCompoundDrawablesWithIntrinsicBounds( @@ -98,6 +99,7 @@ abstract class StatusTileTimelineItem : AbsBaseMessageItem { genericFooterItem { id("failure") - text(host.stringProvider.getString(R.string.unknown_error)) + text(host.stringProvider.getString(R.string.unknown_error).toEpoxyCharSequence()) } } is Success -> { 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 2da3dab16a..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 @@ -22,6 +22,7 @@ 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 @@ -40,7 +41,7 @@ class RoomWidgetsController @Inject constructor( if (widgets.isEmpty()) { genericFooterItem { id("empty") - text(host.stringProvider.getString(R.string.room_no_active_widgets)) + text(host.stringProvider.getString(R.string.room_no_active_widgets).toEpoxyCharSequence()) } } else { widgets.forEach { widget -> 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/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 07630796e6..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 @@ -74,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 @@ -313,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) } @@ -528,10 +529,10 @@ 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) } 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 06f38b381d..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 @@ -30,6 +30,7 @@ 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 @@ -78,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) @@ -150,7 +151,7 @@ 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, 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 5d1e0fdade..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,13 +852,17 @@ class OnboardingViewModel @AssistedInject constructor( } withState { - when (it.onboardingFlow) { - OnboardingFlow.SignIn -> handleUpdateSignMode(OnboardingAction.UpdateSignMode(SignMode.SignIn)) - OnboardingFlow.SignUp -> handleUpdateSignMode(OnboardingAction.UpdateSignMode(SignMode.SignUp)) - OnboardingFlow.SignInSignUp, - null -> { - _viewEvents.post(OnboardingViewEvents.OnLoginFlowRetrieved) + if (loginMode.supportsSignModeScreen()) { + when (it.onboardingFlow) { + OnboardingFlow.SignIn -> handleUpdateSignMode(OnboardingAction.UpdateSignMode(SignMode.SignIn)) + OnboardingFlow.SignUp -> handleUpdateSignMode(OnboardingAction.UpdateSignMode(SignMode.SignUp)) + OnboardingFlow.SignInSignUp, + null -> { + _viewEvents.post(OnboardingViewEvents.OnLoginFlowRetrieved) + } } + } else { + _viewEvents.post(OnboardingViewEvents.OnLoginFlowRetrieved) } } } @@ -850,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 f89ae36eb6..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 @@ -22,24 +22,35 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope +import androidx.viewpager2.widget.ViewPager2 import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.tabs.TabLayoutMediator import im.vector.app.BuildConfig import im.vector.app.R +import im.vector.app.core.extensions.incrementByOneAndWrap +import im.vector.app.core.extensions.setCurrentItem import im.vector.app.databinding.FragmentFtueSplashCarouselBinding import im.vector.app.features.VectorFeatures import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingFlow import im.vector.app.features.settings.VectorPreferences +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.failure.Failure import java.net.UnknownHostException import javax.inject.Inject +private const val CAROUSEL_ROTATION_DELAY_MS = 5000L +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 { @@ -52,13 +63,18 @@ class FtueAuthSplashCarouselFragment @Inject constructor( } private fun setupViews() { - views.splashCarousel.adapter = carouselController.adapter + 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() } } @@ -69,10 +85,38 @@ class FtueAuthSplashCarouselFragment @Inject constructor( "Branch: ${BuildConfig.GIT_BRANCH_NAME}" views.loginSplashVersion.debouncedClicks { navigator.openDebug(requireContext()) } } + views.splashCarousel.registerAutomaticUntilInteractionTransitions() } - private fun getStarted() { - val getStartedFlow = if (vectorFeatures.isAlreadyHaveAccountSplashEnabled()) OnboardingFlow.SignUp else OnboardingFlow.SignInSignUp + 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() + } + } + }) + } + + private fun ViewPager2.scheduleCarouselTransition(): Job { + val itemCount = adapter?.itemCount ?: throw IllegalStateException("An adapter must be set") + return lifecycleScope.launch { + delay(CAROUSEL_ROTATION_DELAY_MS) + setCurrentItem(currentItem.incrementByOneAndWrap(max = itemCount - 1), duration = CAROUSEL_TRANSITION_TIME_MS) + } + } + + 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..6310eaea95 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthUseCaseFragment.kt @@ -0,0 +1,72 @@ +/* + * 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.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.annotation.StringRes +import im.vector.app.R +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 javax.inject.Inject + +class FtueAuthUseCaseFragment @Inject constructor() : 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.setUseCase(R.string.ftue_auth_use_case_option_one, FtueUseCase.FRIENDS_FAMILY) + views.useCaseOptionTwo.setUseCase(R.string.ftue_auth_use_case_option_two, FtueUseCase.TEAMS) + views.useCaseOptionThree.setUseCase(R.string.ftue_auth_use_case_option_three, FtueUseCase.COMMUNITIES) + + 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.setUseCase(@StringRes label: Int, useCase: FtueUseCase) { + setText(label) + debouncedClicks { + viewModel.handle(OnboardingAction.UpdateUseCase(useCase)) + } + } +} 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..2a977684e0 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,26 @@ 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 +131,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/vector/src/main/java/im/vector/app/features/poll/create/PollMode.kt b/vector/src/main/java/im/vector/app/features/poll/create/PollMode.kt new file mode 100644 index 0000000000..0007589d10 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/poll/create/PollMode.kt @@ -0,0 +1,22 @@ +/* + * 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 + +enum class PollMode { + CREATE, + EDIT +} 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/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= Build.VERSION_CODES.N) { - toUpdateWhenNotBusy.removeIf { it.second == holder } - } else { - val index = toUpdateWhenNotBusy.indexOfFirst { it.second == holder } - if (index != -1) { - toUpdateWhenNotBusy.removeAt(index) - } - } + toUpdateWhenNotBusy.removeIfCompat { it.second == holder } } super.onViewRecycled(holder) } diff --git a/vector/src/main/java/im/vector/app/features/reactions/EmojiSearchResultController.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiSearchResultController.kt index c9903e396e..f699a86d33 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/EmojiSearchResultController.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/EmojiSearchResultController.kt @@ -22,6 +22,7 @@ import im.vector.app.EmojiCompatFontProvider import im.vector.app.R 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 EmojiSearchResultController @Inject constructor( @@ -52,13 +53,13 @@ class EmojiSearchResultController @Inject constructor( // display 'Type something to find' genericFooterItem { id("type.query.item") - text(host.stringProvider.getString(R.string.reaction_search_type_hint)) + text(host.stringProvider.getString(R.string.reaction_search_type_hint).toEpoxyCharSequence()) } } else { // Display no search Results genericFooterItem { id("no.results.item") - text(host.stringProvider.getString(R.string.no_result_placeholder)) + text(host.stringProvider.getString(R.string.no_result_placeholder).toEpoxyCharSequence()) } } } else { 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 b9c7f1124c..22a1779a2b 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 @@ -23,6 +23,7 @@ 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 @@ -95,11 +96,14 @@ class RoomMemberProfileController @Inject constructor( private fun buildSecuritySection(state: RoomMemberProfileViewState) { // Security - buildProfileSection(stringProvider.getString(R.string.room_profile_section_security)) val host = this if (state.isRoomEncrypted) { - if (state.userMXCrossSigningInfo != null) { + if (!state.isAlgorithmSupported) { + // TODO find sensible message to display here + // For now we just remove the verify actions as well as the Security status + } else if (state.userMXCrossSigningInfo != null) { + buildProfileSection(stringProvider.getString(R.string.room_profile_section_security)) // Cross signing is enabled for this user if (state.userMXCrossSigningInfo.isTrusted()) { // User is trusted @@ -147,11 +151,13 @@ class RoomMemberProfileController @Inject constructor( genericFooterItem { id("verify_footer") - text(host.stringProvider.getString(R.string.room_profile_encrypted_subtitle)) + text(host.stringProvider.getString(R.string.room_profile_encrypted_subtitle).toEpoxyCharSequence()) centered(false) } } } else { + buildProfileSection(stringProvider.getString(R.string.room_profile_section_security)) + buildProfileAction( id = "learn_more", title = stringProvider.getString(R.string.room_profile_section_security_learn_more), @@ -162,9 +168,11 @@ class RoomMemberProfileController @Inject constructor( ) } } else { + buildProfileSection(stringProvider.getString(R.string.room_profile_section_security)) + genericFooterItem { id("verify_footer_not_encrypted") - text(host.stringProvider.getString(R.string.room_profile_not_encrypted_subtitle)) + text(host.stringProvider.getString(R.string.room_profile_not_encrypted_subtitle).toEpoxyCharSequence()) centered(false) } } 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..872f86d44b 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() 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 4f3d9d0776..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 @@ -52,6 +52,7 @@ import org.matrix.android.sdk.api.session.profile.ProfileService 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.Membership +import org.matrix.android.sdk.api.session.room.model.RoomEncryptionAlgorithm import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.powerlevels.Role @@ -210,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)) @@ -254,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)) @@ -344,7 +345,15 @@ class RoomMemberProfileViewModel @AssistedInject constructor( }.launchIn(viewModelScope) roomSummaryLive.execute { - copy(isRoomEncrypted = it.invoke()?.isEncrypted == true) + val summary = it.invoke() ?: return@execute this + if (summary.isEncrypted) { + copy( + isRoomEncrypted = true, + isAlgorithmSupported = summary.roomEncryptionAlgorithm is RoomEncryptionAlgorithm.SupportedAlgorithm + ) + } else { + copy(isRoomEncrypted = false) + } } roomSummaryLive.combine(powerLevelsContentLive) { roomSummary, powerLevelsContent -> val roomName = roomSummary.toMatrixItem().getBestName() diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewState.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewState.kt index 1f2c3d6ce4..94bf9e8f8e 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewState.kt @@ -33,6 +33,7 @@ data class RoomMemberProfileViewState( val isMine: Boolean = false, val isIgnored: Async = Uninitialized, val isRoomEncrypted: Boolean = false, + val isAlgorithmSupported: Boolean = true, val powerLevelsContent: PowerLevelsContent? = null, val userPowerLevelString: Async = Uninitialized, val userMatrixItem: Async = Uninitialized, 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 83a4e614ec..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 @@ -97,7 +97,7 @@ class DeviceListEpoxyController @Inject constructor(private val stringProvider: // Can this really happen? genericFooterItem { id("empty") - text(host.stringProvider.getString(R.string.search_no_results)) + text(host.stringProvider.getString(R.string.search_no_results).toEpoxyCharSequence()) } } else { // Build list of device with status 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 c700842c19..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 @@ -67,12 +67,12 @@ class DeviceTrustInfoEpoxyController @Inject constructor(private val stringProvi // TODO FORMAT text(host.stringProvider.getString(R.string.verification_profile_device_verified_because, data.userItem?.displayName ?: "", - data.userItem?.id ?: "")) + data.userItem?.id ?: "").toEpoxyCharSequence()) } else { // TODO what if mine text(host.stringProvider.getString(R.string.verification_profile_device_new_signing, data.userItem?.displayName ?: "", - data.userItem?.id ?: "")) + data.userItem?.id ?: "").toEpoxyCharSequence()) } } // text(stringProvider.getString(R.string.verification_profile_device_untrust_info)) @@ -98,7 +98,7 @@ class DeviceTrustInfoEpoxyController @Inject constructor(private val stringProvi id("warn") centered(false) textColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) - text(host.stringProvider.getString(R.string.verification_profile_device_untrust_info)) + text(host.stringProvider.getString(R.string.verification_profile_device_untrust_info).toEpoxyCharSequence()) } bottomSheetVerificationActionItem { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt index 073d30ff8e..22b040b4c0 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt @@ -26,4 +26,5 @@ sealed class RoomProfileAction : VectorViewModelAction { data class ChangeRoomNotificationState(val notificationState: RoomNotificationState) : RoomProfileAction() object ShareRoomProfile : RoomProfileAction() object CreateShortcut : RoomProfileAction() + object RestoreEncryptionState : RoomProfileAction() } 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 b237faa17d..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 @@ -23,6 +23,7 @@ import im.vector.app.core.epoxy.expandableTextItem import im.vector.app.core.epoxy.profiles.buildProfileAction import im.vector.app.core.epoxy.profiles.buildProfileSection import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.resources.DrawableProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericPositiveButtonItem @@ -30,7 +31,11 @@ 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 +import org.matrix.android.sdk.api.session.room.model.RoomEncryptionAlgorithm import org.matrix.android.sdk.api.session.room.model.RoomSummary import javax.inject.Inject @@ -38,6 +43,7 @@ class RoomProfileController @Inject constructor( private val stringProvider: StringProvider, private val colorProvider: ColorProvider, private val vectorPreferences: VectorPreferences, + private val drawableProvider: DrawableProvider, private val shortcutCreator: ShortcutCreator ) : TypedEpoxyController() { @@ -59,6 +65,7 @@ class RoomProfileController @Inject constructor( fun onRoomDevToolsClicked() fun onUrlInTopicLongClicked(url: String) fun doMigrateToVersion(newVersion: String) + fun restoreEncryptionState() } override fun buildModels(data: RoomProfileViewState?) { @@ -101,7 +108,7 @@ class RoomProfileController @Inject constructor( data.recommendedRoomVersion != null) { genericFooterItem { id("version_warning") - text(host.stringProvider.getString(R.string.room_using_unstable_room_version, roomVersion)) + text(host.stringProvider.getString(R.string.room_using_unstable_room_version, roomVersion).toEpoxyCharSequence()) textColor(host.colorProvider.getColorFromAttribute(R.attr.colorError)) centered(false) } @@ -113,15 +120,58 @@ class RoomProfileController @Inject constructor( } } - val learnMoreSubtitle = if (roomSummary.isEncrypted) { - if (roomSummary.isDirect) R.string.direct_room_profile_encrypted_subtitle else R.string.room_profile_encrypted_subtitle + var encryptionMisconfigured = false + val e2eInfoText = if (roomSummary.isEncrypted) { + if (roomSummary.roomEncryptionAlgorithm is RoomEncryptionAlgorithm.SupportedAlgorithm) { + stringProvider.getString( + if (roomSummary.isDirect) R.string.direct_room_profile_encrypted_subtitle + else R.string.room_profile_encrypted_subtitle + ) + } else { + encryptionMisconfigured = true + buildString { + append(stringProvider.getString(R.string.encryption_has_been_misconfigured)) + append(" ") + apply { + if (!data.canUpdateRoomState) { + append(stringProvider.getString(R.string.contact_admin_to_restore_encryption)) + } + } + } + } } else { - if (roomSummary.isDirect) R.string.direct_room_profile_not_encrypted_subtitle else R.string.room_profile_not_encrypted_subtitle + stringProvider.getString( + if (roomSummary.isDirect) R.string.direct_room_profile_not_encrypted_subtitle + else R.string.room_profile_not_encrypted_subtitle + ) } genericFooterItem { id("e2e info") centered(false) - text(host.stringProvider.getString(learnMoreSubtitle)) + text( + span { + apply { + if (encryptionMisconfigured) { + host.drawableProvider.getDrawable(R.drawable.ic_warning_badge)?.let { + image(it, "baseline") + } + +" " + } + } + +e2eInfoText + }.toEpoxyCharSequence() + ) + } + + if (encryptionMisconfigured && data.canUpdateRoomState) { + genericPositiveButtonItem { + id("restore_encryption") + text(host.stringProvider.getString(R.string.room_profile_section_restore_security)) + iconRes(R.drawable.ic_shield_black_no_border) + buttonClickAction { + host.callback?.restoreEncryptionState() + } + } } buildEncryptionAction(data.actionPermissions, roomSummary) 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 ee4d0601c6..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, @@ -121,6 +124,7 @@ class RoomProfileFragment @Inject constructor( is RoomProfileViewEvents.Failure -> showFailure(it.throwable) is RoomProfileViewEvents.ShareRoomProfile -> onShareRoomProfile(it.permalink) is RoomProfileViewEvents.OnShortcutReady -> addShortcut(it) + RoomProfileViewEvents.DismissLoading -> dismissLoadingDialog() }.exhaustive } roomListQuickActionsSharedActionViewModel @@ -299,6 +303,10 @@ class RoomProfileFragment @Inject constructor( roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomPermissionsSettings) } + override fun restoreEncryptionState() { + roomProfileViewModel.handle(RoomProfileAction.RestoreEncryptionState) + } + override fun onRoomIdClicked() { copyToClipboard(requireContext(), roomProfileArgs.roomId) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewEvents.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewEvents.kt index 237df0bed5..181115091c 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewEvents.kt @@ -24,6 +24,7 @@ import im.vector.app.core.platform.VectorViewEvents */ sealed class RoomProfileViewEvents : VectorViewEvents { data class Loading(val message: CharSequence? = null) : RoomProfileViewEvents() + object DismissLoading : RoomProfileViewEvents() data class Failure(val throwable: Throwable) : RoomProfileViewEvents() data class ShareRoomProfile(val permalink: String) : RoomProfileViewEvents() diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt index 472ddfc6b9..363cb1ea31 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt @@ -29,7 +29,10 @@ import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.features.home.ShortcutCreator import im.vector.app.features.powerlevel.PowerLevelsFlowFactory +import im.vector.app.features.session.coroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session @@ -44,6 +47,7 @@ import org.matrix.android.sdk.flow.FlowRoom import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.flow.mapOptional import org.matrix.android.sdk.flow.unwrap +import timber.log.Timber class RoomProfileViewModel @AssistedInject constructor( @Assisted private val initialState: RoomProfileViewState, @@ -67,6 +71,19 @@ class RoomProfileViewModel @AssistedInject constructor( observeRoomCreateContent(flowRoom) observeBannedRoomMembers(flowRoom) observePermissions() + observePowerLevels() + } + + private fun observePowerLevels() { + val powerLevelsContentLive = PowerLevelsFlowFactory(room).createFlow() + powerLevelsContentLive + .onEach { + val powerLevelsHelper = PowerLevelsHelper(it) + val canUpdateRoomState = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_ENCRYPTION) + setState { + copy(canUpdateRoomState = canUpdateRoomState) + } + }.launchIn(viewModelScope) } private fun observeRoomCreateContent(flowRoom: FlowRoom) { @@ -119,6 +136,7 @@ class RoomProfileViewModel @AssistedInject constructor( is RoomProfileAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action) is RoomProfileAction.ShareRoomProfile -> handleShareRoomProfile() RoomProfileAction.CreateShortcut -> handleCreateShortcut() + RoomProfileAction.RestoreEncryptionState -> restoreEncryptionState() }.exhaustive } @@ -182,4 +200,18 @@ class RoomProfileViewModel @AssistedInject constructor( _viewEvents.post(RoomProfileViewEvents.ShareRoomProfile(permalink)) } } + + private fun restoreEncryptionState() { + _viewEvents.post(RoomProfileViewEvents.Loading()) + session.coroutineScope.launch { + try { + room.enableEncryption(force = true) + } catch (failure: Throwable) { + Timber.e(failure, "Failed to restore encryption state in room ${room.roomId}") + _viewEvents.post(RoomProfileViewEvents.Failure(failure)) + } finally { + _viewEvents.post(RoomProfileViewEvents.DismissLoading) + } + } + } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt index 14b415c53a..87db15ea3b 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt @@ -34,7 +34,8 @@ data class RoomProfileViewState( val isUsingUnstableRoomVersion: Boolean = false, val recommendedRoomVersion: String? = null, val canUpgradeRoom: Boolean = false, - val isTombstoned: Boolean = false + val isTombstoned: Boolean = false, + val canUpdateRoomState: Boolean = false ) : MavericksState { constructor(args: RoomProfileArgs) : this(roomId = args.roomId) 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 f95d1a8c24..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 @@ -26,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 @@ -53,7 +54,7 @@ class RoomBannedMemberListController @Inject constructor( genericFooterItem { id("footer") - text(quantityString) + text(quantityString.toEpoxyCharSequence()) } } else { buildProfileSection(quantityString) 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/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 7adfc594b7..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 @@ -25,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 @@ -49,7 +50,7 @@ class RoomJoinRuleAdvancedController @Inject constructor( genericFooterItem { id("header") - text(host.stringProvider.getString(R.string.room_settings_room_access_title)) + text(host.stringProvider.getString(R.string.room_settings_room_access_title).toEpoxyCharSequence()) centered(false) style(ItemStyle.TITLE) textColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) @@ -57,7 +58,7 @@ class RoomJoinRuleAdvancedController @Inject constructor( genericFooterItem { id("desc") - text(host.stringProvider.getString(R.string.decide_who_can_find_and_join)) + text(host.stringProvider.getString(R.string.decide_who_can_find_and_join).toEpoxyCharSequence()) centered(false) } 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 86bfd38a46..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 @@ -28,6 +28,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.spaces.manage.roomSelectionItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.util.MatrixItem import javax.inject.Inject @@ -76,7 +77,7 @@ class ChooseRestrictedController @Inject constructor( // when no filters genericFooterItem { id("h1") - text(host.stringProvider.getString(R.string.space_you_know_that_contains_this_room)) + text(host.stringProvider.getString(R.string.space_you_know_that_contains_this_room).toEpoxyCharSequence()) centered(false) } @@ -93,7 +94,7 @@ class ChooseRestrictedController @Inject constructor( if (data.unknownRestricted.isNotEmpty()) { genericFooterItem { id("others") - text(host.stringProvider.getString(R.string.other_spaces_or_rooms_you_might_not_know)) + text(host.stringProvider.getString(R.string.other_spaces_or_rooms_you_might_not_know).toEpoxyCharSequence()) centered(false) } 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/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index d1f96fe76c..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 @@ -199,8 +199,6 @@ 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 @@ -997,10 +995,6 @@ 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 } 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/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 { genericFooterItem { id("fail") - text(data.accountData.error.localizedMessage) + text(data.accountData.error.localizedMessage?.toEpoxyCharSequence()) } } is Success -> { @@ -62,7 +62,7 @@ class AccountDataEpoxyController @Inject constructor( if (dataList.isEmpty()) { genericFooterItem { id("noResults") - text(host.stringProvider.getString(R.string.no_result_placeholder)) + text(host.stringProvider.getString(R.string.no_result_placeholder).toEpoxyCharSequence()) } } else { dataList.forEach { accountData -> diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt index 08d0d60909..c1b05cca42 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt @@ -21,10 +21,10 @@ import com.airbnb.epoxy.paging.PagedListEpoxyController 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.ui.list.GenericItem_ import im.vector.app.core.utils.createUIHandler +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.span import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt index 1e13fa37bb..4c8bd65c0e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt @@ -20,9 +20,9 @@ import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.paging.PagedListEpoxyController 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.ui.list.GenericItem_ import im.vector.app.core.utils.createUIHandler +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.span import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestPagedController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestPagedController.kt index 5d89bf9b1c..0a52c1a7dd 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestPagedController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestPagedController.kt @@ -18,9 +18,9 @@ package im.vector.app.features.settings.devtools import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.paging.PagedListEpoxyController -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.ui.list.GenericItem_ import im.vector.app.core.utils.createUIHandler +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.span import org.matrix.android.sdk.internal.crypto.OutgoingRoomKeyRequest import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/settings/homeserver/HomeserverSettingsController.kt b/vector/src/main/java/im/vector/app/features/settings/homeserver/HomeserverSettingsController.kt index d2ec53523c..f690a25ab8 100644 --- a/vector/src/main/java/im/vector/app/features/settings/homeserver/HomeserverSettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/homeserver/HomeserverSettingsController.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.error.ErrorFormatter @@ -32,6 +31,7 @@ import im.vector.app.features.discovery.settingsCenteredImageItem import im.vector.app.features.discovery.settingsInfoItem import im.vector.app.features.discovery.settingsSectionTitleItem import im.vector.app.features.settings.VectorPreferences +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.federation.FederationVersion import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.api.session.homeserver.RoomVersionStatus diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsDefaultNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsDefaultNotificationPreferenceFragment.kt index 840e8ccde0..b6f2098209 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsDefaultNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsDefaultNotificationPreferenceFragment.kt @@ -16,8 +16,10 @@ package im.vector.app.features.settings.notifications +import android.os.Bundle import im.vector.app.R import im.vector.app.core.preference.VectorPreferenceCategory +import im.vector.app.features.analytics.plan.Screen import org.matrix.android.sdk.api.pushrules.RuleIds class VectorSettingsDefaultNotificationPreferenceFragment : @@ -34,6 +36,11 @@ class VectorSettingsDefaultNotificationPreferenceFragment : "SETTINGS_PUSH_RULE_MESSAGES_IN_E2E_GROUP_CHAT_PREFERENCE_KEY" to RuleIds.RULE_ID_ENCRYPTED ) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + analyticsScreenName = Screen.ScreenName.SettingsDefaultNotifications + } + override fun bindPref() { super.bindPref() val category = findPreference("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 6cb19b13c5..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 @@ -20,6 +20,7 @@ import com.airbnb.epoxy.TypedEpoxyController import im.vector.app.R 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( @@ -34,7 +35,7 @@ class PushGateWayController @Inject constructor( if (pushers.isEmpty()) { genericFooterItem { id("footer") - text(host.stringProvider.getString(R.string.settings_push_gateway_no_pushers)) + text(host.stringProvider.getString(R.string.settings_push_gateway_no_pushers).toEpoxyCharSequence()) } } else { pushers.forEach { @@ -50,7 +51,7 @@ class PushGateWayController @Inject constructor( } ?: run { genericFooterItem { id("loading") - text(host.stringProvider.getString(R.string.loading)) + text(host.stringProvider.getString(R.string.loading).toEpoxyCharSequence()) } } } 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 c0119ed3be..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 @@ -20,6 +20,7 @@ import com.airbnb.epoxy.TypedEpoxyController import im.vector.app.R 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( @@ -38,7 +39,7 @@ class PushRulesController @Inject constructor( } ?: run { genericFooterItem { id("footer") - text(host.stringProvider.getString(R.string.settings_push_rules_no_rules)) + text(host.stringProvider.getString(R.string.settings_push_rules_no_rules).toEpoxyCharSequence()) } } } 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 cdc40185aa..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 @@ -37,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 @@ -86,7 +87,7 @@ class ThreePidsSettingsController @Inject constructor( is Fail -> { genericFooterItem { id("fail") - text(data.threePids.error.localizedMessage) + text(data.threePids.error.localizedMessage?.toEpoxyCharSequence()) } } is Success -> { 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 5ffc2a0fae..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 @@ -29,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 @@ -66,7 +67,7 @@ class SpaceSummaryController @Inject constructor( if (!nonNullViewState.legacyGroups.isNullOrEmpty()) { genericFooterItem { id("legacy_space") - text(" ") + text(" ".toEpoxyCharSequence()) } genericHeaderItem { 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 7e1902aac0..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( @@ -43,12 +43,12 @@ class SpaceAdd3pidEpoxyController @Inject constructor( genericFooterItem { id("info_help_header") style(ItemStyle.TITLE) - text(host.stringProvider.getString(R.string.create_spaces_invite_public_header)) + text(host.stringProvider.getString(R.string.create_spaces_invite_public_header).toEpoxyCharSequence()) textColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) } genericFooterItem { id("info_help_desc") - text(host.stringProvider.getString(R.string.create_spaces_invite_public_header_desc, data.name ?: "")) + text(host.stringProvider.getString(R.string.create_spaces_invite_public_header_desc, data.name ?: "").toEpoxyCharSequence()) textColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary)) } 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 15e983423f..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 @@ -24,6 +24,7 @@ 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( @@ -45,7 +46,7 @@ class SpaceDefaultRoomEpoxyController @Inject constructor( host.stringProvider.getString(R.string.create_spaces_room_public_header, data.name) } else { host.stringProvider.getString(R.string.create_spaces_room_private_header) - } + }.toEpoxyCharSequence() ) textColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) } @@ -59,7 +60,7 @@ class SpaceDefaultRoomEpoxyController @Inject constructor( } else { R.string.create_spaces_room_private_header_desc } - ) + ).toEpoxyCharSequence() ) textColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary)) } 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 14b0db2cd1..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 @@ -27,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 @@ -61,7 +62,7 @@ class SpaceDetailEpoxyController @Inject constructor( host.stringProvider.getString(R.string.create_spaces_details_public_header) } else { host.stringProvider.getString(R.string.create_spaces_details_private_header) - } + }.toEpoxyCharSequence() ) } 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 f9dfec8f40..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 @@ -27,6 +27,7 @@ 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 @@ -74,7 +75,7 @@ class SpaceManageRoomsController @Inject constructor( if (filteredResult.isEmpty()) { genericFooterItem { id("empty_result") - text(host.stringProvider.getString(R.string.no_result_placeholder)) + text(host.stringProvider.getString(R.string.no_result_placeholder).toEpoxyCharSequence()) } } else { filteredResult.forEach { childInfo -> 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..43657b6eb8 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 = "${state.selectedRooms.size} selected" } // views.addRoomToSpaceToolbar.isVisible = false // views.addRoomToSpaceToolbar.startActionMode(this) 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/ic_communities.xml b/vector/src/main/res/drawable/ic_communities.xml new file mode 100644 index 0000000000..f550de8106 --- /dev/null +++ b/vector/src/main/res/drawable/ic_communities.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/vector/src/main/res/drawable/ic_friends_and_family.xml b/vector/src/main/res/drawable/ic_friends_and_family.xml new file mode 100644 index 0000000000..d7ac86f240 --- /dev/null +++ b/vector/src/main/res/drawable/ic_friends_and_family.xml @@ -0,0 +1,16 @@ + + + + + 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_teams.xml b/vector/src/main/res/drawable/ic_teams.xml new file mode 100644 index 0000000000..8745cfd2d4 --- /dev/null +++ b/vector/src/main/res/drawable/ic_teams.xml @@ -0,0 +1,26 @@ + + + + + + + + + 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"> - + + + + + + + + + + + + + + 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" /> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + +