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/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 e93d1bb089..4d2394db1f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,69 @@ +Changes in Element 1.3.16 (2022-01-25) +====================================== + +Features ✨ +---------- + - Static location sharing and rendering ([#2210](https://github.com/vector-im/element-android/issues/2210)) + - Enables the FTUE splash carousel ([#4584](https://github.com/vector-im/element-android/issues/4584)) + - Allow editing polls ([#5036](https://github.com/vector-im/element-android/issues/5036)) + +Bugfixes 🐛 +---------- + - Fixing missing notifications in FDroid variants using `optimised for battery` background sync mode ([#5003](https://github.com/vector-im/element-android/issues/5003)) + - Fix for stuck local event messages at the bottom of the screen ([#516](https://github.com/vector-im/element-android/issues/516)) + - Notification does not take me to the room when another space was last viewed ([#3839](https://github.com/vector-im/element-android/issues/3839)) + - Explore Rooms overflow menu - content update include "Create room" ([#3932](https://github.com/vector-im/element-android/issues/3932)) + - Fix sync timeout after returning from background ([#4669](https://github.com/vector-im/element-android/issues/4669)) + - Fix a wrong network error issue in the Legals screen ([#4935](https://github.com/vector-im/element-android/issues/4935)) + - Prevent Alerts to be displayed in the automatically displayed analytics opt-in screen ([#4948](https://github.com/vector-im/element-android/issues/4948)) + - EmojiPopupDismissListener not being triggered after dismissing the EmojiPopup ([#4991](https://github.com/vector-im/element-android/issues/4991)) + - Fix an error in string resource ([#4997](https://github.com/vector-im/element-android/issues/4997)) + - Big messages taking inappropriately long to evaluate .m.rule.roomnotif push rules ([#5008](https://github.com/vector-im/element-android/issues/5008)) + - Improve auto rageshake lab feature ([#5021](https://github.com/vector-im/element-android/issues/5021)) + +In development 🚧 +---------------- + - Updates the onboarding carousel images, copy and improves the handling of different device sizes ([#4880](https://github.com/vector-im/element-android/issues/4880)) + - Disabling onboarding automatic carousel transitions on user interaction ([#4914](https://github.com/vector-im/element-android/issues/4914)) + - Locking phones to portrait during the FTUE onboarding ([#4918](https://github.com/vector-im/element-android/issues/4918)) + - Adds a messaging use case screen to the FTUE onboarding ([#4927](https://github.com/vector-im/element-android/issues/4927)) + - Updating the FTUE use case icons ([#5025](https://github.com/vector-im/element-android/issues/5025)) + - Support undisclosed polls ([#5037](https://github.com/vector-im/element-android/issues/5037)) + +Other changes +------------- + - Enabling native support for window resizing ([#4811](https://github.com/vector-im/element-android/issues/4811)) + - Analytics: send more Events ([#4734](https://github.com/vector-im/element-android/issues/4734)) + - Fix integration tests and add a comment with results (still not perfect due to github actions resource limitations) ([#4842](https://github.com/vector-im/element-android/issues/4842)) + - "/kick" command is replaced with "/remove". Also replaced all occurrences in string resources ([#4865](https://github.com/vector-im/element-android/issues/4865)) + - Toolbar management rework. Toolbar title's and subtitle's text appearance now controlled by theme without local overrides. Helper class introduced to + help with toolbar configuration. Toolbar title, subtitle and navigation button widgets are removed where it is possible and replaced with built-in + toolbar widgets. ([#4884](https://github.com/vector-im/element-android/issues/4884)) + - Add signing config for the release buildType. No secret added ([#4926](https://github.com/vector-im/element-android/issues/4926)) + - Remove unused module matrix-sdk-android-rx and do some cleanup ([#4942](https://github.com/vector-im/element-android/issues/4942)) + - Sync issue automation with element-web ([#4949](https://github.com/vector-im/element-android/issues/4949)) + - Improves local echo blinking when non room events received ([#4960](https://github.com/vector-im/element-android/issues/4960)) + - Including onboarding server options in the all screen sanity test suite ([#4975](https://github.com/vector-im/element-android/issues/4975)) + - Exclude dependabot upgrade for @github-script@v3 ([#4988](https://github.com/vector-im/element-android/issues/4988)) + - Small iteration on command parser and unit test it. ([#4998](https://github.com/vector-im/element-android/issues/4998)) + +SDK API changes ⚠️ +------------------ + - `StateService.sendStateEvent()` now takes a non-nullable String for the parameter `stateKey`. If null was used, just now use an empty string. ([#4895](https://github.com/vector-im/element-android/issues/4895)) + - 429 are not automatically retried anymore in case of too long retry delay ([#4995](https://github.com/vector-im/element-android/issues/4995)) + + +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) ======================================= 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/changelog.d/3932.bugfix b/changelog.d/3932.bugfix deleted file mode 100644 index 76e90f2bfd..0000000000 --- a/changelog.d/3932.bugfix +++ /dev/null @@ -1 +0,0 @@ -Explore Rooms overflow menu - content update include "Create room" \ No newline at end of file diff --git a/changelog.d/4669.bugfix b/changelog.d/4669.bugfix deleted file mode 100644 index 8e38becdd9..0000000000 --- a/changelog.d/4669.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix sync timeout after returning from background diff --git a/changelog.d/4811.feature b/changelog.d/4811.feature deleted file mode 100644 index e113078f21..0000000000 --- a/changelog.d/4811.feature +++ /dev/null @@ -1 +0,0 @@ -Enabling native support for window resizing \ No newline at end of file diff --git a/changelog.d/4842.misc b/changelog.d/4842.misc deleted file mode 100644 index ebeb54084e..0000000000 --- a/changelog.d/4842.misc +++ /dev/null @@ -1 +0,0 @@ -Fix integration tests and add a comment with results (still not perfect due to github actions resource limitations) diff --git a/changelog.d/4865.misc b/changelog.d/4865.misc deleted file mode 100644 index 291c4a099f..0000000000 --- a/changelog.d/4865.misc +++ /dev/null @@ -1 +0,0 @@ -"/kick" command is replaced with "/remove". Also replaced all occurrences in string resources diff --git a/changelog.d/4880.wip b/changelog.d/4880.wip deleted file mode 100644 index e74c56cdc4..0000000000 --- a/changelog.d/4880.wip +++ /dev/null @@ -1 +0,0 @@ -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/4895.removal b/changelog.d/4895.removal deleted file mode 100644 index 8b3e3adba4..0000000000 --- a/changelog.d/4895.removal +++ /dev/null @@ -1 +0,0 @@ -`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 deleted file mode 100644 index 9d471d40ac..0000000000 --- a/changelog.d/4914.wip +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index ed5a273e16..0000000000 --- a/changelog.d/4918.wip +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index c84ff0d9eb..0000000000 --- a/changelog.d/4926.misc +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index 86e9abad26..0000000000 --- a/changelog.d/4927.wip +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index 18967ed4a5..0000000000 --- a/changelog.d/4935.bugfix +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index 29fb8cee55..0000000000 --- a/changelog.d/4942.misc +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index da4b0a2500..0000000000 --- a/changelog.d/4948.bugfix +++ /dev/null @@ -1 +0,0 @@ -Prevent Alerts to be displayed in the automatically displayed analytics opt-in screen \ No newline at end of file diff --git a/changelog.d/4960.misc b/changelog.d/4960.misc deleted file mode 100644 index 0829d74b61..0000000000 --- a/changelog.d/4960.misc +++ /dev/null @@ -1 +0,0 @@ -Improves local echo blinking when non room events received diff --git a/changelog.d/5062.bugfix b/changelog.d/5062.bugfix new file mode 100644 index 0000000000..ec24bfd6c1 --- /dev/null +++ b/changelog.d/5062.bugfix @@ -0,0 +1 @@ +Show the legal mention of mapbox when sharing location \ No newline at end of file diff --git a/changelog.d/5067.bugfix b/changelog.d/5067.bugfix new file mode 100644 index 0000000000..7ad88b608d --- /dev/null +++ b/changelog.d/5067.bugfix @@ -0,0 +1 @@ +Poll cannot end in some unencrypted rooms \ No newline at end of file diff --git a/changelog.d/516.bugfix b/changelog.d/516.bugfix deleted file mode 100644 index 7a5f745c32..0000000000 --- a/changelog.d/516.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix for stuck local event messages at the bottom of the screen diff --git a/dependencies.gradle b/dependencies.gradle index b4e1a7e7fa..77d072e7c7 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -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 : [ diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index 3853919bcb..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', @@ -84,6 +83,7 @@ ext.groups = [ 'com.jakewharton.android.repackaged', 'com.jakewharton.timber', 'com.linkedin.dexmaker', + 'com.mapbox.mapboxsdk', 'com.nulab-inc', 'com.otaliastudios.opengl', 'com.parse.bolts', @@ -154,11 +154,13 @@ ext.groups = [ 'org.jetbrains.intellij.deps', 'org.jetbrains.kotlin', 'org.jetbrains.kotlinx', + 'org.json', 'org.jsoup', 'org.junit', 'org.junit.jupiter', 'org.junit.platform', 'org.jvnet.staxex', + 'org.maplibre.gl', 'org.matrix.android', 'org.mockito', 'org.mongodb', diff --git a/fastlane/README.md b/fastlane/README.md index dc33f422d6..7fea7afdd5 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -1,49 +1,64 @@ fastlane documentation -================ +---- + # Installation Make sure you have the latest version of the Xcode command line tools installed: -``` +```sh xcode-select --install ``` -Install _fastlane_ using -``` -[sudo] gem install fastlane -NV -``` -or alternatively using `brew install fastlane` +For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) # Available Actions + ## Android + ### android test + +```sh +[bundle exec] fastlane android test ``` -fastlane android test -``` + Runs all the tests + ### android beta + +```sh +[bundle exec] fastlane android beta ``` -fastlane android beta -``` + Submit a new Beta Build to Crashlytics Beta + ### android deploy + +```sh +[bundle exec] fastlane android deploy ``` -fastlane android deploy -``` + Deploy a new version to the Google Play + ### android deployMeta + +```sh +[bundle exec] fastlane android deployMeta ``` -fastlane android deployMeta -``` + Deploy Google Play metadata + ### android getVersionCode + +```sh +[bundle exec] fastlane android getVersionCode ``` -fastlane android getVersionCode -``` + Get version code ---- This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. -More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). -The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). + +More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). + +The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). diff --git a/fastlane/metadata/android/de-DE/changelogs/40103100.txt b/fastlane/metadata/android/de-DE/changelogs/40103100.txt new file mode 100644 index 0000000000..8daa7b51a5 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40103100.txt @@ -0,0 +1,2 @@ +Änderungen: Die Websitevorschau hat ein neues Design erhalten. Außerdem gibt es in den experimentellen Einstellungen Abstimmungen. +Alle Änderungen: https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/de-DE/changelogs/40103110.txt b/fastlane/metadata/android/de-DE/changelogs/40103110.txt new file mode 100644 index 0000000000..a3d6aad6ca --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40103110.txt @@ -0,0 +1,2 @@ +Hauptänderungen: Bugfixes! +Alle Änderungen: https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/de-DE/changelogs/40103120.txt b/fastlane/metadata/android/de-DE/changelogs/40103120.txt new file mode 100644 index 0000000000..6930764750 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40103120.txt @@ -0,0 +1,2 @@ +Hauptänderungen: Bugfixes! +Alle Änderungen: https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/en-US/changelogs/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/en-US/changelogs/40103160.txt b/fastlane/metadata/android/en-US/changelogs/40103160.txt new file mode 100644 index 0000000000..900abee127 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40103160.txt @@ -0,0 +1,2 @@ +Main changes in this version: send your location to any room. Edit poll. +Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.16 \ No newline at end of file diff --git a/fastlane/metadata/android/et/title.txt b/fastlane/metadata/android/et/title.txt index b0bf39ba23..907f907f99 100644 --- a/fastlane/metadata/android/et/title.txt +++ b/fastlane/metadata/android/et/title.txt @@ -1 +1 @@ -Element - turvaline sõnumiklient +Element diff --git a/fastlane/metadata/android/fr-FR/changelogs/40103100.txt b/fastlane/metadata/android/fr-FR/changelogs/40103100.txt new file mode 100644 index 0000000000..b6484603d4 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40103100.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : prise en charge des sondages (dans les labs). Nouvel affichage des prévisualisations d’URL +Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/fr-FR/changelogs/40103110.txt b/fastlane/metadata/android/fr-FR/changelogs/40103110.txt new file mode 100644 index 0000000000..aef05c238d --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40103110.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : corrections de bugs ! +Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/fr-FR/changelogs/40103120.txt b/fastlane/metadata/android/fr-FR/changelogs/40103120.txt new file mode 100644 index 0000000000..18cba17990 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40103120.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : corrections de bugs ! +Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/hu-HU/title.txt b/fastlane/metadata/android/hu-HU/title.txt index c463dea393..907f907f99 100644 --- a/fastlane/metadata/android/hu-HU/title.txt +++ b/fastlane/metadata/android/hu-HU/title.txt @@ -1 +1 @@ -Element - Biztonságos üzenetküldő +Element diff --git a/fastlane/metadata/android/nl/changelogs/40103070.txt b/fastlane/metadata/android/nl-NL/changelogs/40103070.txt similarity index 100% rename from fastlane/metadata/android/nl/changelogs/40103070.txt rename to fastlane/metadata/android/nl-NL/changelogs/40103070.txt diff --git a/fastlane/metadata/android/nl/changelogs/40103080.txt b/fastlane/metadata/android/nl-NL/changelogs/40103080.txt similarity index 100% rename from fastlane/metadata/android/nl/changelogs/40103080.txt rename to fastlane/metadata/android/nl-NL/changelogs/40103080.txt diff --git a/fastlane/metadata/android/nl/changelogs/40103090.txt b/fastlane/metadata/android/nl-NL/changelogs/40103090.txt similarity index 100% rename from fastlane/metadata/android/nl/changelogs/40103090.txt rename to fastlane/metadata/android/nl-NL/changelogs/40103090.txt diff --git a/fastlane/metadata/android/nl/changelogs/40103100.txt b/fastlane/metadata/android/nl-NL/changelogs/40103100.txt similarity index 100% rename from fastlane/metadata/android/nl/changelogs/40103100.txt rename to fastlane/metadata/android/nl-NL/changelogs/40103100.txt diff --git a/fastlane/metadata/android/nl/changelogs/40103110.txt b/fastlane/metadata/android/nl-NL/changelogs/40103110.txt similarity index 100% rename from fastlane/metadata/android/nl/changelogs/40103110.txt rename to fastlane/metadata/android/nl-NL/changelogs/40103110.txt diff --git a/fastlane/metadata/android/nl/changelogs/40103120.txt b/fastlane/metadata/android/nl-NL/changelogs/40103120.txt similarity index 100% rename from fastlane/metadata/android/nl/changelogs/40103120.txt rename to fastlane/metadata/android/nl-NL/changelogs/40103120.txt diff --git a/fastlane/metadata/android/nl/short_description.txt b/fastlane/metadata/android/nl-NL/short_description.txt similarity index 100% rename from fastlane/metadata/android/nl/short_description.txt rename to fastlane/metadata/android/nl-NL/short_description.txt diff --git a/fastlane/metadata/android/nl/title.txt b/fastlane/metadata/android/nl-NL/title.txt similarity index 100% rename from fastlane/metadata/android/nl/title.txt rename to fastlane/metadata/android/nl-NL/title.txt diff --git a/fastlane/metadata/android/pl/changelogs/40103070.txt b/fastlane/metadata/android/pl-PL/changelogs/40103070.txt similarity index 100% rename from fastlane/metadata/android/pl/changelogs/40103070.txt rename to fastlane/metadata/android/pl-PL/changelogs/40103070.txt diff --git a/fastlane/metadata/android/pl/full_description.txt b/fastlane/metadata/android/pl-PL/full_description.txt similarity index 100% rename from fastlane/metadata/android/pl/full_description.txt rename to fastlane/metadata/android/pl-PL/full_description.txt diff --git a/fastlane/metadata/android/pl/short_description.txt b/fastlane/metadata/android/pl-PL/short_description.txt similarity index 100% rename from fastlane/metadata/android/pl/short_description.txt rename to fastlane/metadata/android/pl-PL/short_description.txt diff --git a/fastlane/metadata/android/pl-PL/title.txt b/fastlane/metadata/android/pl-PL/title.txt new file mode 100644 index 0000000000..907f907f99 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/title.txt @@ -0,0 +1 @@ +Element diff --git a/fastlane/metadata/android/pl/title.txt b/fastlane/metadata/android/pl/title.txt deleted file mode 100644 index 3f4f1ba418..0000000000 --- a/fastlane/metadata/android/pl/title.txt +++ /dev/null @@ -1 +0,0 @@ -Element - Bezpieczny Komunikator diff --git a/fastlane/metadata/android/ru-RU/title.txt b/fastlane/metadata/android/ru-RU/title.txt index b7b25082a4..907f907f99 100644 --- a/fastlane/metadata/android/ru-RU/title.txt +++ b/fastlane/metadata/android/ru-RU/title.txt @@ -1 +1 @@ -Element - Безопасный мессенджер +Element diff --git a/fastlane/metadata/android/sq/changelogs/40103100.txt b/fastlane/metadata/android/sq/changelogs/40103100.txt new file mode 100644 index 0000000000..c6916fa0ab --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40103100.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Shtim mbulimi për anketime (në zhvillim). Skemë e re grafike për paraprje URL-sh. +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/sq/changelogs/40103110.txt b/fastlane/metadata/android/sq/changelogs/40103110.txt new file mode 100644 index 0000000000..f66779b5ef --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40103110.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje të metash! +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/sq/changelogs/40103120.txt b/fastlane/metadata/android/sq/changelogs/40103120.txt new file mode 100644 index 0000000000..279c523a82 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40103120.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Ndreqje të metash! +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/sq/title.txt b/fastlane/metadata/android/sq/title.txt index 097f9c48ea..907f907f99 100644 --- a/fastlane/metadata/android/sq/title.txt +++ b/fastlane/metadata/android/sq/title.txt @@ -1 +1 @@ -Element - Shkëmbyes i Sigurt Mesazhesh +Element diff --git a/library/core-utils/src/main/java/im/vector/lib/core/utils/compat/MutableCollectionCompat.kt b/library/core-utils/src/main/java/im/vector/lib/core/utils/compat/MutableCollectionCompat.kt new file mode 100644 index 0000000000..332ed27ca3 --- /dev/null +++ b/library/core-utils/src/main/java/im/vector/lib/core/utils/compat/MutableCollectionCompat.kt @@ -0,0 +1,27 @@ +/* + * 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.lib.core.utils.compat + +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/library/jsonviewer/.gitignore b/library/jsonviewer/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/library/jsonviewer/.gitignore @@ -0,0 +1 @@ +/build 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/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/Utils.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/Utils.kt new file mode 100644 index 0000000000..efb2bfd855 --- /dev/null +++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/Utils.kt @@ -0,0 +1,30 @@ +/* + * 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.util.TypedValue + +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/res/drawable/bg_carousel_page_dark.xml b/library/ui-styles/src/main/res/drawable/bg_carousel_page_dark.xml index f229c51d1b..2542ff2b1d 100644 --- 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 @@ -1,7 +1,4 @@ - + \ 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/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index efef826a87..86ce67ba8d 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.16\"" + buildConfigField "String", "SDK_VERSION", "\"1.3.18\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" resValue "string", "git_sdk_revision", "\"${gitRevision()}\"" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt index 13a26c89c1..aabe6e0d06 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt @@ -32,13 +32,18 @@ fun Throwable.is401() = fun Throwable.isTokenError() = this is Failure.ServerError && (error.code == MatrixError.M_UNKNOWN_TOKEN || - error.code == MatrixError.M_MISSING_TOKEN || - error.code == MatrixError.ORG_MATRIX_EXPIRED_ACCOUNT) + error.code == MatrixError.M_MISSING_TOKEN || + error.code == MatrixError.ORG_MATRIX_EXPIRED_ACCOUNT) + +fun Throwable.isLimitExceededError() = + this is Failure.ServerError && + httpCode == 429 && + error.code == MatrixError.M_LIMIT_EXCEEDED fun Throwable.shouldBeRetried(): Boolean { return this is Failure.NetworkConnection || this is IOException || - (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED) + this.isLimitExceededError() } /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/EventMatchCondition.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/EventMatchCondition.kt index eec5b0a402..65a13b4fec 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/EventMatchCondition.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/EventMatchCondition.kt @@ -56,7 +56,13 @@ class EventMatchCondition( if (wordsOnly) { value.caseInsensitiveFind(pattern) } else { - val modPattern = if (pattern.hasSpecialGlobChar()) pattern.simpleGlobToRegExp() else "*$pattern*".simpleGlobToRegExp() + val modPattern = if (pattern.hasSpecialGlobChar()) { + // Regex.containsMatchIn() is way faster without leading and trailing + // stars, that don't make any difference for the evaluation result + pattern.removePrefix("*").removeSuffix("*").simpleGlobToRegExp() + } else { + pattern.simpleGlobToRegExp() + } val regex = Regex(modPattern, RegexOption.DOT_MATCHES_ALL) regex.containsMatchIn(value) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt index 36ab007314..be924e2063 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt @@ -54,6 +54,7 @@ import org.matrix.android.sdk.api.session.securestorage.SecureStorageService import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService import org.matrix.android.sdk.api.session.signout.SignOutService import org.matrix.android.sdk.api.session.space.SpaceService +import org.matrix.android.sdk.api.session.statistics.StatisticsListener import org.matrix.android.sdk.api.session.sync.FilterService import org.matrix.android.sdk.api.session.sync.SyncState import org.matrix.android.sdk.api.session.sync.model.SyncResponse @@ -287,7 +288,7 @@ interface Session : /** * A global session listener to get notified for some events. */ - interface Listener : SessionLifecycleObserver { + interface Listener : StatisticsListener, SessionLifecycleObserver { /** * Called when the session received new invites to room so the client can react to it once. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAsset.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAsset.kt new file mode 100644 index 0000000000..e8b3cf2488 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAsset.kt @@ -0,0 +1,25 @@ +/* + * 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.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class LocationAsset( + @Json(name = "type") val type: LocationAssetType? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAssetType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAssetType.kt new file mode 100644 index 0000000000..ef40e21c47 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAssetType.kt @@ -0,0 +1,26 @@ +/* + * 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.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = false) +enum class LocationAssetType { + @Json(name = "m.self") + SELF +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt index a76c3c5b64..a1fd3bd2ec 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt @@ -18,29 +18,17 @@ package org.matrix.android.sdk.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo @JsonClass(generateAdapter = true) data class LocationInfo( /** - * The URL to the thumbnail of the file. Only present if the thumbnail is unencrypted. + * Required. RFC5870 formatted geo uri 'geo:latitude,longitude;uncertainty' like 'geo:40.05,29.24;30' representing this location. */ - @Json(name = "thumbnail_url") val thumbnailUrl: String? = null, + @Json(name = "uri") val geoUri: String? = null, /** - * Metadata about the image referred to in thumbnail_url. + * Required. A description of the location e.g. 'Big Ben, London, UK', or some kind + * of content description for accessibility e.g. 'location attachment'. */ - @Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null, - - /** - * Information on the encrypted thumbnail file, as specified in End-to-end encryption. Only present if the thumbnail is encrypted. - */ - @Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null + @Json(name = "description") val description: String? = null ) - -/** - * Get the url of the encrypted thumbnail or of the thumbnail - */ -fun LocationInfo.getThumbnailUrl(): String? { - return thumbnailFile?.url ?: thumbnailUrl -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt index 6881c09924..bf51e7177b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt @@ -26,7 +26,7 @@ data class MessageLocationContent( /** * Required. Must be 'm.location'. */ - @Json(name = MessageContent.MSG_TYPE_JSON_KEY) override val msgType: String, + @Json(name = MessageContent.MSG_TYPE_JSON_KEY) override val msgType: String = MessageType.MSGTYPE_LOCATION, /** * Required. A description of the location e.g. 'Big Ben, London, UK', or some kind @@ -35,15 +35,33 @@ data class MessageLocationContent( @Json(name = "body") override val body: String, /** - * Required. A geo URI representing this location. + * Required. RFC5870 formatted geo uri 'geo:latitude,longitude;uncertainty' like 'geo:40.05,29.24;30' representing this location. */ @Json(name = "geo_uri") val geoUri: String, /** - * + * See https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md */ - @Json(name = "info") val locationInfo: LocationInfo? = null, + @Json(name = "org.matrix.msc3488.location") val locationInfo: LocationInfo? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, - @Json(name = "m.new_content") override val newContent: Content? = null -) : MessageContent + @Json(name = "m.new_content") override val newContent: Content? = null, + + /** + * m.asset defines a generic asset that can be used for location tracking but also in other places like + * inventories, geofencing, checkins/checkouts etc. + * It should contain a mandatory namespaced type key defining what particular asset is being referred to. + * For the purposes of user location tracking m.self should be used in order to avoid duplicating the mxid. + */ + @Json(name = "m.asset") val locationAsset: LocationAsset? = null, + + /** + * Exact time that the data in the event refers to (milliseconds since the UNIX epoch) + */ + @Json(name = "org.matrix.msc3488.ts") val ts: Long? = null, + + @Json(name = "org.matrix.msc1767.text") val text: String? = null +) : MessageContent { + + fun getUri() = locationInfo?.geoUri ?: geoUri +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt index e652514b92..a82c01b159 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt @@ -22,7 +22,7 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class PollCreationInfo( @Json(name = "question") val question: PollQuestion? = null, - @Json(name = "kind") val kind: String? = "org.matrix.msc3381.poll.disclosed", + @Json(name = "kind") val kind: PollType? = PollType.DISCLOSED, @Json(name = "max_selections") val maxSelections: Int = 1, @Json(name = "answers") val answers: List? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollType.kt new file mode 100644 index 0000000000..3a8066b9bc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollType.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = false) +enum class PollType { + /** + * Voters should see results as soon as they have voted. + */ + @Json(name = "org.matrix.msc3381.poll.disclosed") + DISCLOSED, + + /** + * Results should be only revealed when the poll is ended. + */ + @Json(name = "org.matrix.msc3381.poll.undisclosed") + UNDISCLOSED +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt index a9c5ba19a8..9f2850e26a 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 @@ -19,6 +19,7 @@ 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.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 import org.matrix.android.sdk.api.util.Optional @@ -68,6 +69,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 f9a775589c..913dbfd010 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 @@ -95,11 +96,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. @@ -135,6 +137,14 @@ interface SendService { */ fun resendMediaMessage(localEcho: TimelineEvent): Cancelable + /** + * Send a location event to the room + * @param latitude required latitude of the location + * @param longitude required longitude of the location + * @param uncertainty Accuracy of the location in meters + */ + fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?): Cancelable + /** * Remove this failed message from the timeline * @param localEcho the unsent local echo diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt index 8e2b79752b..6f8bae876b 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 @@ -135,7 +135,7 @@ fun TimelineEvent.getEditedEventId(): String? { fun TimelineEvent.getLastMessageContent(): MessageContent? { return when (root.getClearType()) { EventType.STICKER -> root.getClearContent().toModel() - EventType.POLL_START -> root.getClearContent().toModel() + EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/statistics/StatisticEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/statistics/StatisticEvent.kt new file mode 100644 index 0000000000..946792d31e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/statistics/StatisticEvent.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.statistics + +/** + * Statistic Events. You can subscribe to received such events using [Session.Listener] + */ +sealed interface StatisticEvent { + /** + * Initial sync request, response downloading, and treatment (parsing and storage) of response + */ + data class InitialSyncRequest(val requestDurationMs: Int, + val downloadDurationMs: Int, + val treatmentDurationMs: Int, + val nbOfJoinedRooms: Int) : StatisticEvent + + /** + * Incremental sync event + */ + data class SyncTreatment(val durationMs: Int, + val afterPause: Boolean, + val nbOfJoinedRooms: Int) : StatisticEvent +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/statistics/StatisticsListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/statistics/StatisticsListener.kt new file mode 100644 index 0000000000..a2cb7910a7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/statistics/StatisticsListener.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.statistics + +import org.matrix.android.sdk.api.session.Session + +interface StatisticsListener { + fun onStatisticsEvent(session: Session, statisticEvent: StatisticEvent) = Unit +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/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/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index 56704474bc..89d34cba1b 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,14 @@ 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.TimelineEvent +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.helper.findRootThreadEvent @@ -56,6 +60,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 @@ -64,7 +69,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( @@ -80,6 +87,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 ) @@ -209,6 +217,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) @@ -275,6 +291,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 }) { @@ -339,6 +369,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor( val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return val eventTimestamp = event.originServerTs ?: return + val targetPollContent = getPollContent(roomId, targetEventId) ?: return + // ok, this is a poll response var existing = EventAnnotationsSummaryEntity.where(realm, roomId, targetEventId).findFirst() if (existing == null) { @@ -379,6 +411,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) { @@ -432,6 +470,17 @@ internal class EventRelationsAggregationProcessor @Inject constructor( isLocalEcho: Boolean) { val pollEventId = content.relatesTo?.eventId ?: return + val pollOwnerId = getPollEvent(roomId, pollEventId)?.root?.senderId + val isPollOwner = pollOwnerId == event.senderId + + val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) + ?.content?.toModel() + ?.let { PowerLevelsHelper(it) } + if (!isPollOwner && !powerLevelsHelper?.isUserAbleToRedact(event.senderId ?: "").orFalse()) { + Timber.v("## Received poll.end event $pollEventId but user ${event.senderId} doesn't have enough power level in room $roomId") + return + } + var existing = EventAnnotationsSummaryEntity.where(realm, roomId, pollEventId).findFirst() if (existing == null) { Timber.v("## POLL creating new relation summary for $pollEventId") @@ -449,14 +498,6 @@ internal class EventRelationsAggregationProcessor @Inject constructor( return } - val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) - ?.content?.toModel() - ?.let { PowerLevelsHelper(it) } - if (!powerLevelsHelper?.isUserAbleToRedact(event.senderId ?: "").orFalse()) { - Timber.v("## Received poll.end event $pollEventId but user ${event.senderId} doesn't have enough power level in room $roomId") - return - } - val txId = event.unsignedData?.transactionId // is it a remote echo? if (!isLocalEcho && existingPollSummary.sourceLocalEchoEvents.contains(txId)) { @@ -470,6 +511,21 @@ internal class EventRelationsAggregationProcessor @Inject constructor( existingPollSummary.closedTime = event.originServerTs } + private fun getPollEvent(roomId: String, eventId: String): TimelineEvent? { + val session = sessionManager.getSessionComponent(sessionId)?.session() + return session?.getRoom(roomId)?.getTimeLineEvent(eventId) ?: return null.also { + Timber.v("## POLL target poll event $eventId not found in room $roomId") + } + } + + private fun getPollContent(roomId: String, eventId: String): MessagePollContent? { + val pollEvent = getPollEvent(roomId, eventId) ?: return null + + return pollEvent.getLastMessageContent() as? MessagePollContent ?: return null.also { + Timber.v("## POLL target poll event $eventId content is malformed") + } + } + private fun handleInitialAggregatedRelations(realm: Realm, event: Event, roomId: String, 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 ac5e90733b..95e5771757 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 @@ -118,6 +119,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 ea94b93767..4551f390e7 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 23322b081c..8c0ea0ec4c 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 @@ -109,8 +110,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) } } @@ -127,6 +128,12 @@ internal class DefaultSendService @AssistedInject constructor( .let { sendEvent(it) } } + override fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?): Cancelable { + return localEchoEventFactory.createLocationEvent(roomId, latitude, longitude, uncertainty) + .also { createLocalEcho(it) } + .let { sendEvent(it) } + } + override fun redactEvent(event: Event, reason: String?): Cancelable { // TODO manage media/attachements? val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index 17c396b724..0e8223e51d 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 @@ -33,6 +33,9 @@ import org.matrix.android.sdk.api.session.room.model.message.AudioInfo import org.matrix.android.sdk.api.session.room.model.message.AudioWaveformInfo import org.matrix.android.sdk.api.session.room.model.message.FileInfo import org.matrix.android.sdk.api.session.room.model.message.ImageInfo +import org.matrix.android.sdk.api.session.room.model.message.LocationAsset +import org.matrix.android.sdk.api.session.room.model.message.LocationAssetType +import org.matrix.android.sdk.api.session.room.model.message.LocationInfo import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody @@ -40,6 +43,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollConte import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent import org.matrix.android.sdk.api.session.room.model.message.MessageFormat import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent @@ -50,6 +54,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 @@ -63,6 +68,8 @@ import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.content.ThumbnailExtractor import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory import org.matrix.android.sdk.internal.session.room.send.pills.TextPillsUtils +import java.util.UUID +import java.util.concurrent.TimeUnit import javax.inject.Inject /** @@ -126,6 +133,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 { @@ -151,21 +197,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, @@ -196,6 +231,27 @@ internal class LocalEchoEventFactory @Inject constructor( unsignedData = UnsignedData(age = null, transactionId = localId)) } + fun createLocationEvent(roomId: String, + latitude: Double, + longitude: Double, + uncertainty: Double?): Event { + val geoUri = buildGeoUri(latitude, longitude, uncertainty) + val content = MessageLocationContent( + geoUri = geoUri, + body = geoUri, + locationInfo = LocationInfo( + geoUri = geoUri, + description = geoUri + ), + locationAsset = LocationAsset( + type = LocationAssetType.SELF + ), + ts = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()), + text = geoUri + ) + return createMessageEvent(roomId, content) + } + fun createReplaceTextOfReply(roomId: String, eventReplaced: TimelineEvent, originalEvent: TimelineEvent, @@ -587,6 +643,23 @@ internal class LocalEchoEventFactory @Inject constructor( } } + /** + * Returns RFC5870 formatted geo uri 'geo:latitude,longitude;uncertainty' like 'geo:40.05,29.24;30' + * Uncertainty of the location is in meters and not required. + */ + private fun buildGeoUri(latitude: Double, longitude: Double, uncertainty: Double?): String { + return buildString { + append("geo:") + append(latitude) + append(",") + append(longitude) + uncertainty?.let { + append(";") + append(it) + } + } + } + /* * { "content": { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt index 3be01762e7..eb69161614 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt @@ -23,8 +23,8 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.auth.data.SessionParams import org.matrix.android.sdk.api.failure.Failure -import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.getRetryDelay +import org.matrix.android.sdk.api.failure.isLimitExceededError import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.events.model.Event @@ -145,17 +145,17 @@ internal class EventSenderProcessorCoroutine @Inject constructor( task.execute() } catch (exception: Throwable) { when { - exception is IOException || exception is Failure.NetworkConnection -> { + exception is IOException || exception is Failure.NetworkConnection -> { canReachServer.set(false) task.markAsFailedOrRetry(exception, 0) } - (exception is Failure.ServerError && exception.error.code == MatrixError.M_LIMIT_EXCEEDED) -> { + (exception.isLimitExceededError()) -> { task.markAsFailedOrRetry(exception, exception.getRetryDelay(3_000)) } - exception is CancellationException -> { + exception is CancellationException -> { Timber.v("## $task has been cancelled, try next task") } - else -> { + else -> { Timber.v("## un-retryable error for $task, try next task") // this task is in error, check next one? task.onTaskFailed() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorThread.kt index f32890f3fb..1ee3139194 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorThread.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorThread.kt @@ -23,7 +23,7 @@ import org.matrix.android.sdk.api.auth.data.SessionParams import org.matrix.android.sdk.api.auth.data.sessionId import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.failure.Failure -import org.matrix.android.sdk.api.failure.MatrixError +import org.matrix.android.sdk.api.failure.isLimitExceededError import org.matrix.android.sdk.api.failure.isTokenError import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.CryptoService @@ -171,7 +171,7 @@ internal class EventSenderProcessorThread @Inject constructor( break@retryLoop } catch (exception: Throwable) { when { - exception is IOException || exception is Failure.NetworkConnection -> { + exception is IOException || exception is Failure.NetworkConnection -> { canReachServer = false if (task.retryCount.getAndIncrement() >= 3) task.onTaskFailed() while (!canReachServer) { @@ -180,7 +180,7 @@ internal class EventSenderProcessorThread @Inject constructor( waitForNetwork() } } - (exception is Failure.ServerError && exception.error.code == MatrixError.M_LIMIT_EXCEEDED) -> { + (exception.isLimitExceededError()) -> { if (task.retryCount.getAndIncrement() >= 3) task.onTaskFailed() Timber.v("## SendThread retryLoop retryable error for $task reason: ${exception.localizedMessage}") // wait a bit @@ -188,17 +188,17 @@ internal class EventSenderProcessorThread @Inject constructor( sleep(3_000) continue@retryLoop } - exception.isTokenError() -> { + exception.isTokenError() -> { Timber.v("## SendThread retryLoop retryable TOKEN error, interrupt") // we can exit the loop task.onTaskFailed() throw InterruptedException() } - exception is CancellationException -> { + exception is CancellationException -> { Timber.v("## SendThread task has been cancelled") break@retryLoop } - else -> { + else -> { Timber.v("## SendThread retryLoop Un-Retryable error, try next task") // this task is in error, check next one? task.onTaskFailed() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt index 621a08a414..1ee62ad774 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt @@ -16,10 +16,13 @@ package org.matrix.android.sdk.internal.session.sync +import android.os.SystemClock import okhttp3.ResponseBody import org.matrix.android.sdk.api.logger.LoggerTag +import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.initsync.InitSyncStep import org.matrix.android.sdk.api.session.initsync.SyncStatusService +import org.matrix.android.sdk.api.session.statistics.StatisticEvent import org.matrix.android.sdk.api.session.sync.model.LazyRoomSyncEphemeral import org.matrix.android.sdk.api.session.sync.model.SyncResponse import org.matrix.android.sdk.internal.di.SessionFilesDirectory @@ -28,6 +31,8 @@ import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.TimeOutInterceptor import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.toFailure +import org.matrix.android.sdk.internal.session.SessionListeners +import org.matrix.android.sdk.internal.session.dispatchTo import org.matrix.android.sdk.internal.session.filter.FilterRepository import org.matrix.android.sdk.internal.session.homeserver.GetHomeServerCapabilitiesTask import org.matrix.android.sdk.internal.session.initsync.DefaultSyncStatusService @@ -49,7 +54,8 @@ internal interface SyncTask : Task { data class Params( val timeout: Long, - val presence: SyncPresence? + val presence: SyncPresence?, + val afterPause: Boolean ) } @@ -62,6 +68,8 @@ internal class DefaultSyncTask @Inject constructor( private val syncTokenStore: SyncTokenStore, private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask, private val userStore: UserStore, + private val session: Session, + private val sessionListeners: SessionListeners, private val syncTaskSequencer: SyncTaskSequencer, private val globalErrorReceiver: GlobalErrorReceiver, @SessionFilesDirectory @@ -105,6 +113,7 @@ internal class DefaultSyncTask @Inject constructor( val readTimeOut = (params.timeout + TIMEOUT_MARGIN).coerceAtLeast(TimeOutInterceptor.DEFAULT_LONG_TIMEOUT) var syncResponseToReturn: SyncResponse? = null + val syncStatisticsData = SyncStatisticsData(isInitialSync, params.afterPause) if (isInitialSync) { Timber.tag(loggerTag.value).d("INIT_SYNC with filter: ${requestParams["filter"]}") val initSyncStrategy = initialSyncStrategy @@ -112,7 +121,7 @@ internal class DefaultSyncTask @Inject constructor( if (initSyncStrategy is InitialSyncStrategy.Optimized) { roomSyncEphemeralTemporaryStore.reset() workingDir.mkdirs() - val file = downloadInitSyncResponse(requestParams) + val file = downloadInitSyncResponse(requestParams, syncStatisticsData) syncResponseToReturn = reportSubtask(defaultSyncStatusService, InitSyncStep.ImportingAccount, 1, 0.7F) { handleSyncFile(file, initSyncStrategy) } @@ -127,6 +136,9 @@ internal class DefaultSyncTask @Inject constructor( ) } } + // We cannot distinguish request and download in this case. + syncStatisticsData.requestInitSyncTime = SystemClock.elapsedRealtime() + syncStatisticsData.downloadInitSyncTime = syncStatisticsData.requestInitSyncTime logDuration("INIT_SYNC Database insertion", loggerTag) { syncResponseHandler.handleResponse(syncResponse, token, defaultSyncStatusService) } @@ -161,12 +173,15 @@ internal class DefaultSyncTask @Inject constructor( Timber.tag(loggerTag.value).d("Incremental sync done") defaultSyncStatusService.setStatus(SyncStatusService.Status.IncrementalSyncDone) } + syncStatisticsData.treatmentSyncTime = SystemClock.elapsedRealtime() + syncStatisticsData.nbOfRooms = syncResponseToReturn?.rooms?.join?.size ?: 0 + sendStatistics(syncStatisticsData) Timber.tag(loggerTag.value).d("Sync task finished on Thread: ${Thread.currentThread().name}") // Should throw if null as it's a mandatory value. return syncResponseToReturn!! } - private suspend fun downloadInitSyncResponse(requestParams: Map): File { + private suspend fun downloadInitSyncResponse(requestParams: Map, syncStatisticsData: SyncStatisticsData): File { val workingFile = File(workingDir, "initSync.json") val status = initialSyncStatusRepository.getStep() if (workingFile.exists() && status >= InitialSyncStatus.STEP_DOWNLOADED) { @@ -181,7 +196,7 @@ internal class DefaultSyncTask @Inject constructor( getSyncResponse(requestParams, MAX_NUMBER_OF_RETRY_AFTER_TIMEOUT) } } - + syncStatisticsData.requestInitSyncTime = SystemClock.elapsedRealtime() if (syncResponse.isSuccessful) { logDuration("INIT_SYNC Download and save to file", loggerTag) { reportSubtask(defaultSyncStatusService, InitSyncStep.Downloading, 1, 0.1f) { @@ -196,6 +211,7 @@ internal class DefaultSyncTask @Inject constructor( throw syncResponse.toFailure(globalErrorReceiver) .also { Timber.tag(loggerTag.value).w("INIT_SYNC request failure: $this") } } + syncStatisticsData.downloadInitSyncTime = SystemClock.elapsedRealtime() initialSyncStatusRepository.setStep(InitialSyncStatus.STEP_DOWNLOADED) } return workingFile @@ -239,6 +255,45 @@ internal class DefaultSyncTask @Inject constructor( } } + /** + * Aggregator to send stat event. + */ + class SyncStatisticsData( + val isInitSync: Boolean, + val isAfterPause: Boolean + ) { + val startTime = SystemClock.elapsedRealtime() + var requestInitSyncTime = startTime + var downloadInitSyncTime = startTime + var treatmentSyncTime = startTime + var nbOfRooms: Int = 0 + } + + private fun sendStatistics(data: SyncStatisticsData) { + sendStatisticEvent( + if (data.isInitSync) { + (StatisticEvent.InitialSyncRequest( + requestDurationMs = (data.requestInitSyncTime - data.startTime).toInt(), + downloadDurationMs = (data.downloadInitSyncTime - data.requestInitSyncTime).toInt(), + treatmentDurationMs = (data.treatmentSyncTime - data.downloadInitSyncTime).toInt(), + nbOfJoinedRooms = data.nbOfRooms, + )) + } else { + StatisticEvent.SyncTreatment( + durationMs = (data.treatmentSyncTime - data.startTime).toInt(), + afterPause = data.isAfterPause, + nbOfJoinedRooms = data.nbOfRooms + ) + } + ) + } + + private fun sendStatisticEvent(statisticEvent: StatisticEvent) { + session.dispatchTo(sessionListeners) { session, listener -> + listener.onStatisticsEvent(session, statisticEvent) + } + } + companion object { private const val MAX_NUMBER_OF_RETRY_AFTER_TIMEOUT = 50 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt index c17b31b910..0ecf91f6fa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt @@ -152,7 +152,7 @@ abstract class SyncService : Service() { private suspend fun doSync() { Timber.v("## Sync: Execute sync request with timeout $syncTimeoutSeconds seconds") - val params = SyncTask.Params(syncTimeoutSeconds * 1000L, SyncPresence.Offline) + val params = SyncTask.Params(syncTimeoutSeconds * 1000L, SyncPresence.Offline, afterPause = false) try { // never do that in foreground, let the syncThread work syncTask.execute(params) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt index b6ea7a68f7..2460720adc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt @@ -173,13 +173,14 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, if (state !is SyncState.Running) { updateStateTo(SyncState.Running(afterPause = true)) } + val afterPause = state.let { it is SyncState.Running && it.afterPause } val timeout = when { - previousSyncResponseHasToDevice -> 0L /* Force timeout to 0 */ - state.let { it is SyncState.Running && it.afterPause } -> 0L /* No timeout after a pause */ - else -> DEFAULT_LONG_POOL_TIMEOUT + previousSyncResponseHasToDevice -> 0L /* Force timeout to 0 */ + afterPause -> 0L /* No timeout after a pause */ + else -> DEFAULT_LONG_POOL_TIMEOUT } Timber.tag(loggerTag.value).d("Execute sync request with timeout $timeout") - val params = SyncTask.Params(timeout, SyncPresence.Online) + val params = SyncTask.Params(timeout, SyncPresence.Online, afterPause = afterPause) val sync = syncScope.launch { previousSyncResponseHasToDevice = doSync(params) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt index 2f1241f4d8..423a4e553f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt @@ -113,7 +113,7 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, * Will return true if the sync response contains some toDevice events. */ private suspend fun doSync(timeout: Long): Boolean { - val taskParams = SyncTask.Params(timeout * 1000, SyncPresence.Offline) + val taskParams = SyncTask.Params(timeout * 1000, SyncPresence.Offline, afterPause = false) val syncResponse = syncTask.execute(taskParams) return syncResponse.toDevice?.events?.isNotEmpty().orFalse() } @@ -151,6 +151,7 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, sessionId = sessionId, timeout = serverTimeoutInSeconds, delay = delayInSeconds, + periodic = true, forceImmediate = forceImmediate ) ) diff --git a/settings.gradle b/settings.gradle index 7d3c1de746..d3b217c517 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,4 +5,5 @@ 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 50f9c7e0a0..21ab0bab77 100755 --- 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===122 - ### Do not import temporary legacy classes import org.matrix.android.sdk.internal.legacy.riot===3 import org.matrix.androidsdk.crypto.data===2 diff --git a/tools/release/pushPlayStoreMetaData.sh b/tools/release/pushPlayStoreMetaData.sh index 276ea32fce..2d8fd9b36a 100755 --- a/tools/release/pushPlayStoreMetaData.sh +++ b/tools/release/pushPlayStoreMetaData.sh @@ -32,6 +32,15 @@ mv ./fastlane/metadata/android/nb ./fastlane_tmp # Fastlane / PlayStore require longDescription and shortDescription file to be set, so copy the default # one for languages where they are missing echo "Copying default description when missing" +if [[ -f "./fastlane/metadata/android/nl-NL/full_description.txt" ]]; then + echo "It appears that file ./fastlane/metadata/android/nl-NL/full_description.txt now exists. This can be removed." + removeFullDes_nl=0 +else + echo "Copy default full description to ./fastlane/metadata/android/nl-NL" + cp ./fastlane/metadata/android/en-US/full_description.txt ./fastlane/metadata/android/nl-NL + removeFullDes_nl=1 +fi + if [[ -f "./fastlane/metadata/android/ro/full_description.txt" ]]; then echo "It appears that file ./fastlane/metadata/android/ro/full_description.txt now exists. This can be removed." removeFullDes_ro=0 @@ -78,6 +87,10 @@ mv ./fastlane_tmp/* ./fastlane/metadata/android/ # Delete the tmp folder (should be empty) rmdir ./fastlane_tmp +if [[ ${removeFullDes_nl} -eq 1 ]]; then + rm ./fastlane/metadata/android/nl-NL/full_description.txt +fi + if [[ ${removeFullDes_ro} -eq 1 ]]; then rm ./fastlane/metadata/android/ro/full_description.txt fi diff --git a/vector/build.gradle b/vector/build.gradle index c6880b565e..e24e552ad8 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -18,7 +18,7 @@ ext.versionMinor = 3 // Note: even values are reserved for regular release, odd values for hotfix release. // When creating a hotfix, you should decrease the value, since the current value // is the value for the next regular release. -ext.versionPatch = 16 +ext.versionPatch = 18 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' @@ -156,6 +156,9 @@ android { // Indicates whether or not threading support is enabled buildConfigField "Boolean", "THREADING_ENABLED", "${isThreadingEnabled}" + buildConfigField "Boolean", "enableLocationSharing", "true" + buildConfigField "String", "mapTilerKey", "\"fU3vlMsMn4Jb6dnEIFsx\"" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" // Keep abiFilter for the universalApk @@ -253,7 +256,7 @@ android { optimizeCode true proguardFiles 'proguard-rules.pro' } - signingConfig signingConfigs.release + // signingConfig signingConfigs.release } } @@ -335,6 +338,7 @@ 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' @@ -468,7 +472,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 @@ -501,6 +504,10 @@ dependencies { } implementation 'commons-codec:commons-codec:1.15' + // MapTiler + implementation 'org.maplibre.gl:android-sdk:9.5.2' + implementation 'org.maplibre.gl:android-plugin-annotation-v9:1.0.0' + // TESTS testImplementation libs.tests.junit diff --git a/vector/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/EspressoExt.kt b/vector/src/androidTest/java/im/vector/app/EspressoExt.kt index 59982c72aa..59ad122f36 100644 --- a/vector/src/androidTest/java/im/vector/app/EspressoExt.kt +++ b/vector/src/androidTest/java/im/vector/app/EspressoExt.kt @@ -160,43 +160,50 @@ fun initialSyncIdlingResource(session: Session): IdlingResource { } fun activityIdlingResource(activityClass: Class<*>): IdlingResource { + val lifecycleMonitor = ActivityLifecycleMonitorRegistry.getInstance() + val res = object : IdlingResource, ActivityLifecycleCallback { private var callback: IdlingResource.ResourceCallback? = null + private var resumedActivity: Activity? = null + private val uniqTS = System.currentTimeMillis() - var hasResumed = false - private var currentActivity: Activity? = null - - val uniqTS = System.currentTimeMillis() override fun getName() = "activityIdlingResource_${activityClass.name}_$uniqTS" override fun isIdleNow(): Boolean { - val currentActivity = currentActivity ?: ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED).elementAtOrNull(0) + val activity = resumedActivity ?: ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED).firstOrNull { + activityClass == it.javaClass + } - val isIdle = hasResumed || currentActivity?.javaClass?.let { activityClass.isAssignableFrom(it) } ?: false - println("*** [$name] isIdleNow activityIdlingResource $currentActivity isIdle:$isIdle") + val isIdle = activity != null + if (isIdle) { + unregister() + } return isIdle } override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) { println("*** [$name] registerIdleTransitionCallback $callback") this.callback = callback - // if (hasResumed) callback?.onTransitionToIdle() } override fun onActivityLifecycleChanged(activity: Activity?, stage: Stage?) { - println("*** [$name] onActivityLifecycleChanged $activity $stage") - currentActivity = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED).elementAtOrNull(0) - val isIdle = currentActivity?.javaClass?.let { activityClass.isAssignableFrom(it) } ?: false - println("*** [$name] onActivityLifecycleChanged $currentActivity isIdle:$isIdle") - if (isIdle) { - hasResumed = true - println("*** [$name] onActivityLifecycleChanged callback: $callback") - callback?.onTransitionToIdle() - ActivityLifecycleMonitorRegistry.getInstance().removeLifecycleCallback(this) + if (activityClass == activity?.javaClass) { + when (stage) { + Stage.RESUMED -> { + unregister() + resumedActivity = activity + println("*** [$name] onActivityLifecycleChanged callback: $callback") + callback?.onTransitionToIdle() + } + } } } + + private fun unregister() { + lifecycleMonitor.removeLifecycleCallback(this) + } } - ActivityLifecycleMonitorRegistry.getInstance().addLifecycleCallback(res) + lifecycleMonitor.addLifecycleCallback(res) return res } 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..d625cf0390 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) @@ -65,12 +69,11 @@ class UiAllScreensSanityTest { preferences { crawl() } voiceAndVideo() ignoredUsers() - // TODO Test analytics securityAndPrivacy { crawl() } labs() advancedSettings { crawl() } - // TODO Rework this part (Legals, etc.) - // helpAndAbout { crawl() } + helpAndAbout { crawl() } + legals { crawl() } } elementRobot.newDirectMessage { diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt index 22a5a0790b..d7e99c63dd 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt @@ -40,6 +40,10 @@ import timber.log.Timber class ElementRobot { + fun onboarding(block: OnboardingRobot.() -> Unit) { + block(OnboardingRobot()) + } + fun signUp(userId: String) { val onboardingRobot = OnboardingRobot() onboardingRobot.createAccount(userId = userId) @@ -124,7 +128,7 @@ class ElementRobot { } waitUntilActivityVisible { - assertDisplayed(R.id.loginSplashLogo) + assertDisplayed(R.id.loginSplashSubmit) } } diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingRobot.kt index 633d3cceab..47bf31355c 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingRobot.kt @@ -18,6 +18,7 @@ package im.vector.app.ui.robot import androidx.test.espresso.Espresso.closeSoftKeyboard import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.Espresso.pressBack import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.espresso.matcher.ViewMatchers.withId import com.adevinta.android.barista.assertion.BaristaEnabledAssertions.assertDisabled @@ -31,6 +32,24 @@ import im.vector.app.waitForView class OnboardingRobot { + fun crawl() { + waitUntilViewVisible(withId(R.id.loginSplashSubmit)) + crawlGetStarted() + crawlAlreadyHaveAccount() + } + + private fun crawlGetStarted() { + clickOn(R.id.loginSplashSubmit) + OnboardingServersRobot().crawlSignUp() + pressBack() + } + + private fun crawlAlreadyHaveAccount() { + clickOn(R.id.loginSplashAlreadyHaveAccount) + OnboardingServersRobot().crawlSignIn() + pressBack() + } + fun createAccount(userId: String, password: String = "password", homeServerUrl: String = "http://10.0.2.2:8080") { initSession(true, userId, password, homeServerUrl) } @@ -44,7 +63,7 @@ class OnboardingRobot { password: String, homeServerUrl: String) { waitUntilViewVisible(withId(R.id.loginSplashSubmit)) - assertDisplayed(R.id.loginSplashSubmit, R.string.login_splash_submit) + assertDisplayed(R.id.loginSplashSubmit, R.string.login_splash_create_account) if (createAccount) { clickOn(R.id.loginSplashSubmit) } else { diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingServersRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingServersRobot.kt new file mode 100644 index 0000000000..1625b4580d --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/OnboardingServersRobot.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.ui.robot + +import androidx.test.espresso.Espresso +import androidx.test.espresso.matcher.ViewMatchers +import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions +import com.adevinta.android.barista.interaction.BaristaClickInteractions +import com.adevinta.android.barista.interaction.BaristaEditTextInteractions +import im.vector.app.R +import im.vector.app.espresso.tools.waitUntilViewVisible + +class OnboardingServersRobot { + + fun crawlSignUp() { + BaristaVisibilityAssertions.assertDisplayed(R.id.loginServerTitle, R.string.login_server_title) + crawlMatrixServer(isSignUp = true) + crawlEmsServer() + crawlOtherServer(isSignUp = true) + crawlSignInWithMatrixId() + } + + fun crawlSignIn() { + BaristaVisibilityAssertions.assertDisplayed(R.id.loginServerTitle, R.string.login_server_title) + crawlMatrixServer(isSignUp = false) + crawlEmsServer() + crawlOtherServer(isSignUp = false) + crawlSignInWithMatrixId() + } + + private fun crawlOtherServer(isSignUp: Boolean) { + BaristaClickInteractions.clickOn(R.id.loginServerChoiceOther) + waitUntilViewVisible(ViewMatchers.withId(R.id.loginServerUrlFormTitle)) + BaristaEditTextInteractions.writeTo(R.id.loginServerUrlFormHomeServerUrl, "https://chat.mozilla.org") + BaristaClickInteractions.clickOn(R.id.loginServerUrlFormSubmit) + waitUntilViewVisible(ViewMatchers.withId(R.id.loginSignupSigninTitle)) + BaristaVisibilityAssertions.assertDisplayed(R.id.loginSignupSigninText, "Connect to chat.mozilla.org") + BaristaVisibilityAssertions.assertDisplayed(R.id.loginSignupSigninSubmit, R.string.login_signin_sso) + Espresso.pressBack() + + BaristaEditTextInteractions.writeTo(R.id.loginServerUrlFormHomeServerUrl, "https://matrix.org") + BaristaClickInteractions.clickOn(R.id.loginServerUrlFormSubmit) + assetMatrixSignInOptions(isSignUp) + Espresso.pressBack() + Espresso.pressBack() + } + + private fun crawlEmsServer() { + BaristaClickInteractions.clickOn(R.id.loginServerChoiceEms) + waitUntilViewVisible(ViewMatchers.withId(R.id.loginServerUrlFormTitle)) + BaristaVisibilityAssertions.assertDisplayed(R.id.loginServerUrlFormTitle, R.string.login_connect_to_modular) + + BaristaEditTextInteractions.writeTo(R.id.loginServerUrlFormHomeServerUrl, "https://one.ems.host") + BaristaClickInteractions.clickOn(R.id.loginServerUrlFormSubmit) + + waitUntilViewVisible(ViewMatchers.withId(R.id.loginSignupSigninTitle)) + BaristaVisibilityAssertions.assertDisplayed(R.id.loginSignupSigninText, "one.ems.host") + BaristaVisibilityAssertions.assertDisplayed(R.id.loginSignupSigninSubmit, R.string.login_signin_sso) + Espresso.pressBack() + Espresso.pressBack() + } + + private fun crawlMatrixServer(isSignUp: Boolean) { + BaristaClickInteractions.clickOn(R.id.loginServerChoiceMatrixOrg) + assetMatrixSignInOptions(isSignUp) + Espresso.pressBack() + } + + private fun assetMatrixSignInOptions(isSignUp: Boolean) { + waitUntilViewVisible(ViewMatchers.withId(R.id.loginTitle)) + when (isSignUp) { + true -> BaristaVisibilityAssertions.assertDisplayed(R.id.loginTitle, "Sign up to matrix.org") + false -> BaristaVisibilityAssertions.assertDisplayed(R.id.loginTitle, "Connect to matrix.org") + } + } + + private fun crawlSignInWithMatrixId() { + BaristaClickInteractions.clickOn(R.id.loginServerIKnowMyIdSubmit) + waitUntilViewVisible(ViewMatchers.withId(R.id.loginTitle)) + BaristaVisibilityAssertions.assertDisplayed(R.id.loginTitle, R.string.login_signin_matrix_id_title) + Espresso.pressBack() + } +} diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt index 53d6c16bb7..ebf5fdf23d 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt @@ -37,7 +37,6 @@ import im.vector.app.features.home.room.detail.timeline.action.MessageActionsBot import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet import im.vector.app.features.reactions.data.EmojiDataSource import im.vector.app.interactWithSheet -import im.vector.app.waitForView import im.vector.app.withRetry import java.lang.Thread.sleep @@ -127,7 +126,7 @@ class RoomDetailRobot { fun openSettings(block: RoomSettingsRobot.() -> Unit) { clickMenu(R.id.timeline_setting) - waitForView(withId(R.id.roomProfileAvatarView)) + waitUntilViewVisible(withId(R.id.roomProfileAvatarView)) sleep(1000) block(RoomSettingsRobot()) pressBack() diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsHelpRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsHelpRobot.kt index 75f610d016..cf0c997d80 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsHelpRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsHelpRobot.kt @@ -16,10 +16,6 @@ package im.vector.app.ui.robot.settings -import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn -import com.adevinta.android.barista.interaction.BaristaDialogInteractions.clickDialogPositiveButton -import im.vector.app.R - class SettingsHelpRobot { fun crawl() { @@ -34,7 +30,5 @@ class SettingsHelpRobot { clickOn(R.string.settings_privacy_policy) pressBack() */ - clickOn(R.string.settings_third_party_notices) - clickDialogPositiveButton() } } diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsLegalsRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsLegalsRobot.kt new file mode 100644 index 0000000000..842471752a --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsLegalsRobot.kt @@ -0,0 +1,29 @@ +/* + * 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.ui.robot.settings + +import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn +import com.adevinta.android.barista.interaction.BaristaDialogInteractions.clickDialogPositiveButton +import im.vector.app.R + +class SettingsLegalsRobot { + + fun crawl() { + clickOn(R.string.settings_third_party_notices) + clickDialogPositiveButton() + } +} diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsRobot.kt index a9c053f6c3..561f14c6f2 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsRobot.kt @@ -64,4 +64,8 @@ class SettingsRobot { fun helpAndAbout(block: SettingsHelpRobot.() -> Unit) { clickOnAndGoBack(R.string.preference_root_help_about) { block(SettingsHelpRobot()) } } + + fun legals(block: SettingsLegalsRobot.() -> Unit) { + clickOnAndGoBack(R.string.preference_root_legals) { block(SettingsLegalsRobot()) } + } } diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsSecurityRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsSecurityRobot.kt index f2607bbc1c..168db3e0e9 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsSecurityRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/SettingsSecurityRobot.kt @@ -33,5 +33,8 @@ class SettingsSecurityRobot { clickOnPreference(R.string.encryption_export_e2e_room_keys) pressBack() */ + + clickOnPreference(R.string.settings_opt_in_of_analytics) + Espresso.pressBack() } } diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index f2c6cd763e..2e5412870f 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -42,6 +42,10 @@ android:name="android.permission.WRITE_CALENDAR" tools:node="remove" /> + + + + @@ -335,6 +339,7 @@ + diff --git a/vector/src/main/assets/open_source_licenses.html b/vector/src/main/assets/open_source_licenses.html index 0eefa3b863..2c25606f57 100755 --- a/vector/src/main/assets/open_source_licenses.html +++ b/vector/src/main/assets/open_source_licenses.html @@ -254,6 +254,33 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +
    +
  • + org.maplibre.gl:android-sdk +
    + org.maplibre.gl:android-plugin-annotation-v9 +
    + BSD 2-Clause License + + Copyright (c) 2021 MapLibre contributors + + Copyright (c) 2018-2021 MapTiler.com + + Copyright (c) 2014-2020 Mapbox +
  • +
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
  • textdrawable diff --git a/vector/src/main/java/im/vector/app/AppStateHandler.kt b/vector/src/main/java/im/vector/app/AppStateHandler.kt index 2a3d2cd87f..1ff3d97576 100644 --- a/vector/src/main/java/im/vector/app/AppStateHandler.kt +++ b/vector/src/main/java/im/vector/app/AppStateHandler.kt @@ -78,11 +78,17 @@ class AppStateHandler @Inject constructor( } } - fun setCurrentSpace(spaceId: String?, session: Session? = null) { + fun setCurrentSpace(spaceId: String?, session: Session? = null, persistNow: Boolean = false) { val uSession = session ?: activeSessionHolder.getSafeActiveSession() ?: return if (selectedSpaceDataSource.currentValue?.orNull() is RoomGroupingMethod.BySpace && spaceId == selectedSpaceDataSource.currentValue?.orNull()?.space()?.roomId) return val spaceSum = spaceId?.let { uSession.getRoomSummary(spaceId) } + + if (persistNow) { + uiStateRepository.storeGroupingMethod(true, uSession.sessionId) + uiStateRepository.storeSelectedSpace(spaceSum?.roomId, uSession.sessionId) + } + selectedSpaceDataSource.post(Option.just(RoomGroupingMethod.BySpace(spaceSum))) if (spaceId != null) { uSession.coroutineScope.launch(Dispatchers.IO) { diff --git a/vector/src/main/java/im/vector/app/AutoRageShaker.kt b/vector/src/main/java/im/vector/app/AutoRageShaker.kt index ca91f728cb..0238931e4c 100644 --- a/vector/src/main/java/im/vector/app/AutoRageShaker.kt +++ b/vector/src/main/java/im/vector/app/AutoRageShaker.kt @@ -67,6 +67,7 @@ class AutoRageShaker @Inject constructor( fun initialize() { observeActiveSession() + enable(vectorPreferences.labsAutoReportUISI()) // It's a singleton... vectorPreferences.subscribeToChanges(this) @@ -141,17 +142,19 @@ class AutoRageShaker @Inject constructor( withCrashLogs = true, withKeyRequestHistory = true, withScreenshot = false, - theBugDescription = "UISI detected", + theBugDescription = "Auto-reporting decryption error", serverVersion = "", canContact = false, - customFields = mapOf("auto-uisi" to buildString { - append("\neventId: ${target.eventId}") - append("\nroomId: ${target.roomId}") - append("\nsenderKey: ${target.senderKey}") - append("\nsource: ${target.source}") - append("\ndeviceId: ${target.senderDeviceId}") - append("\nuserId: ${target.senderUserId}") - append("\nsessionId: ${target.sessionId}") + customFields = mapOf("auto_uisi" to buildString { + append("{") + append("\"event_id\": \"${target.eventId}\",") + append("\"room_id\": \"${target.roomId}\",") + append("\"sender_key\": \"${target.senderKey}\",") + append("\"device_id\": \"${target.senderDeviceId}\",") + append("\"source\": \"${target.source}\",") + append("\"user_id\": \"${target.senderUserId}\",") + append("\"session_id\": \"${target.sessionId}\"") + append("}") }), listener = object : BugReporter.IMXBugReportListener { override fun onUploadCancelled() { @@ -221,17 +224,19 @@ class AutoRageShaker @Inject constructor( withCrashLogs = true, withKeyRequestHistory = true, withScreenshot = false, - theBugDescription = "UISI detected $matchingIssue", + theBugDescription = "Auto-reporting decryption error \nRecipient rageshake: $matchingIssue", serverVersion = "", canContact = false, customFields = mapOf( - "auto-uisi" to buildString { - append("\neventId: $eventId") - append("\nroomId: $roomId") - append("\nsenderKey: $senderKey") - append("\ndeviceId: $deviceId") - append("\nuserId: $userId") - append("\nsessionId: $sessionId") + "auto_uisi" to buildString { + append("{") + append("\"event_id\": \"$eventId\",") + append("\"room_id\": \"$roomId\",") + append("\"sender_key\": \"$senderKey\",") + append("\"device_id\": \"$deviceId\",") + append("\"user_id\": \"$userId\",") + append("\"session_id\": \"$sessionId\"") + append("}") }, "recipient_rageshake" to matchingIssue ), diff --git a/vector/src/main/java/im/vector/app/VectorApplication.kt b/vector/src/main/java/im/vector/app/VectorApplication.kt index 05d20662c7..d252b5d9bd 100644 --- a/vector/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector/src/main/java/im/vector/app/VectorApplication.kt @@ -36,6 +36,7 @@ import com.airbnb.epoxy.EpoxyController import com.airbnb.mvrx.Mavericks import com.facebook.stetho.Stetho import com.gabrielittner.threetenbp.LazyThreeTen +import com.mapbox.mapboxsdk.Mapbox import com.vanniktech.emoji.EmojiManager import com.vanniktech.emoji.google.GoogleEmojiProvider import dagger.hilt.android.HiltAndroidApp @@ -197,6 +198,9 @@ class VectorApplication : }) EmojiManager.install(GoogleEmojiProvider()) + + // Initialize Mapbox before inflating mapViews + Mapbox.getInstance(this) } private val startSyncOnFirstStart = object : DefaultLifecycleObserver { diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index e87b1fbeba..e7aa83ae75 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 @@ -62,6 +62,8 @@ import im.vector.app.features.home.room.detail.TimelineFragment import im.vector.app.features.home.room.detail.search.SearchFragment import im.vector.app.features.home.room.list.RoomListFragment import im.vector.app.features.home.room.threads.list.views.ThreadListFragment +import im.vector.app.features.location.LocationPreviewFragment +import im.vector.app.features.location.LocationSharingFragment import im.vector.app.features.login.LoginCaptchaFragment import im.vector.app.features.login.LoginFragment import im.vector.app.features.login.LoginGenericTextInputFormFragment @@ -945,4 +947,14 @@ interface FragmentModule { @IntoMap @FragmentKey(CreatePollFragment::class) fun bindCreatePollFragment(fragment: CreatePollFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(LocationSharingFragment::class) + fun bindLocationSharingFragment(fragment: LocationSharingFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(LocationPreviewFragment::class) + fun bindLocationPreviewFragment(fragment: LocationPreviewFragment): Fragment } diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt index 98b1a5d048..cce231708a 100644 --- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt @@ -54,6 +54,7 @@ import im.vector.app.features.home.room.detail.upgrade.MigrateRoomViewModel import im.vector.app.features.home.room.list.RoomListViewModel import im.vector.app.features.homeserver.HomeServerCapabilitiesViewModel import im.vector.app.features.invite.InviteUsersToRoomViewModel +import im.vector.app.features.location.LocationSharingViewModel import im.vector.app.features.login.LoginViewModel import im.vector.app.features.login2.LoginViewModel2 import im.vector.app.features.login2.created.AccountCreatedViewModel @@ -588,4 +589,9 @@ interface MavericksViewModelModule { @IntoMap @MavericksViewModelKey(CreatePollViewModel::class) fun createPollViewModelFactory(factory: CreatePollViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(LocationSharingViewModel::class) + fun createLocationSharingViewModelFactory(factory: LocationSharingViewModel.Factory): MavericksAssistedViewModelFactory<*, *> } diff --git a/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt b/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt index 0b9855ef56..283437c679 100644 --- a/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt +++ b/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt @@ -21,7 +21,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import im.vector.app.core.dialogs.UnrecognizedCertificateDialog import im.vector.app.core.error.ErrorFormatter -import im.vector.app.features.analytics.VectorAnalytics +import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.navigation.Navigator @@ -56,7 +56,7 @@ interface SingletonEntryPoint { fun pinLocker(): PinLocker - fun analytics(): VectorAnalytics + fun analyticsTracker(): AnalyticsTracker fun webRtcCallManager(): WebRtcCallManager diff --git a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt index d83bb5cb57..0e19cd4388 100644 --- a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt @@ -33,6 +33,7 @@ import im.vector.app.core.error.DefaultErrorFormatter import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.time.Clock import im.vector.app.core.time.DefaultClock +import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.analytics.impl.DefaultVectorAnalytics import im.vector.app.features.invite.AutoAcceptInvites @@ -64,6 +65,9 @@ abstract class VectorBindModule { @Binds abstract fun bindVectorAnalytics(analytics: DefaultVectorAnalytics): VectorAnalytics + @Binds + abstract fun bindAnalyticsTracker(analytics: DefaultVectorAnalytics): AnalyticsTracker + @Binds abstract fun bindErrorFormatter(formatter: DefaultErrorFormatter): ErrorFormatter diff --git a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt index 35db585b79..cdecd2d6c6 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt @@ -26,14 +26,17 @@ import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider import im.vector.app.features.home.room.detail.timeline.item.BindingOptions import im.vector.app.features.home.room.detail.timeline.tools.findPillsAndProcess +import im.vector.app.features.location.LocationData +import im.vector.app.features.location.MapTilerMapView import im.vector.app.features.media.ImageContentRenderer +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence import org.matrix.android.sdk.api.util.MatrixItem /** @@ -66,6 +69,12 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel + holder.mapView.initialize { + if (holder.view.isAttachedToWindow) { + holder.mapView.zoomToLocation(location.latitude, location.longitude, 15.0) + locationPinProvider?.create(matrixItem.id) { pinDrawable -> + holder.mapView.addPinToMap(matrixItem.id, pinDrawable) + holder.mapView.updatePinLocation(matrixItem.id, location.latitude, location.longitude) + } + } + } + } } override fun unbind(holder: Holder) { @@ -101,5 +124,6 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel(R.id.bottom_sheet_message_preview_body_details) val timestamp by bind(R.id.bottom_sheet_message_preview_timestamp) val imagePreview by bind(R.id.bottom_sheet_message_preview_image) + val mapView by bind(R.id.bottom_sheet_message_preview_location) } } diff --git a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt index 8640fa6f05..2eb36d758e 100644 --- a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt @@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.MatrixIdFailure import org.matrix.android.sdk.api.failure.isInvalidPassword +import org.matrix.android.sdk.api.failure.isLimitExceededError import org.matrix.android.sdk.api.session.identity.IdentityServiceError import java.net.HttpURLConnection import java.net.SocketTimeoutException @@ -58,53 +59,53 @@ class DefaultErrorFormatter @Inject constructor( } is Failure.ServerError -> { when { - throwable.error.code == MatrixError.M_CONSENT_NOT_GIVEN -> { + throwable.error.code == MatrixError.M_CONSENT_NOT_GIVEN -> { // Special case for terms and conditions stringProvider.getString(R.string.error_terms_not_accepted) } - throwable.isInvalidPassword() -> { + throwable.isInvalidPassword() -> { stringProvider.getString(R.string.auth_invalid_login_param) } - throwable.error.code == MatrixError.M_USER_IN_USE -> { + throwable.error.code == MatrixError.M_USER_IN_USE -> { stringProvider.getString(R.string.login_signup_error_user_in_use) } - throwable.error.code == MatrixError.M_BAD_JSON -> { + throwable.error.code == MatrixError.M_BAD_JSON -> { stringProvider.getString(R.string.login_error_bad_json) } - throwable.error.code == MatrixError.M_NOT_JSON -> { + throwable.error.code == MatrixError.M_NOT_JSON -> { stringProvider.getString(R.string.login_error_not_json) } - throwable.error.code == MatrixError.M_THREEPID_DENIED -> { + throwable.error.code == MatrixError.M_THREEPID_DENIED -> { stringProvider.getString(R.string.login_error_threepid_denied) } - throwable.error.code == MatrixError.M_LIMIT_EXCEEDED -> { + throwable.isLimitExceededError() -> { limitExceededError(throwable.error) } - throwable.error.code == MatrixError.M_TOO_LARGE -> { + throwable.error.code == MatrixError.M_TOO_LARGE -> { stringProvider.getString(R.string.error_file_too_big_simple) } - throwable.error.code == MatrixError.M_THREEPID_NOT_FOUND -> { + throwable.error.code == MatrixError.M_THREEPID_NOT_FOUND -> { stringProvider.getString(R.string.login_reset_password_error_not_found) } - throwable.error.code == MatrixError.M_USER_DEACTIVATED -> { + throwable.error.code == MatrixError.M_USER_DEACTIVATED -> { stringProvider.getString(R.string.auth_invalid_login_deactivated_account) } throwable.error.code == MatrixError.M_THREEPID_IN_USE && - throwable.error.message == "Email is already in use" -> { + throwable.error.message == "Email is already in use" -> { stringProvider.getString(R.string.account_email_already_used_error) } throwable.error.code == MatrixError.M_THREEPID_IN_USE && - throwable.error.message == "MSISDN is already in use" -> { + throwable.error.message == "MSISDN is already in use" -> { stringProvider.getString(R.string.account_phone_number_already_used_error) } - throwable.error.code == MatrixError.M_THREEPID_AUTH_FAILED -> { + throwable.error.code == MatrixError.M_THREEPID_AUTH_FAILED -> { stringProvider.getString(R.string.error_threepid_auth_failed) } throwable.error.code == MatrixError.M_UNKNOWN && throwable.error.message == "Not allowed to join this room" -> { stringProvider.getString(R.string.room_error_access_unauthorized) } - else -> { + else -> { throwable.error.message.takeIf { it.isNotEmpty() } ?: throwable.error.code.takeIf { it.isNotEmpty() } } diff --git a/vector/src/main/java/im/vector/app/core/extensions/Context.kt b/vector/src/main/java/im/vector/app/core/extensions/Context.kt index 59847da7c9..1063d30a41 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/Context.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/Context.kt @@ -17,9 +17,36 @@ package im.vector.app.core.extensions import android.content.Context +import android.graphics.drawable.Drawable +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.annotation.FloatRange +import androidx.core.content.ContextCompat import dagger.hilt.EntryPoints import im.vector.app.core.di.SingletonEntryPoint +import kotlin.math.roundToInt fun Context.singletonEntryPoint(): SingletonEntryPoint { return EntryPoints.get(applicationContext, SingletonEntryPoint::class.java) } + +fun Context.getResTintedDrawable(@DrawableRes drawableRes: Int, @ColorRes tint: Int, @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1f): Drawable? { + return getTintedDrawable(drawableRes, ContextCompat.getColor(this, tint), alpha) +} + +fun Context.getTintedDrawable(@DrawableRes drawableRes: Int, + @ColorInt tint: Int, + @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1f +) = ContextCompat.getDrawable(this, drawableRes) + ?.mutate() + ?.also { drawable -> + drawable.setTint(tint) + alpha.let { + drawable.alpha = it.toAndroidAlpha() + } + } + +private fun Float.toAndroidAlpha(): Int { + return (this * 255).roundToInt() +} diff --git a/vector/src/main/java/im/vector/app/core/extensions/TextView.kt b/vector/src/main/java/im/vector/app/core/extensions/TextView.kt index 0b1aca1ee5..0564f2055b 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/TextView.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/TextView.kt @@ -16,6 +16,7 @@ package im.vector.app.core.extensions +import android.graphics.drawable.Drawable import android.text.Spannable import android.text.SpannableString import android.text.TextPaint @@ -121,7 +122,11 @@ fun TextView.setLeftDrawable(@DrawableRes iconRes: Int, @AttrRes tintColor: Int? } else { ContextCompat.getDrawable(context, iconRes) } - setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null) + setLeftDrawable(icon) +} + +fun TextView.setLeftDrawable(drawable: Drawable?) { + setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null) } fun TextView.clearDrawables() { 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/ui/list/GenericFooterItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/GenericFooterItem.kt index 5e3ea285a5..6d7763cd48 100644 --- a/vector/src/main/java/im/vector/app/core/ui/list/GenericFooterItem.kt +++ b/vector/src/main/java/im/vector/app/core/ui/list/GenericFooterItem.kt @@ -24,10 +24,10 @@ import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide import im.vector.app.features.themes.ThemeUtils +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence /** * A generic list item. diff --git a/vector/src/main/java/im/vector/app/core/ui/list/GenericItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/GenericItem.kt index 3a691ddfd5..7b00001e4c 100644 --- a/vector/src/main/java/im/vector/app/core/ui/list/GenericItem.kt +++ b/vector/src/main/java/im/vector/app/core/ui/list/GenericItem.kt @@ -28,9 +28,9 @@ import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence /** * A generic list item. diff --git a/vector/src/main/java/im/vector/app/core/ui/list/GenericPillItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/GenericPillItem.kt index e116561ecb..09fdcded6e 100644 --- a/vector/src/main/java/im/vector/app/core/ui/list/GenericPillItem.kt +++ b/vector/src/main/java/im/vector/app/core/ui/list/GenericPillItem.kt @@ -28,10 +28,10 @@ import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide import im.vector.app.features.themes.ThemeUtils +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence /** * A generic list item with a rounded corner background and an optional icon diff --git a/vector/src/main/java/im/vector/app/core/ui/list/GenericWithValueItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/GenericWithValueItem.kt index f95281eb75..e633b633a7 100644 --- a/vector/src/main/java/im/vector/app/core/ui/list/GenericWithValueItem.kt +++ b/vector/src/main/java/im/vector/app/core/ui/list/GenericWithValueItem.kt @@ -27,10 +27,10 @@ import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide import im.vector.app.features.themes.ThemeUtils +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence /** * A generic list item. diff --git a/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt b/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt index 478f4d882b..a9375b6545 100644 --- a/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt +++ b/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt @@ -183,6 +183,26 @@ fun openMedia(activity: Activity, savedMediaPath: String, mimeType: String) { activity.safeStartActivity(intent) } +/** + * Open external location + * @param activity the activity + * @param latitude latitude of the location + * @param longitude longitude of the location + */ +fun openLocation(activity: Activity, latitude: Double, longitude: Double) { + val locationUri = buildString { + append("geo:") + append(latitude) + append(",") + append(longitude) + append("?q=") // This is required to drop a pin to the location + append(latitude) + append(",") + append(longitude) + } + openUri(activity, locationUri) +} + fun shareMedia(context: Context, file: File, mediaMimeType: String?) { val mediaUri = try { FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileProvider", file) diff --git a/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt b/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt index 19dc341f12..dabf11b9d3 100644 --- a/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt +++ b/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt @@ -40,6 +40,7 @@ val PERMISSIONS_FOR_MEMBERS_SEARCH = listOf(Manifest.permission.READ_CONTACTS) val PERMISSIONS_FOR_ROOM_AVATAR = listOf(Manifest.permission.CAMERA) val PERMISSIONS_FOR_WRITING_FILES = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE) val PERMISSIONS_FOR_PICKING_CONTACT = listOf(Manifest.permission.READ_CONTACTS) +val PERMISSIONS_FOR_LOCATION_SHARING = listOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION) val PERMISSIONS_EMPTY = emptyList() diff --git a/vector/src/main/java/im/vector/app/core/utils/ToolbarConfig.kt b/vector/src/main/java/im/vector/app/core/utils/ToolbarConfig.kt new file mode 100644 index 0000000000..53bc60a4a6 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/utils/ToolbarConfig.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.utils + +import androidx.annotation.StringRes +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.content.res.AppCompatResources +import com.google.android.material.appbar.MaterialToolbar +import im.vector.app.R + +/** + * Helper class to configure toolbar. + * Wraps [MaterialToolbar] providing set of methods to configure it + */ +class ToolbarConfig(val activity: AppCompatActivity, val toolbar: MaterialToolbar) { + private var customBackResId: Int? = null + + fun setup() = apply { + activity.setSupportActionBar(toolbar) + } + + /** + * Delegating property for [activity.supportActionBar?.title] + */ + var title: CharSequence? + set(value) { + setTitle(value) + } + get() = activity.supportActionBar?.title + + /** + * Delegating property for [activity.supportActionBar?.subtitle] + */ + var subtitle: CharSequence? + set(value) { + setSubtitle(value) + } + get() = activity.supportActionBar?.subtitle + + /** + * Sets toolbar's title text + */ + fun setTitle(title: CharSequence?) = apply { activity.supportActionBar?.title = title } + + /** + * Sets toolbar's title text using provided string resource + */ + fun setTitle(@StringRes titleRes: Int) = apply { activity.supportActionBar?.setTitle(titleRes) } + + /** + * Sets toolbar's subtitle text + */ + fun setSubtitle(subtitle: CharSequence?) = apply { activity.supportActionBar?.subtitle = subtitle } + + /** + * Sets toolbar's title text using provided string resource + */ + fun setSubtitle(@StringRes subtitleRes: Int) = apply { activity.supportActionBar?.setSubtitle(subtitleRes) } + + /** + * Enables/disables navigate back button + * + * @param isAllowed defines if back button is enabled. Default [true] + * @param useCross defines if cross icon should be used instead of arrow. Default [false] + */ + fun allowBack(isAllowed: Boolean = true, useCross: Boolean = false) = apply { + activity.supportActionBar?.let { + it.setDisplayShowHomeEnabled(isAllowed) + it.setDisplayHomeAsUpEnabled(isAllowed) + if (isAllowed && useCross) { + val navResId = customBackResId ?: R.drawable.ic_x_18dp + toolbar.navigationIcon = AppCompatResources.getDrawable(activity, navResId) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt index 25c204f2ef..03e9954b2c 100644 --- a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt +++ b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt @@ -35,6 +35,6 @@ interface VectorFeatures { class DefaultVectorFeatures : VectorFeatures { override fun onboardingVariant(): VectorFeatures.OnboardingVariant = BuildConfig.ONBOARDING_VARIANT override fun isOnboardingAlreadyHaveAccountSplashEnabled() = true - override fun isOnboardingSplashCarouselEnabled() = false + 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 eb2a99fdd4..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 @@ -18,6 +18,7 @@ package im.vector.app.features.analytics 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 @@ -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/extensions/PerformanceTimerExt.kt b/vector/src/main/java/im/vector/app/features/analytics/extensions/PerformanceTimerExt.kt new file mode 100644 index 0000000000..11b4b670d3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/extensions/PerformanceTimerExt.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.analytics.extensions + +import im.vector.app.features.analytics.plan.PerformanceTimer +import org.matrix.android.sdk.api.session.statistics.StatisticEvent + +fun StatisticEvent.toListOfPerformanceTimer(): List { + return when (this) { + is StatisticEvent.InitialSyncRequest -> + listOf( + PerformanceTimer( + name = PerformanceTimer.Name.InitialSyncRequest, + timeMs = requestDurationMs + downloadDurationMs, + itemCount = nbOfJoinedRooms + ), + PerformanceTimer( + name = PerformanceTimer.Name.InitialSyncParsing, + timeMs = treatmentDurationMs, + itemCount = nbOfJoinedRooms + ) + ) + is StatisticEvent.SyncTreatment -> + if (afterPause) { + listOf( + PerformanceTimer( + name = PerformanceTimer.Name.StartupIncrementalSync, + timeMs = durationMs, + itemCount = nbOfJoinedRooms + ) + ) + } else { + // We do not report + emptyList() + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/CallEnded.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/CallEnded.kt index cd813325f1..3bf16a6c97 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/CallEnded.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/CallEnded.kt @@ -25,22 +25,22 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent * Triggered when a call has ended. */ data class CallEnded( - /** - * The duration of the call in milliseconds. - */ - val durationMs: Int, - /** - * Whether its a video call or not. - */ - val isVideo: Boolean, - /** - * Number of participants in the call. - */ - val numParticipants: Int, - /** - * Whether this user placed it. - */ - val placed: Boolean, + /** + * The duration of the call in milliseconds. + */ + val durationMs: Int, + /** + * Whether its a video call or not. + */ + val isVideo: Boolean, + /** + * Number of participants in the call. + */ + val numParticipants: Int, + /** + * Whether this user placed it. + */ + val placed: Boolean, ) : VectorAnalyticsEvent { override fun getName() = "CallEnded" diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/CallError.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/CallError.kt index 18e77f9f1c..1c3a57e971 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/CallError.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/CallError.kt @@ -25,18 +25,18 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent * Triggered when an error occurred in a call. */ data class CallError( - /** - * Whether its a video call or not. - */ - val isVideo: Boolean, - /** - * Number of participants in the call. - */ - val numParticipants: Int, - /** - * Whether this user placed it. - */ - val placed: Boolean, + /** + * Whether its a video call or not. + */ + val isVideo: Boolean, + /** + * Number of participants in the call. + */ + val numParticipants: Int, + /** + * Whether this user placed it. + */ + val placed: Boolean, ) : VectorAnalyticsEvent { override fun getName() = "CallError" diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/CallStarted.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/CallStarted.kt index 81f4b6c194..e74d07d38c 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/CallStarted.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/CallStarted.kt @@ -25,18 +25,18 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent * Triggered when a call is started. */ data class CallStarted( - /** - * Whether its a video call or not. - */ - val isVideo: Boolean, - /** - * Number of participants in the call. - */ - val numParticipants: Int, - /** - * Whether this user placed it. - */ - val placed: Boolean, + /** + * Whether its a video call or not. + */ + val isVideo: Boolean, + /** + * Number of participants in the call. + */ + val numParticipants: Int, + /** + * Whether this user placed it. + */ + val placed: Boolean, ) : VectorAnalyticsEvent { override fun getName() = "CallStarted" diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/Click.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/Click.kt index fbc36a1195..fbc847165d 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/Click.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/Click.kt @@ -25,14 +25,14 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent * Triggered when the user clicks/taps on a UI element. */ data class Click( - /** - * The index of the element, if its in a list of elements. - */ - val index: Int? = null, - /** - * The unique name of this element. - */ - val name: Name, + /** + * The index of the element, if its in a list of elements. + */ + val index: Int? = null, + /** + * The unique name of this element. + */ + val name: Name, ) : VectorAnalyticsEvent { enum class Name { diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/CreatedRoom.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/CreatedRoom.kt index 9562a6e735..598cc6ac28 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/CreatedRoom.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/CreatedRoom.kt @@ -25,10 +25,10 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent * Triggered when the user creates a room. */ data class CreatedRoom( - /** - * Whether the room is a DM. - */ - val isDM: Boolean, + /** + * Whether the room is a DM. + */ + val isDM: Boolean, ) : VectorAnalyticsEvent { override fun getName() = "CreatedRoom" diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/Error.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/Error.kt index 988ad309b9..a926776680 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/Error.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/Error.kt @@ -25,12 +25,12 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent * Triggered when an error occurred */ data class Error( - /** - * Context - client defined, can be used for debugging - */ - val context: String? = null, - val domain: Domain, - val name: Name, + /** + * Context - client defined, can be used for debugging + */ + val context: String? = null, + val domain: Domain, + val name: Name, ) : VectorAnalyticsEvent { enum class Domain { diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/Identity.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/Identity.kt new file mode 100644 index 0000000000..1cc433aa7e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/Identity.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.analytics.plan + +import im.vector.app.features.analytics.itf.VectorAnalyticsEvent + +// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT +// https://github.com/matrix-org/matrix-analytics-events/ + +/** + * The user properties to apply when identifying + */ +data class Identity( + /** + * The selected messaging use case during the onboarding flow. + */ + val ftueUseCaseSelection: FtueUseCaseSelection? = null, +) : VectorAnalyticsEvent { + + enum class FtueUseCaseSelection { + /** + * The third option, Communities. + */ + CommunityMessaging, + + /** + * The first option, Friends and family. + */ + PersonalMessaging, + + /** + * The footer option to skip the question. + */ + Skip, + + /** + * The second option, Teams. + */ + WorkMessaging, + } + + override fun getName() = "Identity" + + override fun getProperties(): Map? { + return mutableMapOf().apply { + ftueUseCaseSelection?.let { put("ftueUseCaseSelection", it.name) } + }.takeIf { it.isNotEmpty() } + } +} diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/JoinedRoom.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/JoinedRoom.kt index fc5f29bff1..97ac19ec93 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/JoinedRoom.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/JoinedRoom.kt @@ -25,14 +25,14 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent * Triggered when the user joins a room. */ data class JoinedRoom( - /** - * Whether the room is a DM. - */ - val isDM: Boolean, - /** - * The size of the room. - */ - val roomSize: RoomSize, + /** + * Whether the room is a DM. + */ + val isDM: Boolean, + /** + * The size of the room. + */ + val roomSize: RoomSize, ) : VectorAnalyticsEvent { enum class RoomSize { diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/PerformanceTimer.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/PerformanceTimer.kt index 34d0297f2d..2cfc366cd3 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/PerformanceTimer.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/PerformanceTimer.kt @@ -25,22 +25,23 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent * Triggered after timing an operation in the app. */ data class PerformanceTimer( - /** - * Client defined, can be used for debugging. - */ - val context: String? = null, - /** - * Client defined, an optional value to indicate how many items were handled during the operation. - */ - val itemCount: Int? = null, - /** - * The timer that is being reported. - */ - val name: Name, - /** - * The time reported by the timer in milliseconds. - */ - val timeMs: Int, + /** + * Client defined, can be used for debugging. + */ + val context: String? = null, + /** + * Client defined, an optional value to indicate how many items were + * handled during the operation. + */ + val itemCount: Int? = null, + /** + * The timer that is being reported. + */ + val name: Name, + /** + * The time reported by the timer in milliseconds. + */ + val timeMs: Int, ) : VectorAnalyticsEvent { enum class Name { @@ -55,7 +56,8 @@ data class PerformanceTimer( InitialSyncRequest, /** - * The time taken to display an event in the timeline that was opened from a notification. + * The time taken to display an event in the timeline that was opened + * from a notification. */ NotificationsOpenEvent, @@ -65,7 +67,8 @@ data class PerformanceTimer( StartupIncrementalSync, /** - * The duration of an initial /sync request during startup (if the store has been wiped). + * The duration of an initial /sync request during startup (if the store + * has been wiped). */ StartupInitialSync, @@ -80,7 +83,8 @@ data class PerformanceTimer( StartupStorePreload, /** - * The time to load all data from the store (including StartupStorePreload time). + * The time to load all data from the store (including + * StartupStorePreload time). */ StartupStoreReady, } diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/Screen.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/Screen.kt index 1f18ceee00..db4dcd0fac 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/Screen.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/Screen.kt @@ -25,28 +25,221 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsScreen * Triggered when the user changed screen */ data class Screen( - /** - * How long the screen was displayed for in milliseconds. - */ - val durationMs: Int? = null, - val screenName: ScreenName, + /** + * How long the screen was displayed for in milliseconds. + */ + val durationMs: Int? = null, + val screenName: ScreenName, ) : VectorAnalyticsScreen { enum class ScreenName { + /** + * The screen shown to create a new (non-direct) room. + */ + CreateRoom, + + /** + * The confirmation screen shown before deactivating an account. + */ + DeactivateAccount, + + /** + * The form for the forgot password use case + */ + ForgotPassword, + + /** + * Legacy: The screen that shows information about a specific group. + */ Group, + + /** + * The Home tab on iOS | possibly the same on Android? | The Home space + * on Web? + */ Home, + + /** + * The screen that displays the login flow (when the user already has an + * account). + */ + Login, + + /** + * The screen that displays the user's breadcrumbs. + */ + MobileBreadcrumbs, + + /** + * The tab on mobile that displays the dialpad. + */ + MobileDialpad, + + /** + * The Favourites tab on mobile that lists your favourite people/rooms. + */ + MobileFavourites, + + /** + * The screen shown to share a link to download the app. + */ + MobileInviteFriends, + + /** + * The People tab on mobile that lists all the DM rooms you have joined. + */ + MobilePeople, + + /** + * The Rooms tab on mobile that lists all the (non-direct) rooms you've + * joined. + */ + MobileRooms, + + /** + * The Files tab shown in the global search screen on Mobile. + */ + MobileSearchFiles, + + /** + * The Messages tab shown in the global search screen on Mobile. + */ + MobileSearchMessages, + + /** + * The People tab shown in the global search screen on Mobile. + */ + MobileSearchPeople, + + /** + * The Rooms tab shown in the global search screen on Mobile. + */ + MobileSearchRooms, + + /** + * The sidebar shown on mobile with spaces, settings etc. + */ + MobileSidebar, + + /** + * The screen shown to select which room directory you'd like to use. + */ + MobileSwitchDirectory, + + /** + * Legacy: The screen that shows all groups/communities you have joined. + */ MyGroups, + + /** + * The screen that displays the registration flow (when the user wants + * to create an account) + */ + Register, + + /** + * The screen that displays the messages and events received in a room. + */ Room, + + /** + * The screen shown when tapping the name of a room from the Room + * screen. + */ + RoomDetails, + + /** + * The screen that lists public rooms for you to discover. + */ RoomDirectory, + + /** + * The screen that lists all the user's rooms and let them filter the + * rooms. + */ + RoomFilter, + + /** + * The screen that displays the list of members that are part of a room. + */ + RoomMembers, + + /** + * The notifications settings screen shown from the Room Details screen. + */ + RoomNotifications, + + /** + * The screen that allows you to search for messages/files in a specific + * room. + */ + RoomSearch, + + /** + * The settings screen shown from the Room Details screen. + */ + RoomSettings, + + /** + * The screen that allows you to see all of the files sent in a specific + * room. + */ + RoomUploads, + + /** + * The global settings screen shown in the app. + */ + Settings, + + /** + * The settings screen to change the default notification options. + */ + SettingsDefaultNotifications, + + /** + * The settings screen to manage notification mentions and keywords. + */ + SettingsMentionsAndKeywords, + + /** + * The global security settings screen. + */ + SettingsSecurity, + + /** + * The screen shown to create a new direct room. + */ + StartChat, + + /** + * A screen that shows information about a room member. + */ User, + + /** + * ? + */ WebCompleteSecurity, + + /** + * ? + */ WebE2ESetup, - WebForgotPassword, + + /** + * ? + */ WebLoading, - WebLogin, - WebRegister, + + /** + * ? + */ WebSoftLogout, - WebWelcome, + + /** + * The splash screen. + */ + Welcome, } override fun getName() = screenName.name diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/UnauthenticatedError.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/UnauthenticatedError.kt new file mode 100644 index 0000000000..56ef4af4be --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/UnauthenticatedError.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.analytics.plan + +import im.vector.app.features.analytics.itf.VectorAnalyticsEvent + +// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT +// https://github.com/matrix-org/matrix-analytics-events/ + +/** + * Triggered when the user becomes unauthenticated without actually clicking + * sign out(E.g. Due to expiry of an access token without a way to refresh). + */ +data class UnauthenticatedError( + /** + * The error code as defined in matrix spec. The source of this error is + * from the homeserver. + */ + val errorCode: ErrorCode, + /** + * The reason for the error. The source of this error is from the + * homeserver, the reason can vary and is subject to change so there is + * no enum of possible values. + */ + val errorReason: String, + /** + * Whether the auth mechanism is refresh-token-based. + */ + val refreshTokenAuth: Boolean, + /** + * Whether a soft logout or hard logout was triggered. + */ + val softLogout: Boolean, +) : VectorAnalyticsEvent { + + enum class ErrorCode { + M_FORBIDDEN, + M_UNKNOWN, + M_UNKNOWN_TOKEN, + } + + override fun getName() = "UnauthenticatedError" + + override fun getProperties(): Map? { + return mutableMapOf().apply { + put("errorCode", errorCode.name) + put("errorReason", errorReason) + put("refreshTokenAuth", refreshTokenAuth) + put("softLogout", softLogout) + }.takeIf { it.isNotEmpty() } + } +} diff --git a/vector/src/main/java/im/vector/app/features/analytics/screen/ScreenEvent.kt b/vector/src/main/java/im/vector/app/features/analytics/screen/ScreenEvent.kt new file mode 100644 index 0000000000..8e0513f25a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/screen/ScreenEvent.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.analytics.screen + +import android.os.SystemClock +import im.vector.app.features.analytics.AnalyticsTracker +import im.vector.app.features.analytics.plan.Screen +import timber.log.Timber + +/** + * Track a screen display. Unique usage. + */ +class ScreenEvent(val screenName: Screen.ScreenName) { + private val startTime = SystemClock.elapsedRealtime() + + // Protection to avoid multiple sending + private var isSent = false + + /** + * @param screenNameOverride can be used to override the screen name passed in constructor parameter + */ + fun send(analyticsTracker: AnalyticsTracker, + screenNameOverride: Screen.ScreenName? = null) { + if (isSent) { + Timber.w("Event $screenName Already sent!") + return + } + isSent = true + analyticsTracker.screen( + Screen( + screenName = screenNameOverride ?: screenName, + durationMs = (SystemClock.elapsedRealtime() - startTime).toInt() + ) + ) + } +} diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt index 683c5908ba..a15bd52174 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt @@ -37,6 +37,7 @@ import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.core.epoxy.onClick import im.vector.app.core.utils.PERMISSIONS_EMPTY +import im.vector.app.core.utils.PERMISSIONS_FOR_LOCATION_SHARING import im.vector.app.core.utils.PERMISSIONS_FOR_PICKING_CONTACT import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.app.databinding.ViewAttachmentTypeSelectorBinding @@ -71,6 +72,7 @@ class AttachmentTypeSelectorView(context: Context, views.attachmentStickersButton.configure(Type.STICKER) views.attachmentContactButton.configure(Type.CONTACT) views.attachmentPollButton.configure(Type.POLL) + views.attachmentLocationButton.configure(Type.LOCATION) width = LinearLayout.LayoutParams.MATCH_PARENT height = LinearLayout.LayoutParams.WRAP_CONTENT animationStyle = 0 @@ -123,12 +125,13 @@ class AttachmentTypeSelectorView(context: Context, fun setAttachmentVisibility(type: Type, isVisible: Boolean) { when (type) { - Type.CAMERA -> views.attachmentCameraButton - Type.GALLERY -> views.attachmentGalleryButton - Type.FILE -> views.attachmentFileButton - Type.STICKER -> views.attachmentStickersButton - Type.CONTACT -> views.attachmentContactButton - Type.POLL -> views.attachmentPollButton + Type.CAMERA -> views.attachmentCameraButton + Type.GALLERY -> views.attachmentGalleryButton + Type.FILE -> views.attachmentFileButton + Type.STICKER -> views.attachmentStickersButton + Type.CONTACT -> views.attachmentContactButton + Type.POLL -> views.attachmentPollButton + Type.LOCATION -> views.attachmentLocationButton }.let { it.isVisible = isVisible } @@ -211,6 +214,7 @@ class AttachmentTypeSelectorView(context: Context, FILE(PERMISSIONS_EMPTY, R.string.tooltip_attachment_file), STICKER(PERMISSIONS_EMPTY, R.string.tooltip_attachment_sticker), CONTACT(PERMISSIONS_FOR_PICKING_CONTACT, R.string.tooltip_attachment_contact), - POLL(PERMISSIONS_EMPTY, R.string.tooltip_attachment_poll) + POLL(PERMISSIONS_EMPTY, R.string.tooltip_attachment_poll), + LOCATION(PERMISSIONS_FOR_LOCATION_SHARING, R.string.tooltip_attachment_location) } } diff --git a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewActivity.kt b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewActivity.kt index a52036011f..7ddba0d229 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewActivity.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewActivity.kt @@ -19,17 +19,15 @@ package im.vector.app.features.attachments.preview import android.content.Context import android.content.Intent -import com.google.android.material.appbar.MaterialToolbar import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.addFragment -import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleBinding import im.vector.app.features.themes.ActivityOtherThemes import org.matrix.android.sdk.api.session.content.ContentAttachmentData @AndroidEntryPoint -class AttachmentsPreviewActivity : VectorBaseActivity(), ToolbarConfigurable { +class AttachmentsPreviewActivity : VectorBaseActivity() { companion object { private const val EXTRA_FRAGMENT_ARGS = "EXTRA_FRAGMENT_ARGS" @@ -72,8 +70,4 @@ class AttachmentsPreviewActivity : VectorBaseActivity(), setResult(RESULT_OK, resultIntent) finish() } - - override fun configure(toolbar: MaterialToolbar) { - configureToolbar(toolbar) - } } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index 458eff31bc..0338a66a5e 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 786e1655d6..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 @@ -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 57f228e75d..421c83c9fe 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 @@ -42,7 +42,7 @@ enum class Command(val command: String, JOIN_ROOM("/join", arrayOf("/j", "/goto"), " [reason]", R.string.command_description_join_room, false, false), PART("/part", null, "[]", R.string.command_description_part_room, false, false), TOPIC("/topic", null, "", R.string.command_description_topic, false, false), - REMOVE_USER("/remove", arrayOf("/kick"), " [reason]", R.string.command_description_kick_user, false, false), + REMOVE_USER("/remove", arrayOf("/kick"), " [reason]", R.string.command_description_remove_user, false, false), CHANGE_DISPLAY_NAME("/nick", null, "", R.string.command_description_nick, false, false), CHANGE_DISPLAY_NAME_FOR_ROOM("/myroomnick", arrayOf("/roomnick"), "", R.string.command_description_nick_for_room, false, false), ROOM_AVATAR("/roomavatar", null, "", R.string.command_description_room_avatar, true /* User has to know the mxc url */, false), 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 2017e2d877..7641a22922 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. @@ -34,11 +35,9 @@ object CommandParser { */ fun parseSlashCommand(textMessage: CharSequence, isInThreadTimeline: Boolean): ParsedCommand { // check if it has the Slash marker - if (!textMessage.startsWith("/")) { - return ParsedCommand.ErrorNotACommand + return if (!textMessage.startsWith("/")) { + ParsedCommand.ErrorNotACommand } else { - Timber.v("parseSlashCommand") - // "/" 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, "## manageSlashCommand() : split failed") + Timber.e(e, "## parseSlashCommand() : split failed") null } @@ -75,7 +74,7 @@ object CommandParser { } } - return when { + when { Command.PLAIN.matches(slashCommand) -> { if (message.isNotEmpty()) { ParsedCommand.SendPlainText(message = message) 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 4de873a1ac..611d11a222 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,53 +22,53 @@ 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 class ErrorCommandNotSupportedInThreads(val slashCommand: String) : 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 RemoveUser(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/verification/cancel/VerificationCancelController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationCancelController.kt index f0909dbc26..1adafe2760 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationCancelController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationCancelController.kt @@ -20,14 +20,14 @@ import androidx.core.text.toSpannable import com.airbnb.epoxy.EpoxyController import im.vector.app.R import im.vector.app.core.epoxy.bottomSheetDividerItem -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.colorizeMatchingText import im.vector.app.features.crypto.verification.VerificationBottomSheetViewState import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationActionItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import javax.inject.Inject class VerificationCancelController @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationNotMeController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationNotMeController.kt index 34f97d3cb2..a7c987f97e 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationNotMeController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/cancel/VerificationNotMeController.kt @@ -19,13 +19,13 @@ package im.vector.app.features.crypto.verification.cancel import com.airbnb.epoxy.EpoxyController import im.vector.app.R import im.vector.app.core.epoxy.bottomSheetDividerItem -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.features.crypto.verification.VerificationBottomSheetViewState import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationActionItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem import im.vector.app.features.html.EventHtmlRenderer +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import javax.inject.Inject class VerificationNotMeController @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodController.kt index 352c21a156..acc8cf61b9 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodController.kt @@ -19,12 +19,12 @@ package im.vector.app.features.crypto.verification.choose import com.airbnb.epoxy.EpoxyController import im.vector.app.R import im.vector.app.core.epoxy.bottomSheetDividerItem -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationActionItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationQrCodeItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import javax.inject.Inject class VerificationChooseMethodController @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionController.kt index 1314fd6fec..1d6dfbd947 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/conclusion/VerificationConclusionController.kt @@ -19,13 +19,13 @@ package im.vector.app.features.crypto.verification.conclusion import com.airbnb.epoxy.EpoxyController import im.vector.app.R import im.vector.app.core.epoxy.bottomSheetDividerItem -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationActionItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationBigImageItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem import im.vector.app.features.html.EventHtmlRenderer +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeController.kt index 838f25ddfa..9f3e8ff690 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeController.kt @@ -21,7 +21,6 @@ import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Success import im.vector.app.R import im.vector.app.core.epoxy.bottomSheetDividerItem -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.epoxy.errorWithRetryItem import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.resources.ColorProvider @@ -32,6 +31,7 @@ import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationE import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationWaitingItem import im.vector.app.features.displayname.getBestName +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import javax.inject.Inject class VerificationEmojiCodeController @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationNoticeItem.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationNoticeItem.kt index f63459991b..ecd9989cdc 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationNoticeItem.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/epoxy/BottomSheetVerificationNoticeItem.kt @@ -22,7 +22,7 @@ import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence /** * A action for bottom sheet. diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQRWaitingController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQRWaitingController.kt index cef5994c38..8de5f94ec9 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQRWaitingController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQRWaitingController.kt @@ -18,12 +18,12 @@ package im.vector.app.features.crypto.verification.qrconfirmation import com.airbnb.epoxy.EpoxyController import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationBigImageItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationWaitingItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherController.kt index bb21a6ccef..38f29622d0 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherController.kt @@ -19,7 +19,6 @@ package im.vector.app.features.crypto.verification.qrconfirmation import com.airbnb.epoxy.EpoxyController import im.vector.app.R import im.vector.app.core.epoxy.bottomSheetDividerItem -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.features.crypto.verification.VerificationBottomSheetViewState @@ -27,6 +26,7 @@ import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationA import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationBigImageItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem import im.vector.app.features.displayname.getBestName +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt index d40ee1f5c9..90997830a0 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/request/VerificationRequestController.kt @@ -23,7 +23,6 @@ import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import im.vector.app.R import im.vector.app.core.epoxy.bottomSheetDividerItem -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.colorizeMatchingText @@ -33,6 +32,7 @@ import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationA import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationWaitingItem import im.vector.app.features.displayname.getBestName +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import javax.inject.Inject class VerificationRequestController @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolSendFormController.kt b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolSendFormController.kt index f0a6f40208..573ec0c085 100644 --- a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolSendFormController.kt +++ b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolSendFormController.kt @@ -18,11 +18,11 @@ package im.vector.app.features.devtools import com.airbnb.epoxy.TypedEpoxyController import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.features.form.formEditTextItem import im.vector.app.features.form.formMultiLineEditTextItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import javax.inject.Inject class RoomDevToolSendFormController @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/devtools/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 d39b24bc47..b083b74c53 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/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/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index f866bb328d..58e36d2303 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -20,6 +20,7 @@ import android.net.Uri import android.view.View import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.features.call.conference.ConferenceEvent +import im.vector.app.features.location.LocationData import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent @@ -111,4 +112,7 @@ sealed class RoomDetailAction : VectorViewModelAction { // Poll data class EndPoll(val eventId: String) : RoomDetailAction() + + // Location + data class ShowLocation(val locationData: LocationData, val userId: String) : RoomDetailAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt index bc1e17f984..ae24052aa2 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.arguments.TimelineArgs import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker @@ -51,7 +52,6 @@ import javax.inject.Inject @AndroidEntryPoint class RoomDetailActivity : VectorBaseActivity(), - ToolbarConfigurable, MatrixToBottomSheet.InteractionListener { override fun getBinding(): ActivityRoomDetailBinding { @@ -157,11 +157,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/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt index 86240a5ffe..b0921e01f9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt @@ -20,6 +20,7 @@ import android.net.Uri import android.view.View import im.vector.app.core.platform.VectorViewEvents import im.vector.app.features.call.webrtc.WebRtcCall +import im.vector.app.features.location.LocationData import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode @@ -82,4 +83,6 @@ sealed class RoomDetailViewEvents : VectorViewEvents { data class StartChatEffect(val type: ChatEffect) : RoomDetailViewEvents() object StopChatEffects : RoomDetailViewEvents() object RoomReplacementStarted : RoomDetailViewEvents() + + data class ShowLocation(val locationData: LocationData, val userId: String) : RoomDetailViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt index 0f6b9a266e..c63085f647 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 @@ -69,6 +69,7 @@ data class RoomDetailViewState( val isAllowedToSetupEncryption: Boolean = true, val hasFailedSending: Boolean = false, val jitsiState: JitsiState = JitsiState(), + val switchToParentSpace: Boolean = false, val rootThreadEventId: String? = null, val threadNotificationBadgeState: ThreadNotificationBadgeState = ThreadNotificationBadgeState() ) : MavericksState { @@ -78,6 +79,7 @@ data class RoomDetailViewState( eventId = args.eventId, // Also highlight the target event, if any highlightedEventId = args.eventId, + switchToParentSpace = args.switchToParentSpace, rootThreadEventId = args.threadTimelineArgs?.rootThreadEventId ) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 34afe81c3e..4f3d93e0ac 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -107,6 +107,7 @@ import im.vector.app.core.utils.createUIHandler import im.vector.app.core.utils.isValidUrl import im.vector.app.core.utils.onPermissionDeniedDialog import im.vector.app.core.utils.onPermissionDeniedSnackbar +import im.vector.app.core.utils.openLocation import im.vector.app.core.utils.openUrlInExternalBrowser import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.core.utils.safeStartActivity @@ -117,6 +118,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.FragmentTimelineBinding +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 @@ -171,12 +174,15 @@ import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.PillImageSpan import im.vector.app.features.html.PillsPostProcessor import im.vector.app.features.invite.VectorInviteView +import im.vector.app.features.location.LocationData +import im.vector.app.features.location.LocationSharingMode import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.VideoContentRenderer import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.permalink.NavigationInterceptor import im.vector.app.features.permalink.PermalinkHandler +import im.vector.app.features.poll.create.PollMode import im.vector.app.features.reactions.EmojiReactionPickerActivity import im.vector.app.features.roomprofile.RoomProfileActivity import im.vector.app.features.session.coroutineScope @@ -203,6 +209,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 @@ -210,6 +217,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageFormat import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent +import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent @@ -332,6 +340,7 @@ class TimelineFragment @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 -> timelineViewModel.handle(RoomDetailAction.RoomUpgradeSuccess(replacementRoomId)) @@ -359,6 +368,7 @@ class TimelineFragment @Inject constructor( keyboardStateUtils = KeyboardStateUtils(requireActivity()) lazyLoadedViews.bind(views) setupToolbar(views.roomToolbar) + .allowBack() setupRecyclerView() setupComposer() setupNotificationView() @@ -468,6 +478,7 @@ class TimelineFragment @Inject constructor( RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects() is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it) RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement() + is RoomDetailViewEvents.ShowLocation -> handleShowLocationPreview(it) }.exhaustive } @@ -599,6 +610,17 @@ class TimelineFragment @Inject constructor( } } + private fun handleShowLocationPreview(viewEvent: RoomDetailViewEvents.ShowLocation) { + navigator + .openLocationSharing( + context = requireContext(), + roomId = roomDetailArgs.roomId, + mode = LocationSharingMode.PREVIEW, + initialLocationData = viewEvent.locationData, + locationOwnerId = viewEvent.userId + ) + } + private fun requestNativeWidgetPermission(it: RoomDetailViewEvents.RequestNativeWidgetPermission) { val tag = RoomWidgetPermissionBottomSheet::class.java.name val dFrag = childFragmentManager.findFragmentByTag(tag) as? RoomWidgetPermissionBottomSheet @@ -673,7 +695,7 @@ class TimelineFragment @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() } } @@ -1447,8 +1469,8 @@ class TimelineFragment @Inject constructor( if (!::attachmentTypeSelector.isInitialized) { attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@TimelineFragment) attachmentTypeSelector.setAttachmentVisibility( - AttachmentTypeSelectorView.Type.POLL, - vectorPreferences.labsEnablePolls() && !isThreadTimeLine()) + AttachmentTypeSelectorView.Type.LOCATION, + vectorPreferences.isLocationSharingEnabled() && !isThreadTimeLine()) } attachmentTypeSelector.show(views.composerLayout.views.attachmentButton) } @@ -1477,6 +1499,7 @@ class TimelineFragment @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 @@ -1606,7 +1629,7 @@ class TimelineFragment @Inject constructor( views.includeRoomToolbar.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)) @@ -2041,16 +2064,22 @@ class TimelineFragment @Inject constructor( } private fun onShareActionClicked(action: EventSharedAction.Share) { - if (action.messageContent is MessageTextContent) { - shareText(requireContext(), action.messageContent.body) - } else if (action.messageContent is MessageWithAttachmentContent) { - lifecycleScope.launch { - val result = runCatching { session.fileService().downloadFile(messageContent = action.messageContent) } - if (!isAdded) return@launch - result.fold( - { shareMedia(requireContext(), it, getMimeTypeFromUri(requireContext(), it.toUri())) }, - { showErrorInSnackbar(it) } - ) + when (action.messageContent) { + is MessageTextContent -> shareText(requireContext(), action.messageContent.body) + is MessageLocationContent -> { + LocationData.create(action.messageContent.getUri())?.let { + openLocation(requireActivity(), it.latitude, it.longitude) + } + } + is MessageWithAttachmentContent -> { + lifecycleScope.launch { + val result = runCatching { session.fileService().downloadFile(messageContent = action.messageContent) } + if (!isAdded) return@launch + result.fold( + { shareMedia(requireContext(), it, getMimeTypeFromUri(requireContext(), it.toUri())) }, + { showErrorInSnackbar(it) } + ) + } } } } @@ -2142,7 +2171,9 @@ class TimelineFragment @Inject constructor( timelineViewModel.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) @@ -2391,17 +2422,27 @@ class TimelineFragment @Inject constructor( private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) { when (type) { - AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera( + AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera( activity = requireActivity(), vectorPreferences = vectorPreferences, cameraActivityResultLauncher = attachmentCameraActivityResultLauncher, cameraVideoActivityResultLauncher = attachmentCameraVideoActivityResultLauncher ) - AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher) - AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(attachmentMediaActivityResultLauncher) - AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact(attachmentContactActivityResultLauncher) - AttachmentTypeSelectorView.Type.STICKER -> timelineViewModel.handle(RoomDetailAction.SelectStickerAttachment) - AttachmentTypeSelectorView.Type.POLL -> navigator.openCreatePoll(requireContext(), timelineArgs.roomId) + AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher) + AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(attachmentMediaActivityResultLauncher) + AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact(attachmentContactActivityResultLauncher) + AttachmentTypeSelectorView.Type.STICKER -> timelineViewModel.handle(RoomDetailAction.SelectStickerAttachment) + AttachmentTypeSelectorView.Type.POLL -> navigator.openCreatePoll(requireContext(), timelineArgs.roomId, null, PollMode.CREATE) + AttachmentTypeSelectorView.Type.LOCATION -> { + navigator + .openLocationSharing( + context = requireContext(), + roomId = roomDetailArgs.roomId, + mode = LocationSharingMode.STATIC_SHARING, + initialLocationData = null, + locationOwnerId = session.myUserId + ) + } }.exhaustive } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index f8d7fe7628..5f5b5fe5ac 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -28,6 +28,7 @@ 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 @@ -37,7 +38,9 @@ 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 @@ -50,10 +53,12 @@ import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandle import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.typing.TypingHelper +import im.vector.app.features.location.LocationData import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorDataStore import im.vector.app.features.settings.VectorPreferences +import im.vector.app.space import im.vector.lib.core.utils.flow.chunk import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow @@ -116,9 +121,11 @@ class TimelineViewModel @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 { @@ -184,6 +191,24 @@ class TimelineViewModel @AssistedInject constructor( 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) + } + } + } + // Threads initThreads() } @@ -407,9 +432,14 @@ class TimelineViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.OpenRoom(action.replacementRoomId, closeCurrentRoom = true)) } is RoomDetailAction.EndPoll -> handleEndPoll(action.eventId) + is RoomDetailAction.ShowLocation -> handleShowLocation(action.locationData, action.userId) }.exhaustive } + private fun handleShowLocation(locationData: LocationData, userId: String) { + _viewEvents.post(RoomDetailViewEvents.ShowLocation(locationData, userId)) + } + private fun handleJitsiCallJoinStatus(action: RoomDetailAction.UpdateJoinJitsiCallStatus) = withState { state -> if (state.jitsiState.confId == null) { // If jitsi widget is removed while on the call @@ -775,7 +805,10 @@ class TimelineViewModel @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/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt index 766417c320..c2b89a4246 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 @@ -27,6 +27,8 @@ 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.resources.UserPreferencesProvider +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 @@ -70,8 +72,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) { @@ -187,7 +191,7 @@ class MessageComposerViewModel @AssistedInject constructor( withState { state -> when (state.sendMode) { is SendMode.Regular -> { - when (val slashCommandResult = CommandParser.parseSlashCommand( + when (val slashCommandResult = commandParser.parseSlashCommand( textMessage = action.text, isInThreadTimeline = state.isInThreadTimeline())) { is ParsedCommand.ErrorNotACommand -> { @@ -598,6 +602,7 @@ class MessageComposerViewModel @AssistedInject constructor( return@launch } session.getRoomSummary(command.roomAlias) + ?.also { analyticsTracker.capture(it.toAnalyticsJoinedRoom()) } ?.roomId ?.let { _viewEvents.post(MessageComposerViewEvents.JoinRoomCommandSuccess(it)) 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 f966cd251e..2cdc1a0d90 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,13 +26,13 @@ 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.resources.UserPreferencesProvider 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 7e83cbc174..7ed1a66ad6 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 @@ -26,12 +26,12 @@ 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 org.matrix.android.sdk.api.session.threads.ThreadDetails +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 b6b985632c..8851f187e0 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, @@ -194,6 +195,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 @@ -201,7 +204,9 @@ 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( timelineEvent = it, highlightedEventId = partialState.highlightedEventId, @@ -209,7 +214,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec rootThreadEventId = partialState.rootThreadEventId ) } - if (prevDisplayableEventIndex != -1 && currentSnapshot[prevDisplayableEventIndex].senderInfo.userId == invalidatedSenderId) { + if (prevDisplayableEventIndex != -1 && currentSnapshot.getOrNull(prevDisplayableEventIndex)?.senderInfo?.userId == invalidatedSenderId) { modelCache[prevDisplayableEventIndex] = null } requestModelBuild() @@ -219,6 +224,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() @@ -228,6 +235,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) } @@ -238,6 +247,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) } @@ -321,6 +332,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 cc2f8a2ac5..048a4754f5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt @@ -39,7 +39,7 @@ sealed class EventSharedAction(@StringRes val titleRes: Int, data class Copy(val content: String) : EventSharedAction(R.string.action_copy, R.drawable.ic_copy) - data class Edit(val eventId: String) : + data class Edit(val eventId: String, val eventType: String) : EventSharedAction(R.string.edit, R.drawable.ic_edit) data class Quote(val eventId: String) : diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt index 15f33db6ff..1ff9679479 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt @@ -27,21 +27,25 @@ import im.vector.app.core.epoxy.bottomsheet.bottomSheetActionItem import im.vector.app.core.epoxy.bottomsheet.bottomSheetMessagePreviewItem import im.vector.app.core.epoxy.bottomsheet.bottomSheetQuickReactionsItem import im.vector.app.core.epoxy.bottomsheet.bottomSheetSendStateItem -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.DimensionConverter import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.format.EventDetailsFormatter +import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider import im.vector.app.features.home.room.detail.timeline.image.buildImageContentRendererData import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod import im.vector.app.features.home.room.detail.timeline.tools.linkify import im.vector.app.features.html.SpanUtils +import im.vector.app.features.location.LocationData import im.vector.app.features.media.ImageContentRenderer +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent import org.matrix.android.sdk.api.session.room.send.SendState import javax.inject.Inject @@ -57,7 +61,8 @@ class MessageActionsEpoxyController @Inject constructor( private val errorFormatter: ErrorFormatter, private val spanUtils: SpanUtils, private val eventDetailsFormatter: EventDetailsFormatter, - private val dateFormatter: VectorDateFormatter + private val dateFormatter: VectorDateFormatter, + private val locationPinProvider: LocationPinProvider ) : TypedEpoxyController() { var listener: MessageActionsEpoxyControllerListener? = null @@ -69,6 +74,9 @@ class MessageActionsEpoxyController @Inject constructor( val formattedDate = dateFormatter.format(date, DateFormatKind.MESSAGE_DETAIL) val body = state.messageBody.linkify(host.listener) val bindingOptions = spanUtils.getBindingOptions(body) + val locationData = state.timelineEvent()?.root?.getClearContent()?.toModel(catchError = true)?.let { + LocationData.create(it.getUri()) + } bottomSheetMessagePreviewItem { id("preview") avatarRenderer(host.avatarRenderer) @@ -81,6 +89,8 @@ class MessageActionsEpoxyController @Inject constructor( body(body.toEpoxyCharSequence()) bodyDetails(host.eventDetailsFormatter.format(state.timelineEvent()?.root)?.toEpoxyCharSequence()) time(formattedDate) + locationData(locationData) + locationPinProvider(host.locationPinProvider) } // Send state diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 339fcf13a7..745cb0c731 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 @@ -287,7 +287,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 @@ -340,7 +340,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)) { @@ -435,8 +435,9 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted MessageType.MSGTYPE_VIDEO, MessageType.MSGTYPE_AUDIO, MessageType.MSGTYPE_FILE, - MessageType.MSGTYPE_POLL_START -> true - else -> false + MessageType.MSGTYPE_POLL_START, + MessageType.MSGTYPE_LOCATION -> true + else -> false } } @@ -530,14 +531,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) ) } @@ -580,4 +582,10 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted canRedact(event, actionPermissions) && event.annotations?.pollResponseSummary?.closedTime == null } + + private fun canEditPoll(event: TimelineEvent): Boolean { + return event.root.getClearType() == EventType.POLL_START && + event.annotations?.pollResponseSummary?.closedTime == null && + event.annotations?.pollResponseSummary?.aggregatedContent?.totalVotes ?: 0 == 0 + } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt index 19b6b8c71a..1dad6cc4a7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt @@ -23,7 +23,6 @@ import com.airbnb.mvrx.Success import im.vector.app.R import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.VectorDateFormatter -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem @@ -31,6 +30,7 @@ import im.vector.app.core.ui.list.genericHeaderItem import im.vector.app.core.ui.list.genericItem import im.vector.app.core.ui.list.genericLoaderItem import im.vector.app.features.html.EventHtmlRenderer +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.span import name.fraser.neil.plaintext.diff_match_patch import org.matrix.android.sdk.api.session.events.model.Event diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt index f1ffc77a36..413ceb6380 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt @@ -18,7 +18,6 @@ package im.vector.app.features.home.room.detail.timeline.factory import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider import im.vector.app.core.resources.StringProvider @@ -28,6 +27,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttrib import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem_ import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod import im.vector.app.features.settings.VectorPreferences +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.image import me.gujun.android.span.span import org.matrix.android.sdk.api.session.crypto.MXCryptoError diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 62a86813eb..b0c9489bf9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -28,16 +28,17 @@ import dagger.Lazy import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.files.LocalFilesHelper import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.containsOnlyEmojis +import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder +import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider @@ -50,6 +51,8 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData +import im.vector.app.features.home.room.detail.timeline.item.MessageLocationItem +import im.vector.app.features.home.room.detail.timeline.item.MessageLocationItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem @@ -68,8 +71,11 @@ import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.PillsPostProcessor import im.vector.app.features.html.SpanUtils import im.vector.app.features.html.VectorHtmlCompressor +import im.vector.app.features.location.LocationData import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.VideoContentRenderer +import im.vector.app.features.settings.VectorPreferences +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.span import org.commonmark.node.Document import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl @@ -83,12 +89,14 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithF import org.matrix.android.sdk.api.session.room.model.message.MessageEmoteContent import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent +import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent import org.matrix.android.sdk.api.session.room.model.message.MessageNoticeContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent +import org.matrix.android.sdk.api.session.room.model.message.PollType import org.matrix.android.sdk.api.session.room.model.message.getFileName import org.matrix.android.sdk.api.session.room.model.message.getFileUrl import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl @@ -117,7 +125,9 @@ class MessageItemFactory @Inject constructor( private val pillsPostProcessorFactory: PillsPostProcessor.Factory, private val spanUtils: SpanUtils, private val session: Session, - private val voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker) { + private val voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker, + private val locationPinProvider: LocationPinProvider, + private val vectorPreferences: VectorPreferences) { // TODO inject this properly? private var roomId: String = "" @@ -173,7 +183,14 @@ class MessageItemFactory @Inject constructor( } } is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessagePollContent -> buildPollContent(messageContent, informationData, highlight, callback, attributes) + is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes) + is MessageLocationContent -> { + if (vectorPreferences.labsRenderLocationsInTimeline()) { + buildLocationItem(messageContent, informationData, highlight, callback, attributes) + } else { + buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes) + } + } else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) } } @@ -181,11 +198,38 @@ class MessageItemFactory @Inject constructor( private fun isFromThreadTimeline(params: TimelineItemFactoryParams) { params.rootThreadEventId } - private fun buildPollContent(pollContent: MessagePollContent, - informationData: MessageInformationData, - highlight: Boolean, - callback: TimelineEventController.Callback?, - attributes: AbsMessageItem.Attributes): PollItem? { + + private fun buildLocationItem(locationContent: MessageLocationContent, + informationData: MessageInformationData, + highlight: Boolean, + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes): MessageLocationItem? { + val geoUri = locationContent.getUri() + val locationData = LocationData.create(geoUri) + + val mapCallback: MessageLocationItem.Callback = object : MessageLocationItem.Callback { + override fun onMapClicked() { + locationData?.let { + callback?.onTimelineItemAction(RoomDetailAction.ShowLocation(it, informationData.senderId)) + } + } + } + + return MessageLocationItem_() + .attributes(attributes) + .locationData(locationData) + .userId(informationData.senderId) + .locationPinProvider(locationPinProvider) + .highlighted(highlight) + .leftGuideline(avatarSizeProvider.leftGuideline) + .callback(mapCallback) + } + + private fun buildPollItem(pollContent: MessagePollContent, + informationData: MessageInformationData, + highlight: Boolean, + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes): PollItem? { val optionViewStates = mutableListOf() val pollResponseSummary = informationData.pollResponseAggregatedSummary @@ -193,11 +237,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) + } } } @@ -217,6 +268,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) @@ -227,13 +281,22 @@ class MessageItemFactory @Inject constructor( ) } + val question = pollContent.pollCreationInfo?.question?.question ?: "" + return PollItem_() .attributes(attributes) .eventId(informationData.eventId) - .pollQuestion(pollContent.pollCreationInfo?.question?.question ?: "") + .pollQuestion( + if (informationData.hasBeenEdited) { + annotateWithEdited(question, callback, informationData) + } else { + question + }.toEpoxyCharSequence() + ) .pollSent(isPollSent) .totalVotesText(totalVotesText) .optionViewStates(optionViewStates) + .edited(informationData.hasBeenEdited) .highlighted(highlight) .leftGuideline(avatarSizeProvider.leftGuideline) .callback(callback) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt index 70c8fa7574..6951c3c316 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt @@ -16,13 +16,13 @@ package im.vector.app.features.home.room.detail.timeline.factory -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.format.NoticeEventFormatter import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.app.features.home.room.detail.timeline.item.NoticeItem import im.vector.app.features.home.room.detail.timeline.item.NoticeItem_ +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence import org.matrix.android.sdk.api.extensions.orFalse import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt index 3a3d269058..fff709f346 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt @@ -18,10 +18,10 @@ package im.vector.app.features.home.room.detail.timeline.factory import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.features.home.room.detail.timeline.item.RoomCreateItem_ +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.span import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.toModel diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt index 3892bfff85..d5f3a74e4e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt @@ -89,6 +89,9 @@ class DisplayableEventFormatter @Inject constructor( MessageType.MSGTYPE_FILE -> { simpleFormat(senderName, stringProvider.getString(R.string.sent_a_file), appendAuthor) } + MessageType.MSGTYPE_LOCATION -> { + simpleFormat(senderName, stringProvider.getString(R.string.sent_location), appendAuthor) + } else -> { simpleFormat(senderName, messageContent.body, appendAuthor) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index d39b8aec5e..ae541217bf 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -770,12 +770,12 @@ class NoticeEventFormatter @Inject constructor( Membership.JOIN -> if (event.isSentByCurrentUser()) { eventContent.safeReason?.let { reason -> - sp.getString(R.string.notice_room_kick_with_reason_by_you, targetDisplayName, reason) - } ?: sp.getString(R.string.notice_room_kick_by_you, targetDisplayName) + sp.getString(R.string.notice_room_remove_with_reason_by_you, targetDisplayName, reason) + } ?: sp.getString(R.string.notice_room_remove_by_you, targetDisplayName) } else { eventContent.safeReason?.let { reason -> - sp.getString(R.string.notice_room_kick_with_reason, senderDisplayName, targetDisplayName, reason) - } ?: sp.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName) + sp.getString(R.string.notice_room_remove_with_reason, senderDisplayName, targetDisplayName, reason) + } ?: sp.getString(R.string.notice_room_remove, senderDisplayName, targetDisplayName) } Membership.BAN -> if (event.isSentByCurrentUser()) { @@ -803,12 +803,12 @@ class NoticeEventFormatter @Inject constructor( Membership.KNOCK -> if (event.isSentByCurrentUser()) { eventContent.safeReason?.let { reason -> - sp.getString(R.string.notice_room_kick_with_reason_by_you, targetDisplayName, reason) - } ?: sp.getString(R.string.notice_room_kick_by_you, targetDisplayName) + sp.getString(R.string.notice_room_remove_with_reason_by_you, targetDisplayName, reason) + } ?: sp.getString(R.string.notice_room_remove_by_you, targetDisplayName) } else { eventContent.safeReason?.let { reason -> - sp.getString(R.string.notice_room_kick_with_reason, senderDisplayName, targetDisplayName, reason) - } ?: sp.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName) + sp.getString(R.string.notice_room_remove_with_reason, senderDisplayName, targetDisplayName, reason) + } ?: sp.getString(R.string.notice_room_remove, senderDisplayName, targetDisplayName) } else -> null } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/LocationPinProvider.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/LocationPinProvider.kt new file mode 100644 index 0000000000..fe3a7d9007 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/LocationPinProvider.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.timeline.helper + +import android.content.Context +import android.graphics.drawable.Drawable +import android.graphics.drawable.LayerDrawable +import androidx.core.content.ContextCompat +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.transition.Transition +import im.vector.app.R +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.glide.GlideApp +import im.vector.app.core.utils.DimensionConverter +import im.vector.app.features.home.AvatarRenderer +import org.matrix.android.sdk.api.util.toMatrixItem +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class LocationPinProvider @Inject constructor( + private val context: Context, + private val activeSessionHolder: ActiveSessionHolder, + private val dimensionConverter: DimensionConverter, + private val avatarRenderer: AvatarRenderer +) { + private val cache = mutableMapOf() + + private val glideRequests by lazy { + GlideApp.with(context) + } + + fun create(userId: String, callback: (Drawable) -> Unit) { + if (cache.contains(userId)) { + callback(cache[userId]!!) + return + } + + activeSessionHolder.getActiveSession().getUser(userId)?.toMatrixItem()?.let { + val size = dimensionConverter.dpToPx(44) + avatarRenderer.render(glideRequests, it, object : CustomTarget(size, size) { + override fun onResourceReady(resource: Drawable, transition: Transition?) { + val bgUserPin = ContextCompat.getDrawable(context, R.drawable.bg_map_user_pin)!! + val layerDrawable = LayerDrawable(arrayOf(bgUserPin, resource)) + val horizontalInset = dimensionConverter.dpToPx(4) + val topInset = dimensionConverter.dpToPx(4) + val bottomInset = dimensionConverter.dpToPx(8) + layerDrawable.setLayerInset(1, horizontalInset, topInset, horizontalInset, bottomInset) + + cache[userId] = layerDrawable + + callback(layerDrawable) + } + + override fun onLoadCleared(placeholder: Drawable?) { + // Is it possible? Put placeholder instead? + } + }) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt index 561f36ce41..ad422febe1 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 @@ -165,8 +165,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 } if (userPreferencesProvider.areThreadMessagesEnabled() && !isFromThreadTimeline && root.isThread()) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt index be9b727017..915ad6a17d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt @@ -20,9 +20,9 @@ import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence import me.saket.bettermovementmethod.BetterLinkMovementMethod @EpoxyModelClass(layout = R.layout.item_timeline_event_base) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt new file mode 100644 index 0000000000..3f030866a5 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.timeline.item + +import android.widget.FrameLayout +import androidx.constraintlayout.widget.ConstraintLayout +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.onClick +import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider +import im.vector.app.features.location.LocationData +import im.vector.app.features.location.MapTilerMapView + +@EpoxyModelClass(layout = R.layout.item_timeline_event_base) +abstract class MessageLocationItem : AbsMessageItem() { + + interface Callback { + fun onMapClicked() + } + + @EpoxyAttribute + var callback: Callback? = null + + @EpoxyAttribute + var locationData: LocationData? = null + + @EpoxyAttribute + var userId: String? = null + + @EpoxyAttribute + var locationPinProvider: LocationPinProvider? = null + + override fun bind(holder: Holder) { + super.bind(holder) + renderSendState(holder.mapViewContainer, null) + + val location = locationData ?: return + val locationOwnerId = userId ?: return + + holder.clickableMapArea.onClick { + callback?.onMapClicked() + } + + holder.mapView.apply { + initialize { + zoomToLocation(location.latitude, location.longitude, INITIAL_ZOOM) + + locationPinProvider?.create(locationOwnerId) { pinDrawable -> + addPinToMap(locationOwnerId, pinDrawable) + updatePinLocation(locationOwnerId, location.latitude, location.longitude) + } + } + } + } + + override fun getViewType() = STUB_ID + + class Holder : AbsMessageItem.Holder(STUB_ID) { + val mapViewContainer by bind(R.id.mapViewContainer) + val mapView by bind(R.id.mapView) + val clickableMapArea by bind(R.id.clickableMapArea) + } + + companion object { + private const val STUB_ID = R.id.messageContentLocationStub + private const val INITIAL_ZOOM = 15.0 + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt index e499f090fb..1794f04c2a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt @@ -25,7 +25,6 @@ import androidx.core.widget.TextViewCompat import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.onLongClickIgnoringLinks import im.vector.app.features.home.room.detail.timeline.TimelineEventController @@ -34,6 +33,7 @@ import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlUiState import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlView import im.vector.app.features.media.ImageContentRenderer +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence import io.noties.markwon.MarkwonPlugin import org.matrix.android.sdk.api.extensions.orFalse diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/NoticeItem.kt index 03ba828267..357139648d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/NoticeItem.kt @@ -23,11 +23,11 @@ import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.ClickListener -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence import im.vector.app.core.epoxy.onClick import im.vector.app.core.ui.views.ShieldImageView import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel @EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt index 1308fa49c8..b660ee9a59 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt @@ -24,12 +24,13 @@ import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence @EpoxyModelClass(layout = R.layout.item_timeline_event_base) abstract class PollItem : AbsMessageItem() { @EpoxyAttribute - var pollQuestion: String? = null + var pollQuestion: EpoxyCharSequence? = null @EpoxyAttribute var callback: TimelineEventController.Callback? = null @@ -43,6 +44,9 @@ abstract class PollItem : AbsMessageItem() { @EpoxyAttribute var totalVotesText: String? = null + @EpoxyAttribute + var edited: Boolean = false + @EpoxyAttribute lateinit var optionViewStates: List @@ -52,7 +56,7 @@ abstract class PollItem : AbsMessageItem() { renderSendState(holder.view, holder.questionTextView) - holder.questionTextView.text = pollQuestion + holder.questionTextView.text = pollQuestion?.charSequence holder.totalVotesTextView.text = totalVotesText while (holder.optionsContainer.childCount < optionViewStates.size) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt index 2af445041b..2be933d9c3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt @@ -23,6 +23,7 @@ import androidx.appcompat.content.res.AppCompatResources import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isVisible import im.vector.app.R +import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.setAttributeTintedImageResource import im.vector.app.databinding.ItemPollOptionBinding @@ -43,11 +44,12 @@ class PollOptionView @JvmOverloads constructor( views.optionNameTextView.text = state.optionAnswer when (state) { - is PollOptionViewState.PollSending -> renderPollSending() - is PollOptionViewState.PollEnded -> renderPollEnded(state) - is PollOptionViewState.PollReady -> renderPollReady() - is PollOptionViewState.PollVoted -> renderPollVoted(state) - } + is PollOptionViewState.PollSending -> renderPollSending() + is PollOptionViewState.PollEnded -> renderPollEnded(state) + is PollOptionViewState.PollReady -> renderPollReady() + is PollOptionViewState.PollVoted -> renderPollVoted(state) + is PollOptionViewState.PollUndisclosed -> renderPollUndisclosed(state) + }.exhaustive } private fun renderPollSending() { @@ -78,6 +80,12 @@ class PollOptionView @JvmOverloads constructor( renderVoteSelection(state.isSelected) } + private fun renderPollUndisclosed(state: PollOptionViewState.PollUndisclosed) { + views.optionCheckImageView.isVisible = true + views.optionWinnerImageView.isVisible = false + renderVoteSelection(state.isSelected) + } + private fun showVotes(voteCount: Int, votePercentage: Double) { views.optionVoteCountTextView.apply { isVisible = true diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionViewState.kt index 5291e7f20a..ae900d0406 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionViewState.kt @@ -51,4 +51,12 @@ sealed class PollOptionViewState(open val optionId: String, val votePercentage: Double, val isWinner: Boolean ) : PollOptionViewState(optionId, optionAnswer) + + /** + * Represent a poll that is undisclosed, votes will be hidden until the poll is ended. + */ + data class PollUndisclosed(override val optionId: String, + override val optionAnswer: String, + val isSelected: Boolean + ) : PollOptionViewState(optionId, optionAnswer) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/RoomCreateItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/RoomCreateItem.kt index 34edcfe9f4..a6d2bcc66d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/RoomCreateItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/RoomCreateItem.kt @@ -22,7 +22,7 @@ import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence import me.saket.bettermovementmethod.BetterLinkMovementMethod @EpoxyModelClass(layout = R.layout.item_timeline_event_create) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ReactionInfoSimpleItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ReactionInfoSimpleItem.kt index f6976d96bf..f150e13016 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ReactionInfoSimpleItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ReactionInfoSimpleItem.kt @@ -24,8 +24,8 @@ import com.airbnb.epoxy.EpoxyModelWithHolder import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence import im.vector.app.core.epoxy.onClick +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence /** * Item displaying an emoji reaction (single line with emoji, author, time) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsEpoxyController.kt index 17a3ac4a5f..10af3792d5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsEpoxyController.kt @@ -22,10 +22,10 @@ import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Success import im.vector.app.EmojiSpanify import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericLoaderItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import javax.inject.Inject /** diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsController.kt index 5c2ad3799b..b2da3bfc78 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsController.kt @@ -18,11 +18,11 @@ package im.vector.app.features.home.room.detail.widget import com.airbnb.epoxy.TypedEpoxyController import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericButtonItem import im.vector.app.core.ui.list.genericFooterItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.session.widgets.model.Widget import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomsActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomsActivity.kt index 5f8ccc794a..0e16b4b0df 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomsActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomsActivity.kt @@ -24,6 +24,7 @@ import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityFilteredRoomsBinding +import im.vector.app.features.analytics.plan.Screen import im.vector.app.features.home.RoomListDisplayMode import im.vector.app.features.home.room.list.RoomListFragment import im.vector.app.features.home.room.list.RoomListParams @@ -42,7 +43,9 @@ class FilteredRoomsActivity : VectorBaseActivity() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - configureToolbar(views.filteredRoomsToolbar) + analyticsScreenName = Screen.ScreenName.RoomFilter + setupToolbar(views.filteredRoomsToolbar) + .allowBack() if (isFirstCreation()) { val params = RoomListParams(RoomListDisplayMode.FILTERED) replaceFragment(views.filteredRoomsFragmentContainer, RoomListFragment::class.java, params, FRAGMENT_TAG) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt index 5171319a41..b6481c9cbb 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt @@ -42,6 +42,7 @@ import im.vector.app.core.platform.StateView import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.databinding.FragmentRoomListBinding +import im.vector.app.features.analytics.plan.Screen import im.vector.app.features.home.RoomListDisplayMode import im.vector.app.features.home.room.filtered.FilteredRoomFooterItem import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet @@ -100,6 +101,15 @@ class RoomListFragment @Inject constructor( private val adapterInfosList = mutableListOf() private var concatAdapter: ConcatAdapter? = null + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + analyticsScreenName = when (roomListParams.displayMode) { + RoomListDisplayMode.PEOPLE -> Screen.ScreenName.MobilePeople + RoomListDisplayMode.ROOMS -> Screen.ScreenName.MobileRooms + else -> null + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) views.stateView.contentView = views.roomListView diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt index 46a91e2c72..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 @@ -223,6 +226,7 @@ class RoomListViewModel @AssistedInject constructor( viewModelScope.launch { try { room.join() + analyticsTracker.capture(action.roomSummary.toAnalyticsJoinedRoom()) // We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data. // Instead, we wait for the room to be joined } catch (failure: Throwable) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt index 4261acd7ee..b037191ad1 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt @@ -30,7 +30,6 @@ import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.ui.views.PresenceStateImageView @@ -38,6 +37,7 @@ import im.vector.app.core.ui.views.ShieldImageView import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.themes.ThemeUtils +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.presence.model.UserPresence import org.matrix.android.sdk.api.util.MatrixItem diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt index ff57544a36..6326d9c97a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt @@ -23,12 +23,12 @@ import im.vector.app.R import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.resources.StringProvider import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter import im.vector.app.features.home.room.typing.TypingHelper +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomSummary diff --git a/vector/src/main/java/im/vector/app/features/invite/InviteButtonStateBinder.kt b/vector/src/main/java/im/vector/app/features/invite/InviteButtonStateBinder.kt index 2c8589eca1..43763cdf37 100644 --- a/vector/src/main/java/im/vector/app/features/invite/InviteButtonStateBinder.kt +++ b/vector/src/main/java/im/vector/app/features/invite/InviteButtonStateBinder.kt @@ -16,7 +16,7 @@ package im.vector.app.features.invite -import androidx.core.view.isInvisible +import androidx.core.view.isGone import im.vector.app.core.platform.ButtonStateView import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState @@ -38,11 +38,11 @@ object InviteButtonStateBinder { } // ButtonStateView.State.Loaded not used because roomSummary will not be displayed as a room invitation anymore - rejectView.isInvisible = requestInProgress + rejectView.isGone = requestInProgress - when { - changeMembershipState is ChangeMembershipState.FailedLeaving -> rejectView.render(ButtonStateView.State.Error) - else -> rejectView.render(ButtonStateView.State.Button) + when (changeMembershipState) { + is ChangeMembershipState.FailedLeaving -> rejectView.render(ButtonStateView.State.Error) + else -> rejectView.render(ButtonStateView.State.Button) } } } diff --git a/vector/src/main/java/im/vector/app/features/location/Config.kt b/vector/src/main/java/im/vector/app/features/location/Config.kt new file mode 100644 index 0000000000..630df16a37 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/Config.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.location + +const val INITIAL_MAP_ZOOM = 15.0 +const val MIN_TIME_MILLIS_TO_UPDATE_LOCATION = 1 * 60 * 1000L // every 1 minute +const val MIN_DISTANCE_METERS_TO_UPDATE_LOCATION = 10f diff --git a/vector/src/main/java/im/vector/app/features/location/LocationData.kt b/vector/src/main/java/im/vector/app/features/location/LocationData.kt new file mode 100644 index 0000000000..c3ff09ebcd --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/LocationData.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.location + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class LocationData( + val latitude: Double, + val longitude: Double, + val uncertainty: Double? +) : Parcelable { + + companion object { + + /** + * Creates location data from geo uri + * @param geoUri geo:latitude,longitude;uncertainty + * @return location data or null if geo uri is not valid + */ + fun create(geoUri: String): LocationData? { + val geoParts = geoUri + .split(":") + .takeIf { it.firstOrNull() == "geo" } + ?.getOrNull(1) + ?.split(",") + + val latitude = geoParts?.firstOrNull() + val geoTailParts = geoParts?.getOrNull(1)?.split(";") + val longitude = geoTailParts?.firstOrNull() + val uncertainty = geoTailParts?.getOrNull(1)?.replace("u=", "") + + return if (latitude != null && longitude != null) { + LocationData( + latitude = latitude.toDouble(), + longitude = longitude.toDouble(), + uncertainty = uncertainty?.toDouble() + ) + } else null + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt new file mode 100644 index 0000000000..6209bf5a4f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.location + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import com.airbnb.mvrx.args +import im.vector.app.R +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.utils.openLocation +import im.vector.app.databinding.FragmentLocationPreviewBinding +import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider +import javax.inject.Inject + +class LocationPreviewFragment @Inject constructor( + private val locationPinProvider: LocationPinProvider +) : VectorBaseFragment() { + + private val args: LocationSharingArgs by args() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLocationPreviewBinding { + return FragmentLocationPreviewBinding.inflate(layoutInflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + views.mapView.initialize { + if (isAdded) { + onMapReady() + } + } + } + + override fun onPause() { + views.mapView.onPause() + super.onPause() + } + + override fun onStop() { + views.mapView.onStop() + super.onStop() + } + + override fun getMenuRes() = R.menu.menu_location_preview + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.share_external -> { + onShareLocationExternal() + return true + } + } + return super.onOptionsItemSelected(item) + } + + private fun onShareLocationExternal() { + val location = args.initialLocationData ?: return + openLocation(requireActivity(), location.latitude, location.longitude) + } + + private fun onMapReady() { + if (!isAdded) return + + val location = args.initialLocationData ?: return + val userId = args.locationOwnerId + + locationPinProvider.create(userId) { pinDrawable -> + views.mapView.apply { + zoomToLocation(location.latitude, location.longitude, INITIAL_MAP_ZOOM) + deleteAllPins() + addPinToMap(userId, pinDrawable) + updatePinLocation(userId, location.latitude, location.longitude) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt new file mode 100644 index 0000000000..71101d0612 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.location + +import im.vector.app.core.platform.VectorViewModelAction + +sealed class LocationSharingAction : VectorViewModelAction { + data class OnLocationUpdate(val locationData: LocationData) : LocationSharingAction() + object OnShareLocation : LocationSharingAction() + object OnLocationProviderIsNotAvailable : LocationSharingAction() +} diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingActivity.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingActivity.kt new file mode 100644 index 0000000000..67b36b8442 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingActivity.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.location + +import android.content.Context +import android.content.Intent +import android.os.Parcelable +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.core.extensions.addFragment +import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.databinding.ActivityLocationSharingBinding +import kotlinx.parcelize.Parcelize + +@Parcelize +data class LocationSharingArgs( + val roomId: String, + val mode: LocationSharingMode, + val initialLocationData: LocationData?, + val locationOwnerId: String +) : Parcelable + +@AndroidEntryPoint +class LocationSharingActivity : VectorBaseActivity() { + + override fun getBinding() = ActivityLocationSharingBinding.inflate(layoutInflater) + + override fun initUiAndData() { + val locationSharingArgs: LocationSharingArgs? = intent?.extras?.getParcelable(EXTRA_LOCATION_SHARING_ARGS) + if (locationSharingArgs == null) { + finish() + return + } + setupToolbar(views.toolbar) + .setTitle(locationSharingArgs.mode.titleRes) + .allowBack() + + if (isFirstCreation()) { + when (locationSharingArgs.mode) { + LocationSharingMode.STATIC_SHARING -> { + addFragment( + views.fragmentContainer, + LocationSharingFragment::class.java, + locationSharingArgs + ) + } + LocationSharingMode.PREVIEW -> { + addFragment( + views.fragmentContainer, + LocationPreviewFragment::class.java, + locationSharingArgs + ) + } + } + } + } + + companion object { + + private const val EXTRA_LOCATION_SHARING_ARGS = "EXTRA_LOCATION_SHARING_ARGS" + + fun getIntent(context: Context, locationSharingArgs: LocationSharingArgs): Intent { + return Intent(context, LocationSharingActivity::class.java).apply { + putExtra(EXTRA_LOCATION_SHARING_ARGS, locationSharingArgs) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt new file mode 100644 index 0000000000..900f465f04 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.location + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.mvrx.fragmentViewModel +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import im.vector.app.R +import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.databinding.FragmentLocationSharingBinding +import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider +import org.matrix.android.sdk.api.session.Session +import javax.inject.Inject + +class LocationSharingFragment @Inject constructor( + private val locationTracker: LocationTracker, + private val session: Session, + private val locationPinProvider: LocationPinProvider +) : VectorBaseFragment(), LocationTracker.Callback { + + init { + locationTracker.callback = this + } + + private val viewModel: LocationSharingViewModel by fragmentViewModel() + + private var lastZoomValue: Double = -1.0 + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLocationSharingBinding { + return FragmentLocationSharingBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + views.mapView.initialize { + if (isAdded) { + onMapReady() + } + } + + views.shareLocationContainer.debouncedClicks { + viewModel.handle(LocationSharingAction.OnShareLocation) + } + + viewModel.observeViewEvents { + when (it) { + LocationSharingViewEvents.LocationNotAvailableError -> handleLocationNotAvailableError() + LocationSharingViewEvents.Close -> activity?.finish() + }.exhaustive + } + } + + override fun onPause() { + views.mapView.onPause() + super.onPause() + } + + override fun onStop() { + views.mapView.onStop() + super.onStop() + } + + override fun onDestroy() { + locationTracker.stop() + super.onDestroy() + } + + private fun onMapReady() { + if (!isAdded) return + + locationPinProvider.create(session.myUserId) { + views.mapView.addPinToMap( + pinId = USER_PIN_NAME, + image = it, + ) + // All set, start location tracker + locationTracker.start() + } + } + + override fun onLocationUpdate(locationData: LocationData) { + lastZoomValue = if (lastZoomValue == -1.0) INITIAL_MAP_ZOOM else views.mapView.getCurrentZoom() ?: INITIAL_MAP_ZOOM + + views.mapView.zoomToLocation(locationData.latitude, locationData.longitude, lastZoomValue) + views.mapView.deleteAllPins() + views.mapView.updatePinLocation(USER_PIN_NAME, locationData.latitude, locationData.longitude) + + viewModel.handle(LocationSharingAction.OnLocationUpdate(locationData)) + } + + override fun onLocationProviderIsNotAvailable() { + viewModel.handle(LocationSharingAction.OnLocationProviderIsNotAvailable) + } + + private fun handleLocationNotAvailableError() { + MaterialAlertDialogBuilder(requireActivity()) + .setTitle(R.string.location_not_available_dialog_title) + .setMessage(R.string.location_not_available_dialog_content) + .setPositiveButton(R.string.ok) { _, _ -> + activity?.finish() + } + .show() + } + + companion object { + const val USER_PIN_NAME = "USER_PIN_NAME" + } +} diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewEvents.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewEvents.kt new file mode 100644 index 0000000000..743daaf5e0 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewEvents.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.location + +import im.vector.app.core.platform.VectorViewEvents + +sealed class LocationSharingViewEvents : VectorViewEvents { + object Close : LocationSharingViewEvents() + object LocationNotAvailableError : LocationSharingViewEvents() +} diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt new file mode 100644 index 0000000000..b3c97310e1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.location + +import com.airbnb.mvrx.MavericksViewModelFactory +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.platform.VectorViewModel +import org.matrix.android.sdk.api.session.Session + +class LocationSharingViewModel @AssistedInject constructor( + @Assisted private val initialState: LocationSharingViewState, + session: Session +) : VectorViewModel(initialState) { + + private val room = session.getRoom(initialState.roomId)!! + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: LocationSharingViewState): LocationSharingViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() { + } + + override fun handle(action: LocationSharingAction) { + when (action) { + is LocationSharingAction.OnLocationUpdate -> handleLocationUpdate(action.locationData) + LocationSharingAction.OnShareLocation -> handleShareLocation() + LocationSharingAction.OnLocationProviderIsNotAvailable -> handleLocationProviderIsNotAvailable() + }.exhaustive + } + + private fun handleShareLocation() = withState { state -> + state.lastKnownLocation?.let { location -> + room.sendLocation( + latitude = location.latitude, + longitude = location.longitude, + uncertainty = location.uncertainty + ) + _viewEvents.post(LocationSharingViewEvents.Close) + } ?: run { + _viewEvents.post(LocationSharingViewEvents.LocationNotAvailableError) + } + } + + private fun handleLocationUpdate(locationData: LocationData) { + setState { + copy(lastKnownLocation = locationData) + } + } + + private fun handleLocationProviderIsNotAvailable() { + _viewEvents.post(LocationSharingViewEvents.LocationNotAvailableError) + } +} diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt new file mode 100644 index 0000000000..2869929b12 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.location + +import androidx.annotation.StringRes +import com.airbnb.mvrx.MavericksState +import im.vector.app.R + +enum class LocationSharingMode(@StringRes val titleRes: Int) { + STATIC_SHARING(R.string.location_activity_title_static_sharing), + PREVIEW(R.string.location_activity_title_preview) +} + +data class LocationSharingViewState( + val roomId: String, + val mode: LocationSharingMode, + val lastKnownLocation: LocationData? = null +) : MavericksState { + + constructor(locationSharingArgs: LocationSharingArgs) : this( + roomId = locationSharingArgs.roomId, + mode = locationSharingArgs.mode + ) +} diff --git a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt new file mode 100644 index 0000000000..0c0315cf34 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.location + +import android.Manifest +import android.content.Context +import android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import androidx.annotation.RequiresPermission +import androidx.core.content.getSystemService +import timber.log.Timber +import javax.inject.Inject + +class LocationTracker @Inject constructor( + private val context: Context +) : LocationListener { + + interface Callback { + fun onLocationUpdate(locationData: LocationData) + fun onLocationProviderIsNotAvailable() + } + + private var locationManager: LocationManager? = null + var callback: Callback? = null + + @RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION]) + fun start() { + val locationManager = context.getSystemService() + + locationManager?.let { + val isGpsEnabled = it.isProviderEnabled(LocationManager.GPS_PROVIDER) + val isNetworkEnabled = it.isProviderEnabled(LocationManager.NETWORK_PROVIDER) + + val provider = when { + isGpsEnabled -> LocationManager.GPS_PROVIDER + isNetworkEnabled -> LocationManager.NETWORK_PROVIDER + else -> { + callback?.onLocationProviderIsNotAvailable() + Timber.v("## LocationTracker. There is no location provider available") + return + } + } + + // Send last known location without waiting location updates + it.getLastKnownLocation(provider)?.let { lastKnownLocation -> + callback?.onLocationUpdate(lastKnownLocation.toLocationData()) + } + + it.requestLocationUpdates( + provider, + MIN_TIME_MILLIS_TO_UPDATE_LOCATION, + MIN_DISTANCE_METERS_TO_UPDATE_LOCATION, + this + ) + } ?: run { + callback?.onLocationProviderIsNotAvailable() + Timber.v("## LocationTracker. LocationManager is not available") + } + } + + @RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION]) + fun stop() { + locationManager?.removeUpdates(this) + callback = null + } + + override fun onLocationChanged(location: Location) { + callback?.onLocationUpdate(location.toLocationData()) + } + + private fun Location.toLocationData(): LocationData { + return LocationData(latitude, longitude, accuracy.toDouble()) + } +} diff --git a/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt b/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt new file mode 100644 index 0000000000..c64af1ebaa --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.location + +import android.content.Context +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import com.mapbox.mapboxsdk.camera.CameraPosition +import com.mapbox.mapboxsdk.geometry.LatLng +import com.mapbox.mapboxsdk.maps.MapView +import com.mapbox.mapboxsdk.maps.MapboxMap +import com.mapbox.mapboxsdk.maps.Style +import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager +import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions +import com.mapbox.mapboxsdk.style.layers.Property +import im.vector.app.BuildConfig + +class MapTilerMapView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : MapView(context, attrs, defStyleAttr), VectorMapView { + + private var map: MapboxMap? = null + private var symbolManager: SymbolManager? = null + private var style: Style? = null + + override fun initialize(onMapReady: () -> Unit) { + getMapAsync { map -> + map.setStyle(styleUrl) { style -> + this.symbolManager = SymbolManager(this, map, style) + this.map = map + this.style = style + onMapReady() + } + } + } + + override fun addPinToMap(pinId: String, image: Drawable) { + style?.addImage(pinId, image) + } + + override fun updatePinLocation(pinId: String, latitude: Double, longitude: Double) { + symbolManager?.create( + SymbolOptions() + .withLatLng(LatLng(latitude, longitude)) + .withIconImage(pinId) + .withIconAnchor(Property.ICON_ANCHOR_BOTTOM) + ) + } + + override fun deleteAllPins() { + symbolManager?.deleteAll() + } + + override fun zoomToLocation(latitude: Double, longitude: Double, zoom: Double) { + map?.cameraPosition = CameraPosition.Builder() + .target(LatLng(latitude, longitude)) + .zoom(zoom) + .build() + } + + override fun getCurrentZoom(): Double? { + return map?.cameraPosition?.zoom + } + + override fun onClick(callback: () -> Unit) { + map?.addOnMapClickListener { + callback() + true + } + } + + companion object { + private const val styleUrl = "https://api.maptiler.com/maps/streets/style.json?key=${BuildConfig.mapTilerKey}" + } +} diff --git a/vector/src/main/java/im/vector/app/features/location/VectorMapView.kt b/vector/src/main/java/im/vector/app/features/location/VectorMapView.kt new file mode 100644 index 0000000000..23b59bf99a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/VectorMapView.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.location + +import android.graphics.drawable.Drawable + +interface VectorMapView { + fun initialize(onMapReady: () -> Unit) + + fun addPinToMap(pinId: String, image: Drawable) + fun updatePinLocation(pinId: String, latitude: Double, longitude: Double) + fun deleteAllPins() + + fun zoomToLocation(latitude: Double, longitude: Double, zoom: Double) + fun getCurrentZoom(): Double? + + fun onClick(callback: () -> Unit) +} diff --git a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt index 5ab08ffff7..edc77d73f6 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt @@ -29,7 +29,6 @@ import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentTransaction import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.withState -import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R @@ -37,9 +36,9 @@ import im.vector.app.core.extensions.POP_BACK_STACK_EXCLUSIVE import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragmentToBackstack import im.vector.app.core.extensions.exhaustive -import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityLoginBinding +import im.vector.app.features.analytics.plan.Screen import im.vector.app.features.home.HomeActivity import im.vector.app.features.login.terms.LoginTermsFragment import im.vector.app.features.login.terms.LoginTermsFragmentArgument @@ -53,7 +52,7 @@ import org.matrix.android.sdk.api.extensions.tryOrNull * The LoginActivity manages the fragment navigation and also display the loading View */ @AndroidEntryPoint -open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedActivity { +open class LoginActivity : VectorBaseActivity(), UnlockedActivity { private val loginViewModel: LoginViewModel by viewModel() @@ -82,6 +81,8 @@ open class LoginActivity : VectorBaseActivity(), ToolbarCo override fun getCoordinatorLayout() = views.coordinatorLayout override fun initUiAndData() { + analyticsScreenName = Screen.ScreenName.Login + if (isFirstCreation()) { addFirstFragment() } @@ -200,6 +201,10 @@ open class LoginActivity : VectorBaseActivity(), ToolbarCo private fun updateWithState(loginViewState: LoginViewState) { if (loginViewState.isUserLogged()) { + if (loginViewState.signMode == SignMode.SignUp) { + // change the screen name + analyticsScreenName = Screen.ScreenName.Register + } val intent = HomeActivity.newIntent( this, accountCreation = loginViewState.signMode == SignMode.SignUp @@ -346,10 +351,6 @@ open class LoginActivity : VectorBaseActivity(), ToolbarCo } } - override fun configure(toolbar: MaterialToolbar) { - configureToolbar(toolbar) - } - companion object { private const val FRAGMENT_REGISTRATION_STAGE_TAG = "FRAGMENT_REGISTRATION_STAGE_TAG" private const val FRAGMENT_LOGIN_TAG = "FRAGMENT_LOGIN_TAG" diff --git a/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordFragment.kt index 52bd80a16f..0328d09427 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordFragment.kt @@ -31,6 +31,7 @@ import im.vector.app.core.extensions.hidePassword import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.toReducedUrl import im.vector.app.databinding.FragmentLoginResetPasswordBinding +import im.vector.app.features.analytics.plan.Screen import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map @@ -46,6 +47,11 @@ class LoginResetPasswordFragment @Inject constructor() : AbstractLoginFragment getString(R.string.login_signin) else -> getString(R.string.login_signup) } @@ -149,7 +150,7 @@ class LoginWebFragment @Inject constructor( override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { super.onPageStarted(view, url, favicon) - views.loginWebToolbar.subtitle = url + toolbar?.subtitle = url } override fun onPageFinished(view: WebView, url: String) { diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginWebFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginWebFragment2.kt index 080cce4958..ebe59ee1b9 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginWebFragment2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginWebFragment2.kt @@ -65,6 +65,7 @@ class LoginWebFragment2 @Inject constructor( super.onViewCreated(view, savedInstanceState) setupToolbar(views.loginWebToolbar) + .allowBack() } override fun updateWithState(state: LoginViewState2) { @@ -79,7 +80,7 @@ class LoginWebFragment2 @Inject constructor( } private fun setupTitle(state: LoginViewState2) { - views.loginWebToolbar.title = when (state.signMode) { + toolbar?.title = when (state.signMode) { SignMode2.SignIn -> getString(R.string.login_signin) else -> getString(R.string.login_signup) } @@ -150,7 +151,7 @@ class LoginWebFragment2 @Inject constructor( override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { super.onPageStarted(view, url, favicon) - views.loginWebToolbar.subtitle = url + toolbar?.subtitle = url } override fun onPageFinished(view: WebView, url: String) { diff --git a/vector/src/main/java/im/vector/app/features/media/BigImageViewerActivity.kt b/vector/src/main/java/im/vector/app/features/media/BigImageViewerActivity.kt index 84454ee509..a6b166815c 100644 --- a/vector/src/main/java/im/vector/app/features/media/BigImageViewerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/media/BigImageViewerActivity.kt @@ -38,12 +38,9 @@ class BigImageViewerActivity : VectorBaseActivity override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setSupportActionBar(views.bigImageViewerToolbar) - supportActionBar?.apply { - title = intent.getStringExtra(EXTRA_TITLE) - setHomeButtonEnabled(true) - setDisplayHomeAsUpEnabled(true) - } + setupToolbar(views.bigImageViewerToolbar) + .setTitle(intent.getStringExtra(EXTRA_TITLE)) + .allowBack() val uri = sessionHolder.getSafeActiveSession() ?.contentUrlResolver() diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 387cbaf197..5c49775e37 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 @@ -61,6 +61,10 @@ import im.vector.app.features.home.room.threads.ThreadsActivity import im.vector.app.features.home.room.threads.arguments.ThreadListArgs import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.invite.InviteUsersToRoomActivity +import im.vector.app.features.location.LocationData +import im.vector.app.features.location.LocationSharingActivity +import im.vector.app.features.location.LocationSharingArgs +import im.vector.app.features.location.LocationSharingMode import im.vector.app.features.login.LoginActivity import im.vector.app.features.login.LoginConfig import im.vector.app.features.matrixto.MatrixToBottomSheet @@ -73,6 +77,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 @@ -535,10 +540,22 @@ class DefaultNavigator @Inject constructor( context.startActivity(intent) } - override fun openCreatePoll(context: Context, roomId: String) { + override fun openCreatePoll(context: Context, roomId: String, editedEventId: String?, mode: PollMode) { val intent = CreatePollActivity.getIntent( context, - CreatePollArgs(roomId = roomId) + CreatePollArgs(roomId = roomId, editedEventId = editedEventId, mode = mode) + ) + context.startActivity(intent) + } + + override fun openLocationSharing(context: Context, + roomId: String, + mode: LocationSharingMode, + initialLocationData: LocationData?, + locationOwnerId: String) { + val intent = LocationSharingActivity.getIntent( + context, + LocationSharingArgs(roomId = roomId, mode = mode, initialLocationData = initialLocationData, locationOwnerId = locationOwnerId) ) context.startActivity(intent) } diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index 67a67e17d9..97e2257b36 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 @@ -26,9 +26,12 @@ import androidx.core.util.Pair import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs +import im.vector.app.features.location.LocationData +import im.vector.app.features.location.LocationSharingMode import im.vector.app.features.login.LoginConfig import im.vector.app.features.media.AttachmentData import im.vector.app.features.pin.PinMode +import im.vector.app.features.poll.create.PollMode import im.vector.app.features.roomdirectory.RoomDirectoryData import im.vector.app.features.roomdirectory.roompreview.RoomPreviewData import im.vector.app.features.settings.VectorSettingsActivity @@ -149,8 +152,13 @@ interface Navigator { fun openCallTransfer(context: Context, callId: String) - fun openCreatePoll(context: Context, roomId: String) + fun openCreatePoll(context: Context, roomId: String, editedEventId: String?, mode: PollMode) + fun openLocationSharing(context: Context, + roomId: String, + mode: LocationSharingMode, + initialLocationData: LocationData?, + locationOwnerId: String) fun openThread(context: Context, threadTimelineArgs: ThreadTimelineArgs, eventIdToNavigate: String? = null) fun openThreadList(context: Context, threadTimelineArgs: ThreadTimelineArgs) 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 0982e62df6..d39926f620 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, TimelineArgs(roomId)) + val roomIntentTap = RoomDetailActivity.newIntent(context, TimelineArgs(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/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/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt index 3ae0ecccd2..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 @@ -852,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) } } } @@ -876,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 038e020cf6..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 @@ -68,9 +68,13 @@ class FtueAuthSplashCarouselFragment @Inject constructor( TabLayoutMediator(views.carouselIndicator, views.splashCarousel) { _, _ -> }.attach() 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.isOnboardingAlreadyHaveAccountSplashEnabled() + isVisible = isAlreadyHaveAccountEnabled debouncedClicks { alreadyHaveAnAccount() } } @@ -111,8 +115,8 @@ class FtueAuthSplashCarouselFragment @Inject constructor( } } - private fun getStarted() { - val getStartedFlow = if (vectorFeatures.isOnboardingAlreadyHaveAccountSplashEnabled()) OnboardingFlow.SignUp else OnboardingFlow.SignInSignUp + private fun splashSubmit(isAlreadyHaveAccountEnabled: Boolean) { + val getStartedFlow = if (isAlreadyHaveAccountEnabled) OnboardingFlow.SignUp else OnboardingFlow.SignInSignUp viewModel.handle(OnboardingAction.OnGetStarted(resetLoginConfig = false, onboardingFlow = getStartedFlow)) } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashFragment.kt index fd63889fd6..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,7 +53,11 @@ 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.isOnboardingAlreadyHaveAccountSplashEnabled() debouncedClicks { alreadyHaveAnAccount() } @@ -69,8 +73,8 @@ class FtueAuthSplashFragment @Inject constructor( } } - private fun getStarted() { - val getStartedFlow = if (vectorFeatures.isOnboardingAlreadyHaveAccountSplashEnabled()) 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 index 6310eaea95..5325b25e93 100644 --- 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 @@ -16,21 +16,36 @@ package im.vector.app.features.onboarding.ftueauth +import android.graphics.Color +import android.graphics.drawable.Drawable +import android.graphics.drawable.LayerDrawable import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import androidx.core.content.ContextCompat import im.vector.app.R +import im.vector.app.core.extensions.getResTintedDrawable +import im.vector.app.core.extensions.getTintedDrawable +import im.vector.app.core.extensions.setLeftDrawable import im.vector.app.core.extensions.setTextWithColoredPart import im.vector.app.databinding.FragmentFtueAuthUseCaseBinding import im.vector.app.features.login.ServerType import im.vector.app.features.onboarding.FtueUseCase import im.vector.app.features.onboarding.OnboardingAction +import im.vector.app.features.themes.ThemeProvider import javax.inject.Inject -class FtueAuthUseCaseFragment @Inject constructor() : AbstractFtueAuthFragment() { +private const val DARK_MODE_ICON_BACKGROUND_ALPHA = 0.30f +private const val LIGHT_MODE_ICON_BACKGROUND_ALPHA = 0.15f + +class FtueAuthUseCaseFragment @Inject constructor( + private val themeProvider: ThemeProvider +) : AbstractFtueAuthFragment() { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueAuthUseCaseBinding { return FragmentFtueAuthUseCaseBinding.inflate(inflater, container, false) @@ -42,9 +57,24 @@ class FtueAuthUseCaseFragment @Inject constructor() : AbstractFtueAuthFragment LIGHT_MODE_ICON_BACKGROUND_ALPHA + false -> DARK_MODE_ICON_BACKGROUND_ALPHA + } + val iconBackground = context.getResTintedDrawable(R.drawable.bg_feature_icon, tint, alpha = alpha) + val whiteLayer = context.getTintedDrawable(R.drawable.bg_feature_icon, Color.WHITE) + return LayerDrawable(arrayOf(whiteLayer, iconBackground, ContextCompat.getDrawable(context, icon))) + } } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/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/SplashCarouselState.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/SplashCarouselState.kt index 5328722a99..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,7 +18,7 @@ package im.vector.app.features.onboarding.ftueauth import androidx.annotation.DrawableRes import androidx.annotation.StringRes -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence data class SplashCarouselState( val items: List 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 index 4ea462f737..da5f8b6379 100644 --- 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 @@ -20,13 +20,13 @@ import android.content.Context import androidx.annotation.AttrRes import androidx.annotation.DrawableRes import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence 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 diff --git a/vector/src/main/java/im/vector/app/features/pin/PinActivity.kt b/vector/src/main/java/im/vector/app/features/pin/PinActivity.kt index ef79799074..faf15d8006 100644 --- a/vector/src/main/java/im/vector/app/features/pin/PinActivity.kt +++ b/vector/src/main/java/im/vector/app/features/pin/PinActivity.kt @@ -19,15 +19,13 @@ package im.vector.app.features.pin import android.content.Context import android.content.Intent import com.airbnb.mvrx.Mavericks -import com.google.android.material.appbar.MaterialToolbar import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.addFragment -import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleBinding @AndroidEntryPoint -class PinActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedActivity { +class PinActivity : VectorBaseActivity(), UnlockedActivity { companion object { fun newIntent(context: Context, args: PinArgs): Intent { @@ -47,8 +45,4 @@ class PinActivity : VectorBaseActivity(), ToolbarConfigur addFragment(views.simpleFragmentContainer, PinFragment::class.java, fragmentArgs) } } - - override fun configure(toolbar: MaterialToolbar) { - configureToolbar(toolbar) - } } diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollAction.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollAction.kt index 182750fbd2..5fddcac568 100644 --- a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollAction.kt +++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollAction.kt @@ -17,11 +17,13 @@ package im.vector.app.features.poll.create import im.vector.app.core.platform.VectorViewModelAction +import org.matrix.android.sdk.api.session.room.model.message.PollType sealed class CreatePollAction : VectorViewModelAction { data class OnQuestionChanged(val question: String) : CreatePollAction() data class OnOptionChanged(val index: Int, val option: String) : CreatePollAction() data class OnDeleteOption(val index: Int) : CreatePollAction() + data class OnPollTypeChanged(val pollType: PollType) : CreatePollAction() object OnAddOption : CreatePollAction() object OnCreatePoll : CreatePollAction() } diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollController.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollController.kt index 90128d9263..d938b98eed 100644 --- a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollController.kt +++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollController.kt @@ -20,7 +20,6 @@ import android.view.Gravity import android.view.inputmethod.EditorInfo import com.airbnb.epoxy.EpoxyController import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.ItemStyle @@ -28,6 +27,8 @@ import im.vector.app.core.ui.list.genericButtonItem import im.vector.app.core.ui.list.genericItem import im.vector.app.features.form.formEditTextItem import im.vector.app.features.form.formEditTextWithDeleteItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence +import org.matrix.android.sdk.api.session.room.model.message.PollType import javax.inject.Inject class CreatePollController @Inject constructor( @@ -47,6 +48,28 @@ class CreatePollController @Inject constructor( val currentState = state ?: return val host = this + genericItem { + id("poll_type_title") + style(ItemStyle.BIG_TEXT) + title(host.stringProvider.getString(R.string.poll_type_title).toEpoxyCharSequence()) + } + + /* + pollTypeSelectionItem { + id("poll_type_selection") + pollType(currentState.pollType) + pollTypeChangedListener { _, id -> + host.callback?.onPollTypeChanged( + if (id == R.id.openPollTypeRadioButton) { + PollType.DISCLOSED + } else { + PollType.UNDISCLOSED + } + ) + } + } + */ + genericItem { id("question_title") style(ItemStyle.BIG_TEXT) @@ -110,5 +133,6 @@ class CreatePollController @Inject constructor( fun onOptionChanged(index: Int, option: String) fun onDeleteOption(index: Int) fun onAddOption() + fun onPollTypeChanged(type: PollType) } } diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollFragment.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollFragment.kt index 1d807654e8..4483b00158 100644 --- a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollFragment.kt +++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollFragment.kt @@ -23,18 +23,23 @@ import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.args import com.airbnb.mvrx.withState import im.vector.app.R import im.vector.app.core.extensions.configureWith +import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentCreatePollBinding import im.vector.app.features.poll.create.CreatePollViewModel.Companion.MAX_OPTIONS_COUNT import kotlinx.parcelize.Parcelize +import org.matrix.android.sdk.api.session.room.model.message.PollType import javax.inject.Inject @Parcelize data class CreatePollArgs( val roomId: String, + val editedEventId: String?, + val mode: PollMode ) : Parcelable class CreatePollFragment @Inject constructor( @@ -42,6 +47,7 @@ class CreatePollFragment @Inject constructor( ) : VectorBaseFragment(), CreatePollController.Callback { private val viewModel: CreatePollViewModel by activityViewModel() + private val args: CreatePollArgs by args() override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentCreatePollBinding { return FragmentCreatePollBinding.inflate(inflater, container, false) @@ -49,17 +55,26 @@ class CreatePollFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - vectorBaseActivity.setSupportActionBar(views.createPollToolbar) + + setupToolbar(views.createPollToolbar) + .allowBack(useCross = true) + + when (args.mode) { + PollMode.CREATE -> { + views.createPollToolbar.title = getString(R.string.create_poll_title) + views.createPollButton.text = getString(R.string.create_poll_title) + } + PollMode.EDIT -> { + views.createPollToolbar.title = getString(R.string.edit_poll_title) + views.createPollButton.text = getString(R.string.edit_poll_title) + } + }.exhaustive views.createPollRecyclerView.configureWith(controller, disableItemAnimation = true) // workaround for https://github.com/vector-im/element-android/issues/4735 - views.createPollRecyclerView.setItemViewCacheSize(MAX_OPTIONS_COUNT + 4) + views.createPollRecyclerView.setItemViewCacheSize(MAX_OPTIONS_COUNT + 6) controller.callback = this - views.createPollClose.debouncedClicks { - requireActivity().finish() - } - views.createPollButton.debouncedClicks { viewModel.handle(CreatePollAction.OnCreatePoll) } @@ -103,6 +118,10 @@ class CreatePollFragment @Inject constructor( } } + override fun onPollTypeChanged(type: PollType) { + viewModel.handle(CreatePollAction.OnPollTypeChanged(type)) + } + private fun handleSuccess() { requireActivity().finish() } diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt index b5e66ae682..7750e6d909 100644 --- a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt @@ -24,6 +24,9 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent +import org.matrix.android.sdk.api.session.room.model.message.PollType +import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent class CreatePollViewModel @AssistedInject constructor( @Assisted private val initialState: CreatePollViewState, @@ -45,6 +48,9 @@ class CreatePollViewModel @AssistedInject constructor( init { observeState() + initialState.editedEventId?.let { + initializeEditedPoll(it) + } } private fun observeState() { @@ -61,6 +67,23 @@ class CreatePollViewModel @AssistedInject constructor( } } + private fun initializeEditedPoll(eventId: String) { + val event = room.getTimeLineEvent(eventId) ?: return + val content = event.getLastMessageContent() as? MessagePollContent ?: return + + val pollType = content.pollCreationInfo?.kind ?: PollType.DISCLOSED + val question = content.pollCreationInfo?.question?.question ?: "" + val options = content.pollCreationInfo?.answers?.mapNotNull { it.answer } ?: List(MIN_OPTIONS_COUNT) { "" } + + setState { + copy( + question = question, + options = options, + pollType = pollType + ) + } + } + override fun handle(action: CreatePollAction) { when (action) { CreatePollAction.OnCreatePoll -> handleOnCreatePoll() @@ -68,6 +91,7 @@ class CreatePollViewModel @AssistedInject constructor( is CreatePollAction.OnDeleteOption -> handleOnDeleteOption(action.index) is CreatePollAction.OnOptionChanged -> handleOnOptionChanged(action.index, action.option) is CreatePollAction.OnQuestionChanged -> handleOnQuestionChanged(action.question) + is CreatePollAction.OnPollTypeChanged -> handleOnPollTypeChanged(action.pollType) } } @@ -81,12 +105,20 @@ class CreatePollViewModel @AssistedInject constructor( _viewEvents.post(CreatePollViewEvents.NotEnoughOptionsError(requiredOptionsCount = MIN_OPTIONS_COUNT)) } else -> { - room.sendPoll(state.question, nonEmptyOptions) + when (state.mode) { + PollMode.CREATE -> room.sendPoll(state.pollType, state.question, nonEmptyOptions) + PollMode.EDIT -> sendEditedPoll(state.editedEventId!!, state.pollType, state.question, nonEmptyOptions) + } _viewEvents.post(CreatePollViewEvents.Success) } } } + private fun sendEditedPoll(editedEventId: String, pollType: PollType, question: String, options: List) { + val editedEvent = room.getTimeLineEvent(editedEventId) ?: return + room.editPoll(editedEvent, pollType, question, options) + } + private fun handleOnAddOption() { setState { val extendedOptions = options + "" @@ -122,6 +154,14 @@ class CreatePollViewModel @AssistedInject constructor( } } + private fun handleOnPollTypeChanged(pollType: PollType) { + setState { + copy( + pollType = pollType + ) + } + } + private fun canCreatePoll(question: String, options: List): Boolean { return question.isNotEmpty() && options.filter { it.isNotEmpty() }.size >= MIN_OPTIONS_COUNT diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt index a9060cc89f..175d1b0116 100644 --- a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt +++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt @@ -17,16 +17,22 @@ package im.vector.app.features.poll.create import com.airbnb.mvrx.MavericksState +import org.matrix.android.sdk.api.session.room.model.message.PollType data class CreatePollViewState( val roomId: String, + val editedEventId: String?, + val mode: PollMode, val question: String = "", val options: List = List(CreatePollViewModel.MIN_OPTIONS_COUNT) { "" }, val canCreatePoll: Boolean = false, - val canAddMoreOptions: Boolean = true + val canAddMoreOptions: Boolean = true, + val pollType: PollType = PollType.DISCLOSED ) : MavericksState { constructor(args: CreatePollArgs) : this( - roomId = args.roomId + roomId = args.roomId, + editedEventId = args.editedEventId, + mode = args.mode ) } diff --git a/vector/src/main/java/im/vector/app/core/platform/ToolbarConfigurable.kt b/vector/src/main/java/im/vector/app/features/poll/create/PollMode.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/poll/create/PollMode.kt index 9aca8dd17f..0007589d10 100644 --- a/vector/src/main/java/im/vector/app/core/platform/ToolbarConfigurable.kt +++ b/vector/src/main/java/im/vector/app/features/poll/create/PollMode.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,9 @@ * limitations under the License. */ -package im.vector.app.core.platform +package im.vector.app.features.poll.create -import com.google.android.material.appbar.MaterialToolbar - -interface ToolbarConfigurable { - - fun configure(toolbar: MaterialToolbar) +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/qrcode/QrCodeScannerFragment.kt b/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerFragment.kt index 95d3ce4a07..a7231a0c5b 100644 --- a/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerFragment.kt @@ -38,10 +38,9 @@ class QrCodeScannerFragment @Inject constructor() : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - views.qrScannerClose.debouncedClicks { - requireActivity().onBackPressed() - } - views.qrScannerTitle.text = getString(R.string.verification_scan_their_code) + setupToolbar(views.qrScannerToolbar) + .setTitle(R.string.verification_scan_their_code) + .allowBack(useCross = true) } override fun onResume() { diff --git a/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt b/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt index 02df86e14b..0aec24f4ac 100755 --- a/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt +++ b/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt @@ -45,7 +45,8 @@ class BugReportActivity : VectorBaseActivity() { private var reportType: ReportType = ReportType.BUG_REPORT override fun initUiAndData() { - configureToolbar(views.bugReportToolbar) + setupToolbar(views.bugReportToolbar) + .allowBack() setupViews() if (bugReporter.screenshot != null) { diff --git a/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt b/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt index 26e9cabccb..b62a182fd8 100755 --- a/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt +++ b/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt @@ -259,13 +259,13 @@ class BugReporter @Inject constructor( ReportType.SUGGESTION -> "[Element] [Suggestion] $bugDescription" ReportType.SPACE_BETA_FEEDBACK -> "[Element] [spaces-feedback] $bugDescription" ReportType.AUTO_UISI_SENDER, - ReportType.AUTO_UISI -> "[AutoUISI] $bugDescription" + ReportType.AUTO_UISI -> bugDescription } // build the multi part request val builder = BugReporterMultipartBody.Builder() .addFormDataPart("text", text) - .addFormDataPart("app", "riot-android") + .addFormDataPart("app", rageShakeAppNameForReport(context, reportType)) .addFormDataPart("user_agent", Matrix.getInstance(context).getUserAgent()) .addFormDataPart("user_id", userId) .addFormDataPart("can_contact", canContact.toString()) @@ -340,9 +340,15 @@ class BugReporter @Inject constructor( } ReportType.SUGGESTION -> builder.addFormDataPart("label", "[Suggestion]") ReportType.SPACE_BETA_FEEDBACK -> builder.addFormDataPart("label", "spaces-feedback") - ReportType.AUTO_UISI, + ReportType.AUTO_UISI -> { + builder.addFormDataPart("label", "Z-UISI") + builder.addFormDataPart("label", "android") + builder.addFormDataPart("label", "uisi-recipient") + } ReportType.AUTO_UISI_SENDER -> { builder.addFormDataPart("label", "Z-UISI") + builder.addFormDataPart("label", "android") + builder.addFormDataPart("label", "uisi-sender") } } @@ -481,6 +487,21 @@ class BugReporter @Inject constructor( activity.startActivity(BugReportActivity.intent(activity, reportType)) } + private fun rageShakeAppNameForReport(context: Context, reportType: ReportType): String { + // As per https://github.com/matrix-org/rageshake + // app: Identifier for the application (eg 'riot-web'). + // Should correspond to a mapping configured in the configuration file for github issue reporting to work. + // (see R.string.bug_report_url for configured RS server) + return when (reportType) { + ReportType.AUTO_UISI_SENDER, + ReportType.AUTO_UISI -> { + context.getString(R.string.bug_report_auto_uisi_app_name) + } + else -> { + context.getString(R.string.bug_report_app_name) + } + } + } // ============================================================================================================== // crash report management // ============================================================================================================== diff --git a/vector/src/main/java/im/vector/app/features/reactions/EmojiReactionPickerActivity.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiReactionPickerActivity.kt index dc10c89967..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 @@ -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 f1165e44d4..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 @@ -20,9 +20,9 @@ import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.TypedEpoxyController import im.vector.app.EmojiCompatFontProvider import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import javax.inject.Inject class EmojiSearchResultController @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/room/RequireActiveMembershipViewModel.kt b/vector/src/main/java/im/vector/app/features/room/RequireActiveMembershipViewModel.kt index d2ee3a56ec..a77bd32f26 100644 --- a/vector/src/main/java/im/vector/app/features/room/RequireActiveMembershipViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/room/RequireActiveMembershipViewModel.kt @@ -98,13 +98,13 @@ class RequireActiveMembershipViewModel @AssistedInject constructor( val viewEvent = when (roomSummary.membership) { Membership.LEAVE -> { val message = senderDisplayName?.let { - stringProvider.getString(R.string.has_been_kicked, roomSummary.displayName, it) + stringProvider.getString(R.string.has_been_removed, roomSummary.displayName, it) } RequireActiveMembershipViewEvents.RoomLeft(message) } Membership.KNOCK -> { val message = senderDisplayName?.let { - stringProvider.getString(R.string.has_been_kicked, roomSummary.displayName, it) + stringProvider.getString(R.string.has_been_removed, roomSummary.displayName, it) } RequireActiveMembershipViewEvents.RoomLeft(message) } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsFragment.kt index f5a0d8d00d..14b50c2745 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/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 88bead5244..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 @@ -22,12 +22,11 @@ import android.content.Intent import android.os.Bundle import androidx.lifecycle.lifecycleScope 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 +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 @@ -37,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 @@ -45,10 +44,6 @@ 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 @@ -62,6 +57,7 @@ class CreateRoomActivity : VectorBaseActivity(), ToolbarC override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + analyticsScreenName = Screen.ScreenName.CreateRoom sharedActionViewModel = viewModelProvider.get(RoomDirectorySharedActionViewModel::class.java) sharedActionViewModel .stream() 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 c14c6be2e0..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 @@ -83,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() @@ -99,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 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 698d315337..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 @@ -30,6 +30,8 @@ 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 @@ -52,10 +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, - appStateHandler: AppStateHandler +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 @@ -296,7 +300,7 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted private val init viewModelScope.launch { runCatching { session.createRoom(createRoomParams) }.fold( { roomId -> - + analyticsTracker.capture(CreatedRoom(isDM = createRoomParams.isDirect.orFalse())) if (state.parentSpaceId != null) { // add it as a child try { diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateSubSpaceController.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateSubSpaceController.kt index c8d907b0b2..e67b272c32 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateSubSpaceController.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateSubSpaceController.kt @@ -20,7 +20,6 @@ import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.epoxy.profiles.buildProfileAction import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericPillItem @@ -29,6 +28,7 @@ import im.vector.app.features.form.formEditTextItem import im.vector.app.features.form.formEditableSquareAvatarItem import im.vector.app.features.form.formMultiLineEditTextItem import im.vector.app.features.form.formSubmitButtonItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.MatrixConstants import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure import org.matrix.android.sdk.api.session.room.model.RoomJoinRules diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt index 2707b87c1f..48610dda7b 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt @@ -20,7 +20,6 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.appcompat.app.AppCompatActivity import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState @@ -30,6 +29,7 @@ import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentRoomDirectoryPickerBinding +import im.vector.app.features.analytics.plan.Screen import im.vector.app.features.roomdirectory.RoomDirectoryAction import im.vector.app.features.roomdirectory.RoomDirectoryData import im.vector.app.features.roomdirectory.RoomDirectoryServer @@ -52,15 +52,17 @@ class RoomDirectoryPickerFragment @Inject constructor(private val roomDirectoryP return FragmentRoomDirectoryPickerBinding.inflate(inflater, container, false) } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + analyticsScreenName = Screen.ScreenName.MobileSwitchDirectory + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - vectorBaseActivity.setSupportActionBar(views.toolbar) - - vectorBaseActivity.supportActionBar?.let { - it.setDisplayShowHomeEnabled(true) - it.setDisplayHomeAsUpEnabled(true) - } + setupToolbar(views.toolbar) + .setTitle(R.string.select_room_directory) + .allowBack() sharedActionViewModel = activityViewModelProvider.get(RoomDirectorySharedActionViewModel::class.java) setupRecyclerView() @@ -109,11 +111,6 @@ class RoomDirectoryPickerFragment @Inject constructor(private val roomDirectoryP pickerViewModel.handle(RoomDirectoryPickerAction.RemoveServer(roomDirectoryServer)) } - override fun onResume() { - super.onResume() - (activity as? AppCompatActivity)?.supportActionBar?.setTitle(R.string.select_room_directory) - } - override fun retry() { Timber.v("Retry") pickerViewModel.handle(RoomDirectoryPickerAction.Retry) diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt index 394d738b26..b69788b1ed 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt @@ -19,10 +19,8 @@ package im.vector.app.features.roomdirectory.roompreview import android.content.Context import android.content.Intent import android.os.Parcelable -import com.google.android.material.appbar.MaterialToolbar import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.addFragment -import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleBinding import im.vector.app.features.roomdirectory.RoomDirectoryData @@ -40,6 +38,7 @@ data class RoomPreviewData( val roomAlias: String? = null, val roomType: String? = null, val topic: String? = null, + val numJoinedMembers: Int? = null, val worldReadable: Boolean = false, val avatarUrl: String? = null, val homeServers: List = emptyList(), @@ -52,7 +51,7 @@ data class RoomPreviewData( } @AndroidEntryPoint -class RoomPreviewActivity : VectorBaseActivity(), ToolbarConfigurable { +class RoomPreviewActivity : VectorBaseActivity() { companion object { private const val ARG = "ARG" @@ -69,6 +68,7 @@ class RoomPreviewActivity : VectorBaseActivity(), Toolbar roomName = publicRoom.name, roomAlias = publicRoom.getPrimaryAlias(), topic = publicRoom.topic, + numJoinedMembers = publicRoom.numJoinedMembers, worldReadable = publicRoom.worldReadable, avatarUrl = publicRoom.avatarUrl, homeServers = listOfNotNull(roomDirectoryData.homeServer) @@ -81,10 +81,6 @@ class RoomPreviewActivity : VectorBaseActivity(), Toolbar override fun getCoordinatorLayout() = views.coordinatorLayout - override fun configure(toolbar: MaterialToolbar) { - configureToolbar(toolbar) - } - override fun initUiAndData() { if (isFirstCreation()) { val args = intent.getParcelableExtra(ARG) diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt index 52617e2f1d..6d0195fae3 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt @@ -64,6 +64,7 @@ class RoomPreviewNoPreviewFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupToolbar(views.roomPreviewNoPreviewToolbar) + .allowBack() views.roomPreviewNoPreviewJoin.commonClicked = { roomPreviewViewModel.handle(RoomPreviewAction.Join) } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt index bdb0a96e9e..b1fa0e974a 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt @@ -27,6 +27,9 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.analytics.AnalyticsTracker +import im.vector.app.features.analytics.extensions.toAnalyticsRoomSize +import im.vector.app.features.analytics.plan.JoinedRoom import im.vector.app.features.roomdirectory.JoinState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn @@ -44,9 +47,11 @@ import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.flow.flow import timber.log.Timber -class RoomPreviewViewModel @AssistedInject constructor(@Assisted private val initialState: RoomPreviewViewState, - private val session: Session) : - VectorViewModel(initialState) { +class RoomPreviewViewModel @AssistedInject constructor( + @Assisted private val initialState: RoomPreviewViewState, + private val analyticsTracker: AnalyticsTracker, + private val session: Session +) : VectorViewModel(initialState) { @AssistedFactory interface Factory : MavericksAssistedViewModelFactory { @@ -243,6 +248,11 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted private val ini viewModelScope.launch { try { session.joinRoom(state.roomId, viaServers = state.homeServers) + analyticsTracker.capture(JoinedRoom( + // Always false in this case (?) + isDM = false, + roomSize = state.numJoinMembers.toAnalyticsRoomSize() + )) // We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data. // Instead, we wait for the room to be joined } catch (failure: Throwable) { diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt index 8488dd7267..b2cb43115d 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt @@ -33,6 +33,7 @@ data class RoomPreviewViewState( val roomName: String? = null, val roomTopic: String? = null, + val numJoinMembers: Int? = null, val avatarUrl: String? = null, val shouldPeekFromServer: Boolean = false, @@ -56,6 +57,7 @@ data class RoomPreviewViewState( homeServers = args.homeServers, roomName = args.roomName, roomTopic = args.topic, + numJoinMembers = args.numJoinedMembers, avatarUrl = args.avatarUrl, shouldPeekFromServer = args.peekFromServer, fromEmailInvite = args.fromEmailInvite, @@ -64,6 +66,6 @@ data class RoomPreviewViewState( fun matrixItem(): MatrixItem { return if (roomType == RoomType.SPACE) MatrixItem.SpaceItem(roomId, roomName ?: roomAlias, avatarUrl) - else MatrixItem.RoomItem(roomId, roomName ?: roomAlias, avatarUrl) + else MatrixItem.RoomItem(roomId, roomName ?: roomAlias, avatarUrl) } } diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileActivity.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileActivity.kt index 2d925270b3..1b55207743 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileActivity.kt @@ -22,19 +22,15 @@ import android.content.Intent import android.widget.Toast import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel -import com.google.android.material.appbar.MaterialToolbar import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.addFragment -import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleBinding import im.vector.app.features.room.RequireActiveMembershipViewEvents import im.vector.app.features.room.RequireActiveMembershipViewModel @AndroidEntryPoint -class RoomMemberProfileActivity : - VectorBaseActivity(), - ToolbarConfigurable { +class RoomMemberProfileActivity : VectorBaseActivity() { companion object { fun newIntent(context: Context, args: RoomMemberProfileArgs): Intent { @@ -63,10 +59,6 @@ class RoomMemberProfileActivity : } } - override fun configure(toolbar: MaterialToolbar) { - configureToolbar(toolbar) - } - private fun handleRoomLeft(roomLeft: RequireActiveMembershipViewEvents.RoomLeft) { if (roomLeft.leftMessage != null) { Toast.makeText(this, roomLeft.leftMessage, Toast.LENGTH_LONG).show() diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt index ca022edcab..545e9f7190 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt @@ -19,11 +19,11 @@ package im.vector.app.features.roommemberprofile import com.airbnb.epoxy.TypedEpoxyController import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.epoxy.profiles.buildProfileAction import im.vector.app.core.epoxy.profiles.buildProfileSection import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper @@ -282,7 +282,7 @@ class RoomMemberProfileController @Inject constructor( editable = false, divider = canBan, destructive = true, - title = stringProvider.getString(R.string.room_participants_action_kick), + title = stringProvider.getString(R.string.room_participants_action_remove), action = { callback?.onKickClicked(state.isSpace) } ) } diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt index 0243b44a8c..c68bfca973 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt @@ -47,6 +47,7 @@ import im.vector.app.databinding.DialogBaseEditTextBinding import im.vector.app.databinding.DialogShareQrCodeBinding import im.vector.app.databinding.FragmentMatrixProfileBinding import im.vector.app.databinding.ViewStubRoomMemberProfileHeaderBinding +import im.vector.app.features.analytics.plan.Screen import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer @@ -88,9 +89,15 @@ class RoomMemberProfileFragment @Inject constructor( override fun getMenuRes() = R.menu.vector_room_member_profile + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + analyticsScreenName = Screen.ScreenName.User + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupToolbar(views.matrixProfileToolbar) + .allowBack() val headerView = views.matrixProfileHeaderView.let { it.layoutResource = R.layout.view_stub_room_member_profile_header it.inflate() @@ -356,11 +363,11 @@ class RoomMemberProfileFragment @Inject constructor( .show( activity = requireActivity(), askForReason = true, - confirmationRes = if (isSpace) R.string.space_participants_kick_prompt_msg - else R.string.room_participants_kick_prompt_msg, - positiveRes = R.string.room_participants_action_kick, - reasonHintRes = R.string.room_participants_kick_reason, - titleRes = R.string.room_participants_kick_title + confirmationRes = if (isSpace) R.string.space_participants_remove_prompt_msg + else R.string.room_participants_remove_prompt_msg, + positiveRes = R.string.room_participants_action_remove, + reasonHintRes = R.string.room_participants_remove_reason, + titleRes = R.string.room_participants_remove_title ) { reason -> viewModel.handle(RoomMemberProfileAction.KickUser(reason)) } diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListEpoxyController.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListEpoxyController.kt index 8b49c694c4..3bfb210f8d 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListEpoxyController.kt @@ -22,7 +22,6 @@ import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.epoxy.errorWithRetryItem import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.resources.ColorProvider @@ -33,6 +32,7 @@ import im.vector.app.core.ui.list.genericItem import im.vector.app.core.ui.list.genericWithValueItem import im.vector.app.core.utils.DimensionConverter import im.vector.app.features.settings.VectorPreferences +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.span import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt index 8ee6967afa..c7f6e64f5c 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt @@ -18,7 +18,6 @@ package im.vector.app.features.roommemberprofile.devices import com.airbnb.epoxy.TypedEpoxyController import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.ItemStyle @@ -28,6 +27,7 @@ import im.vector.app.core.ui.list.genericWithValueItem import im.vector.app.core.utils.DimensionConverter import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationActionItem import im.vector.app.features.settings.VectorPreferences +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.span import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt index 7b92007692..4c6d2ed2e3 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt @@ -23,12 +23,10 @@ import android.widget.Toast import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel -import com.google.android.material.appbar.MaterialToolbar import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragmentToBackstack import im.vector.app.core.extensions.exhaustive -import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleBinding import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore @@ -47,8 +45,7 @@ import javax.inject.Inject @AndroidEntryPoint class RoomProfileActivity : - VectorBaseActivity(), - ToolbarConfigurable { + VectorBaseActivity() { companion object { @@ -157,8 +154,4 @@ class RoomProfileActivity : private fun openRoomNotificationSettings() { addFragmentToBackstack(views.simpleFragmentContainer, RoomNotificationSettingsFragment::class.java, roomProfileArgs) } - - override fun configure(toolbar: MaterialToolbar) { - configureToolbar(toolbar) - } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt index a1eeaf9286..e4630dae3f 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt @@ -19,7 +19,6 @@ package im.vector.app.features.roomprofile import com.airbnb.epoxy.TypedEpoxyController import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.epoxy.expandableTextItem import im.vector.app.core.epoxy.profiles.buildProfileAction import im.vector.app.core.epoxy.profiles.buildProfileSection @@ -32,6 +31,7 @@ import im.vector.app.features.home.ShortcutCreator import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod import im.vector.app.features.settings.VectorPreferences +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.image import me.gujun.android.span.span import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt index d82b853fe3..8acf53088d 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt @@ -44,6 +44,7 @@ import im.vector.app.core.utils.copyToClipboard import im.vector.app.core.utils.startSharePlainTextIntent import im.vector.app.databinding.FragmentMatrixProfileBinding import im.vector.app.databinding.ViewStubRoomProfileHeaderBinding +import im.vector.app.features.analytics.plan.Screen import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.RoomDetailPendingAction import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore @@ -88,6 +89,7 @@ class RoomProfileFragment @Inject constructor( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + analyticsScreenName = Screen.ScreenName.RoomSettings setFragmentResultListener(MigrateRoomBottomSheet.REQUEST_KEY) { _, bundle -> bundle.getString(MigrateRoomBottomSheet.BUNDLE_KEY_REPLACEMENT_ROOM)?.let { replacementRoomId -> roomDetailPendingActionStore.data = RoomDetailPendingAction.OpenRoom(replacementRoomId, closeCurrentRoom = true) @@ -107,6 +109,7 @@ class RoomProfileFragment @Inject constructor( headerViews = ViewStubRoomProfileHeaderBinding.bind(headerView) setupWaitingView() setupToolbar(views.matrixProfileToolbar) + .allowBack() setupRecyclerView() appBarStateChangeListener = MatrixItemAppBarStateChangeListener( headerView, diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasFragment.kt index 3128a590ce..e48ce54e6c 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasFragment.kt @@ -68,6 +68,7 @@ class RoomAliasFragment @Inject constructor( controller.callback = this setupToolbar(views.roomSettingsToolbar) + .allowBack() views.roomSettingsRecyclerView.configureWith(controller, hasFixedSize = true) views.waitingView.waitingStatusText.setText(R.string.please_wait) views.waitingView.waitingStatusText.isVisible = true diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListController.kt index 1e0572297b..cc332c0ba2 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListController.kt @@ -18,7 +18,6 @@ package im.vector.app.features.roomprofile.banned import com.airbnb.epoxy.TypedEpoxyController import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.epoxy.dividerItem import im.vector.app.core.epoxy.profiles.buildProfileSection import im.vector.app.core.epoxy.profiles.profileMatrixItemWithProgress @@ -27,6 +26,7 @@ import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.roomprofile.members.RoomMemberSummaryFilter +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListFragment.kt index c9fc889242..5cd4da009a 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListFragment.kt @@ -59,6 +59,7 @@ class RoomBannedMemberListFragment @Inject constructor( super.onViewCreated(view, savedInstanceState) roomMemberListController.callback = this setupToolbar(views.roomSettingsToolbar) + .allowBack() setupSearchView() views.roomSettingsRecyclerView.configureWith(roomMemberListController, hasFixedSize = true) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt index ba7e020ebe..d7a9ecf39b 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt @@ -58,6 +58,7 @@ class RoomMemberListFragment @Inject constructor( super.onViewCreated(view, savedInstanceState) roomMemberListController.callback = this setupToolbar(views.roomSettingGeneric.roomSettingsToolbar) + .allowBack() setupSearchView() setupInviteUsersButton() views.roomSettingGeneric.roomSettingsRecyclerView.configureWith(roomMemberListController, hasFixedSize = true) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/notifications/RoomNotificationSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/notifications/RoomNotificationSettingsFragment.kt index ce0fde32c6..320fdfd833 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/notifications/RoomNotificationSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/notifications/RoomNotificationSettingsFragment.kt @@ -50,6 +50,7 @@ class RoomNotificationSettingsFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupToolbar(views.roomSettingsToolbar) + .allowBack() roomNotificationSettingsController.callback = this views.roomSettingsRecyclerView.configureWith(roomNotificationSettingsController, hasFixedSize = true) setupWaitingView() diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/EditablePermission.kt b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/EditablePermission.kt index b083209f16..22d74ff7a3 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/EditablePermission.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/EditablePermission.kt @@ -99,7 +99,7 @@ sealed class EditablePermission(@StringRes val labelResId: Int, @StringRes val s class ChangeSettings : EditablePermission(R.string.room_permissions_change_settings) // Updates `content.kick` - class KickUsers : EditablePermission(R.string.room_permissions_kick_users) + class KickUsers : EditablePermission(R.string.room_permissions_remove_users) // Updates `content.ban` class BanUsers : EditablePermission(R.string.room_permissions_ban_users) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsFragment.kt index acf01321c9..0d5ac7dea8 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsFragment.kt @@ -58,6 +58,7 @@ class RoomPermissionsFragment @Inject constructor( controller.callback = this setupToolbar(views.roomSettingsToolbar) + .allowBack() views.roomSettingsRecyclerView.configureWith(controller, hasFixedSize = true) views.waitingView.waitingStatusText.setText(R.string.please_wait) views.waitingView.waitingStatusText.isVisible = true diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt index 3fa0bbaa5b..51f6b247d4 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt @@ -85,6 +85,7 @@ class RoomSettingsFragment @Inject constructor( setupRoomJoinRuleSharedActionViewModel() controller.callback = this setupToolbar(views.roomSettingsToolbar) + .allowBack() views.roomSettingsRecyclerView.configureWith(controller, hasFixedSize = true) views.waitingView.waitingStatusText.setText(R.string.please_wait) views.waitingView.waitingStatusText.isVisible = true diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleAdvancedController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleAdvancedController.kt index 5e963b4cbc..caf4b1843a 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleAdvancedController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleAdvancedController.kt @@ -18,7 +18,6 @@ package im.vector.app.features.roomprofile.settings.joinrule import com.airbnb.epoxy.TypedEpoxyController import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.ItemStyle @@ -26,6 +25,7 @@ import im.vector.app.core.ui.list.genericButtonItem import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedState +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import timber.log.Timber import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/advanced/ChooseRestrictedController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/advanced/ChooseRestrictedController.kt index b301b8c947..bbec3ca4a5 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/advanced/ChooseRestrictedController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/advanced/ChooseRestrictedController.kt @@ -22,13 +22,13 @@ import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.epoxy.noResultItem import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.spaces.manage.roomSelectionItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.util.MatrixItem import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt index 3716d9682c..3c1a763072 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt @@ -34,6 +34,7 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.saveMedia import im.vector.app.core.utils.shareMedia import im.vector.app.databinding.FragmentRoomUploadsBinding +import im.vector.app.features.analytics.plan.Screen import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.roomprofile.RoomProfileArgs @@ -54,6 +55,11 @@ class RoomUploadsFragment @Inject constructor( return FragmentRoomUploadsBinding.inflate(inflater, container, false) } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + analyticsScreenName = Screen.ScreenName.RoomUploads + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -68,6 +74,7 @@ class RoomUploadsFragment @Inject constructor( }.attach() setupToolbar(views.roomUploadsToolbar) + .allowBack() viewModel.observeViewEvents { when (it) { diff --git a/vector/src/main/java/im/vector/app/features/session/SessionListener.kt b/vector/src/main/java/im/vector/app/features/session/SessionListener.kt index c1ee0b527e..37db55959f 100644 --- a/vector/src/main/java/im/vector/app/features/session/SessionListener.kt +++ b/vector/src/main/java/im/vector/app/features/session/SessionListener.kt @@ -20,16 +20,21 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import im.vector.app.core.extensions.postLiveEvent import im.vector.app.core.utils.LiveEvent +import im.vector.app.features.analytics.AnalyticsTracker +import im.vector.app.features.analytics.extensions.toListOfPerformanceTimer import im.vector.app.features.call.vectorCallService import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.launch import org.matrix.android.sdk.api.failure.GlobalError import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.statistics.StatisticEvent import javax.inject.Inject import javax.inject.Singleton @Singleton -class SessionListener @Inject constructor() : Session.Listener { +class SessionListener @Inject constructor( + private val analyticsTracker: AnalyticsTracker +) : Session.Listener { private val _globalErrorLiveData = MutableLiveData>() val globalErrorLiveData: LiveData> @@ -45,6 +50,12 @@ class SessionListener @Inject constructor() : Session.Listener { } } + override fun onStatisticsEvent(session: Session, statisticEvent: StatisticEvent) { + statisticEvent.toListOfPerformanceTimer().forEach { + analyticsTracker.capture(it) + } + } + override fun onSessionStopped(session: Session) { session.coroutineScope.coroutineContext.cancelChildren() } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 148b7e3841..1903b3776a 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -187,6 +187,9 @@ class VectorPreferences @Inject constructor(private val context: Context) { private const val DID_ASK_TO_ENABLE_SESSION_PUSH = "DID_ASK_TO_ENABLE_SESSION_PUSH" private const val DID_PROMOTE_NEW_RESTRICTED_JOIN_RULE = "DID_PROMOTE_NEW_RESTRICTED_JOIN_RULE" + // Location Sharing + const val SETTINGS_PREF_ENABLE_LOCATION_SHARING = "SETTINGS_PREF_ENABLE_LOCATION_SHARING" + private const val MEDIA_SAVING_3_DAYS = 0 private const val MEDIA_SAVING_1_WEEK = 1 private const val MEDIA_SAVING_1_MONTH = 2 @@ -196,7 +199,7 @@ class VectorPreferences @Inject constructor(private val context: Context) { private const val TAKE_PHOTO_VIDEO_MODE = "TAKE_PHOTO_VIDEO_MODE" - private const val SETTINGS_LABS_ENABLE_POLLS = "SETTINGS_LABS_ENABLE_POLLS" + private const val SETTINGS_LABS_RENDER_LOCATIONS_IN_TIMELINE = "SETTINGS_LABS_RENDER_LOCATIONS_IN_TIMELINE" const val SETTINGS_LABS_ENABLE_THREAD_MESSAGES = "SETTINGS_LABS_ENABLE_THREAD_MESSAGES" // Possible values for TAKE_PHOTO_VIDEO_MODE @@ -993,8 +996,12 @@ class VectorPreferences @Inject constructor(private val context: Context) { } } - fun labsEnablePolls(): Boolean { - return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_POLLS, false) + fun isLocationSharingEnabled(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_PREF_ENABLE_LOCATION_SHARING, false) && BuildConfig.enableLocationSharing + } + + fun labsRenderLocationsInTimeline(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_LABS_RENDER_LOCATIONS_IN_TIMELINE, true) } fun areThreadMessagesEnabled(): Boolean { diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsActivity.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsActivity.kt index f502da24ff..725c4ceabc 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsActivity.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsActivity.kt @@ -63,7 +63,8 @@ class VectorSettingsActivity : VectorBaseActivity @Inject lateinit var session: Session override fun initUiAndData() { - configureToolbar(views.settingsToolbar) + setupToolbar(views.settingsToolbar) + .allowBack() if (isFirstCreation()) { // display the fragment diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt index 08d67067ec..7cefd20269 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt @@ -29,7 +29,9 @@ import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.toast -import im.vector.app.features.analytics.VectorAnalytics +import im.vector.app.features.analytics.AnalyticsTracker +import im.vector.app.features.analytics.plan.Screen +import im.vector.app.features.analytics.screen.ScreenEvent import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.session.Session @@ -37,6 +39,18 @@ import reactivecircus.flowbinding.android.view.clicks import timber.log.Timber abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), MavericksView { + /* ========================================================================================== + * Analytics + * ========================================================================================== */ + + protected var analyticsScreenName: Screen.ScreenName? = null + private var screenEvent: ScreenEvent? = null + + protected lateinit var analyticsTracker: AnalyticsTracker + + /* ========================================================================================== + * Activity + * ========================================================================================== */ val vectorActivity: VectorBaseActivity<*> by lazy { activity as VectorBaseActivity<*> @@ -47,7 +61,6 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), Maverick // members protected lateinit var session: Session protected lateinit var errorFormatter: ErrorFormatter - protected lateinit var analytics: VectorAnalytics /* ========================================================================================== * Views @@ -72,17 +85,23 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), Maverick super.onAttach(context) session = singletonEntryPoint.activeSessionHolder().getActiveSession() errorFormatter = singletonEntryPoint.errorFormatter() - analytics = singletonEntryPoint.analytics() + analyticsTracker = singletonEntryPoint.analyticsTracker() } override fun onResume() { super.onResume() Timber.i("onResume Fragment ${javaClass.simpleName}") + screenEvent = analyticsScreenName?.let { ScreenEvent(it) } vectorActivity.supportActionBar?.setTitle(titleRes) // find the view from parent activity mLoadingView = vectorActivity.findViewById(R.id.vector_settings_spinner_views) } + override fun onPause() { + super.onPause() + screenEvent?.send(analyticsTracker) + } + abstract fun bindPref() abstract var titleRes: Int diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt index 2e2fab06a3..50e32ae453 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt @@ -22,6 +22,7 @@ import android.widget.CheckedTextView import androidx.core.view.children import androidx.preference.Preference import com.google.android.material.dialog.MaterialAlertDialogBuilder +import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.dialogs.PhotoOrVideoDialog import im.vector.app.core.extensions.restart @@ -149,6 +150,8 @@ class VectorSettingsPreferencesFragment @Inject constructor( }) true } + + findPreference(VectorPreferences.SETTINGS_PREF_ENABLE_LOCATION_SHARING)?.isVisible = BuildConfig.enableLocationSharing } private fun updateTakePhotoOrVideoPreferenceSummary() { diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsRootFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsRootFragment.kt index 79eb0216ee..fb5d83239b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsRootFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsRootFragment.kt @@ -16,8 +16,10 @@ package im.vector.app.features.settings +import android.os.Bundle import im.vector.app.R import im.vector.app.core.preference.VectorPreference +import im.vector.app.features.analytics.plan.Screen import javax.inject.Inject class VectorSettingsRootFragment @Inject constructor() : VectorSettingsBaseFragment() { @@ -25,6 +27,11 @@ class VectorSettingsRootFragment @Inject constructor() : VectorSettingsBaseFragm override var titleRes: Int = R.string.title_activity_settings override val preferenceXmlRes = R.xml.vector_settings_root + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + analyticsScreenName = Screen.ScreenName.Settings + } + override fun bindPref() { tintIcons() } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt index 279499b7e9..31fce00f3c 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt @@ -51,6 +51,7 @@ import im.vector.app.core.utils.copyToClipboard import im.vector.app.core.utils.openFileSelection import im.vector.app.core.utils.toast import im.vector.app.databinding.DialogImportE2eKeysBinding +import im.vector.app.features.analytics.plan.Screen import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewActions import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewModel import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewState @@ -91,6 +92,11 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( private val analyticsConsentViewModel: AnalyticsConsentViewModel by fragmentViewModel() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + analyticsScreenName = Screen.ScreenName.SettingsSecurity + } + // cryptography private val mCryptographyCategory by lazy { findPreference(VectorPreferences.SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY)!! diff --git a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt index 5729e773b7..867526c009 100644 --- a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt @@ -31,6 +31,7 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentDeactivateAccountBinding import im.vector.app.features.MainActivity import im.vector.app.features.MainActivityArgs +import im.vector.app.features.analytics.plan.Screen import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.settings.VectorSettingsActivity import org.matrix.android.sdk.api.auth.data.LoginFlowTypes @@ -47,7 +48,7 @@ class DeactivateAccountFragment @Inject constructor() : VectorBaseFragment if (activityResult.resultCode == Activity.RESULT_OK) { when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) { - LoginFlowTypes.SSO -> { + LoginFlowTypes.SSO -> { viewModel.handle(DeactivateAccountAction.SsoAuthDone) } LoginFlowTypes.PASSWORD -> { @@ -63,6 +64,11 @@ class DeactivateAccountFragment @Inject constructor() : VectorBaseFragment("SETTINGS_DEFAULT")!! diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsKeywordAndMentionsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsKeywordAndMentionsNotificationPreferenceFragment.kt index fb1a357c30..b7cf7f6bbe 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsKeywordAndMentionsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsKeywordAndMentionsNotificationPreferenceFragment.kt @@ -25,6 +25,7 @@ import im.vector.app.core.preference.KeywordPreference import im.vector.app.core.preference.VectorCheckboxPreference import im.vector.app.core.preference.VectorPreference import im.vector.app.core.preference.VectorPreferenceCategory +import im.vector.app.features.analytics.plan.Screen import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -34,7 +35,7 @@ import org.matrix.android.sdk.api.pushrules.rest.PushRule import org.matrix.android.sdk.api.pushrules.toJson class VectorSettingsKeywordAndMentionsNotificationPreferenceFragment : - VectorSettingsPushRuleNotificationPreferenceFragment() { + VectorSettingsPushRuleNotificationPreferenceFragment() { override var titleRes: Int = R.string.settings_notification_mentions_and_keywords @@ -42,6 +43,11 @@ class VectorSettingsKeywordAndMentionsNotificationPreferenceFragment : private var keywordsHasFocus = false + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + analyticsScreenName = Screen.ScreenName.SettingsMentionsAndKeywords + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) session.getKeywords().observe(viewLifecycleOwner, this::updateWithKeywords) @@ -71,7 +77,7 @@ class VectorSettingsKeywordAndMentionsNotificationPreferenceFragment : val keywords = editKeywordPreference.keywords val newChecked = newValue as Boolean displayLoadingView() - updateKeywordPushRules(keywords, newChecked) { result -> + updateKeywordPushRules(keywords, newChecked) { result -> hideLoadingView() if (!isAdded) { return@updateKeywordPushRules @@ -88,7 +94,7 @@ class VectorSettingsKeywordAndMentionsNotificationPreferenceFragment : false } - editKeywordPreference.listener = object : KeywordPreference.Listener { + editKeywordPreference.listener = object : KeywordPreference.Listener { override fun onFocusDidChange(hasFocus: Boolean) { keywordsHasFocus = true } @@ -174,8 +180,8 @@ class VectorSettingsKeywordAndMentionsNotificationPreferenceFragment : } override val prefKeyToPushRuleId = mapOf( - "SETTINGS_PUSH_RULE_CONTAINING_MY_DISPLAY_NAME_PREFERENCE_KEY" to RuleIds.RULE_ID_CONTAIN_DISPLAY_NAME, - "SETTINGS_PUSH_RULE_CONTAINING_MY_USER_NAME_PREFERENCE_KEY" to RuleIds.RULE_ID_CONTAIN_USER_NAME, - "SETTINGS_PUSH_RULE_MESSAGES_CONTAINING_AT_ROOM_PREFERENCE_KEY" to RuleIds.RULE_ID_ROOM_NOTIF - ) + "SETTINGS_PUSH_RULE_CONTAINING_MY_DISPLAY_NAME_PREFERENCE_KEY" to RuleIds.RULE_ID_CONTAIN_DISPLAY_NAME, + "SETTINGS_PUSH_RULE_CONTAINING_MY_USER_NAME_PREFERENCE_KEY" to RuleIds.RULE_ID_CONTAIN_USER_NAME, + "SETTINGS_PUSH_RULE_MESSAGES_CONTAINING_AT_ROOM_PREFERENCE_KEY" to RuleIds.RULE_ID_ROOM_NOTIF + ) } diff --git a/vector/src/main/java/im/vector/app/features/settings/push/PushGateWayController.kt b/vector/src/main/java/im/vector/app/features/settings/push/PushGateWayController.kt index 3daeace09f..68c3e960c7 100644 --- a/vector/src/main/java/im/vector/app/features/settings/push/PushGateWayController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/push/PushGateWayController.kt @@ -18,9 +18,9 @@ package im.vector.app.features.settings.push import com.airbnb.epoxy.TypedEpoxyController import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import javax.inject.Inject class PushGateWayController @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/settings/push/PushRulesController.kt b/vector/src/main/java/im/vector/app/features/settings/push/PushRulesController.kt index ed6a31d070..68f288ffd3 100644 --- a/vector/src/main/java/im/vector/app/features/settings/push/PushRulesController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/push/PushRulesController.kt @@ -18,9 +18,9 @@ package im.vector.app.features.settings.push import com.airbnb.epoxy.TypedEpoxyController import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import javax.inject.Inject class PushRulesController @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsController.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsController.kt index f3c0469d79..d374357396 100644 --- a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsController.kt @@ -22,7 +22,6 @@ import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.epoxy.noResultItem import im.vector.app.core.error.ErrorFormatter @@ -38,6 +37,7 @@ import im.vector.app.features.discovery.settingsEditTextItem import im.vector.app.features.discovery.settingsInfoItem import im.vector.app.features.discovery.settingsInformationItem import im.vector.app.features.discovery.settingsSectionTitleItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.session.identity.ThreePid diff --git a/vector/src/main/java/im/vector/app/features/share/IncomingShareActivity.kt b/vector/src/main/java/im/vector/app/features/share/IncomingShareActivity.kt index 294f1d4d91..439d9b64fa 100644 --- a/vector/src/main/java/im/vector/app/features/share/IncomingShareActivity.kt +++ b/vector/src/main/java/im/vector/app/features/share/IncomingShareActivity.kt @@ -16,15 +16,13 @@ package im.vector.app.features.share -import com.google.android.material.appbar.MaterialToolbar import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.addFragment -import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleBinding @AndroidEntryPoint -class IncomingShareActivity : VectorBaseActivity(), ToolbarConfigurable { +class IncomingShareActivity : VectorBaseActivity() { override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater) @@ -35,8 +33,4 @@ class IncomingShareActivity : VectorBaseActivity(), Toolb addFragment(views.simpleFragmentContainer, IncomingShareFragment::class.java) } } - - override fun configure(toolbar: MaterialToolbar) { - configureToolbar(toolbar, displayBack = false) - } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt index e8c2c4bde6..95e4405da5 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt @@ -19,7 +19,6 @@ package im.vector.app.features.spaces import com.airbnb.epoxy.EpoxyController import im.vector.app.R import im.vector.app.RoomGroupingMethod -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem @@ -30,6 +29,7 @@ import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.list.UnreadCounterBadgeView import im.vector.app.group import im.vector.app.space +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.group.model.GroupSummary import org.matrix.android.sdk.api.session.room.model.Membership diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceAdd3pidEpoxyController.kt b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceAdd3pidEpoxyController.kt index 816931a7c1..1193ecb496 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceAdd3pidEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceAdd3pidEpoxyController.kt @@ -20,7 +20,6 @@ import android.text.InputType import com.airbnb.epoxy.TypedEpoxyController import com.google.android.material.textfield.TextInputLayout import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.ItemStyle @@ -28,6 +27,7 @@ import im.vector.app.core.ui.list.genericButtonItem import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericPillItem import im.vector.app.features.form.formEditTextItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import javax.inject.Inject class SpaceAdd3pidEpoxyController @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt index 3353e66b3c..4ef469500b 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt @@ -19,12 +19,12 @@ package im.vector.app.features.spaces.create import com.airbnb.epoxy.TypedEpoxyController import com.google.android.material.textfield.TextInputLayout import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.ItemStyle import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.features.form.formEditTextItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import javax.inject.Inject class SpaceDefaultRoomEpoxyController @Inject constructor( diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt index a22256c3e1..b25ae4f2c7 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt @@ -20,7 +20,6 @@ import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.mvrx.Fail import im.vector.app.R import im.vector.app.core.epoxy.TextListener -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.features.form.formEditTextItem @@ -28,6 +27,7 @@ import im.vector.app.features.form.formEditableSquareAvatarItem import im.vector.app.features.form.formMultiLineEditTextItem import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.roomdirectory.createroom.RoomAliasErrorFormatter +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.MatrixConstants import org.matrix.android.sdk.api.session.room.alias.RoomAliasError import org.matrix.android.sdk.api.util.MatrixItem diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt index 6dadce30e0..a2c1380098 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt @@ -24,7 +24,6 @@ import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Uninitialized import im.vector.app.R import im.vector.app.core.epoxy.ClickListener -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.epoxy.errorWithRetryItem import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.error.ErrorFormatter @@ -35,6 +34,7 @@ import im.vector.app.core.ui.list.genericEmptyWithActionItem import im.vector.app.core.ui.list.genericPillItem import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.list.spaceChildInfoItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.span import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError.Companion.M_UNRECOGNIZED diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt index 2c7da43988..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 @@ -104,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) @@ -166,13 +163,11 @@ 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) diff --git a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedActivity.kt index 6076388289..a3ce8cea31 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedActivity.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedActivity.kt @@ -26,7 +26,6 @@ import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.Success import com.airbnb.mvrx.viewModel -import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R @@ -34,15 +33,13 @@ import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.extensions.setTextOrHide -import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleLoadingBinding import im.vector.app.features.spaces.SpaceBottomSheetSettingsArgs import javax.inject.Inject @AndroidEntryPoint -class SpaceLeaveAdvancedActivity : VectorBaseActivity(), - ToolbarConfigurable { +class SpaceLeaveAdvancedActivity : VectorBaseActivity() { override fun getBinding(): ActivitySimpleLoadingBinding = ActivitySimpleLoadingBinding.inflate(layoutInflater) @@ -113,8 +110,4 @@ class SpaceLeaveAdvancedActivity : VectorBaseActivity(), - ToolbarConfigurable { +class SpaceManageActivity : VectorBaseActivity() { private lateinit var sharedDirectoryActionViewModel: RoomDirectorySharedActionViewModel @@ -188,8 +185,4 @@ class SpaceManageActivity : VectorBaseActivity(), } } } - - override fun configure(toolbar: MaterialToolbar) { - configureToolbar(toolbar) - } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsController.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsController.kt index 67c9f83498..54f19ce297 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsController.kt @@ -21,13 +21,13 @@ import com.airbnb.epoxy.VisibilityState import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Incomplete import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.epoxy.errorWithRetryItem import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.features.home.AvatarRenderer +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsFragment.kt index 125686d200..bbfa97511c 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsFragment.kt @@ -66,8 +66,11 @@ class SpaceManageRoomsFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + setupToolbar(views.addRoomToSpaceToolbar) - views.appBarTitle.text = getString(R.string.space_manage_rooms_and_spaces) + .setTitle(R.string.space_manage_rooms_and_spaces) + .allowBack() + views.createNewRoom.isVisible = false epoxyController.listener = this views.roomList.configureWith(epoxyController, hasFixedSize = true, dividerDrawable = R.drawable.divider_horizontal) @@ -111,14 +114,15 @@ class SpaceManageRoomsFragment @Inject constructor( epoxyController.setData(state) state.spaceSummary.invoke()?.let { - views.appBarSpaceInfo.text = it.displayName + toolbar?.subtitle = it.displayName } + if (state.selectedRooms.isNotEmpty()) { if (currentActionMode == null) { views.addRoomToSpaceToolbar.isVisible = true vectorBaseActivity.startSupportActionMode(this) } else { - currentActionMode?.title = "${state.selectedRooms.size} selected" + toolbar?.title = resources.getQuantityString(R.plurals.room_details_selected, state.selectedRooms.size, state.selectedRooms.size) } // views.addRoomToSpaceToolbar.isVisible = false // views.addRoomToSpaceToolbar.startActionMode(this) @@ -167,10 +171,10 @@ class SpaceManageRoomsFragment @Inject constructor( override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { when (item?.itemId) { - R.id.action_delete -> { + R.id.action_delete -> { handleDeleteSelection() } - R.id.action_mark_as_suggested -> { + R.id.action_mark_as_suggested -> { viewModel.handle(SpaceManageRoomViewAction.MarkAllAsSuggested(true)) } R.id.action_mark_as_not_suggested -> { diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt index a3d0252c19..266d08fd12 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt @@ -85,6 +85,7 @@ class SpaceSettingsFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupToolbar(views.roomSettingsToolbar) + .allowBack() // roomProfileSharedActionViewModel = activityViewModelProvider.get(RoomProfileSharedActionViewModel::class.java) // setupRoomHistoryVisibilitySharedActionViewModel() setupRoomJoinRuleSharedActionViewModel() diff --git a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleFragment.kt index c5cfed6974..5b2d6bed7b 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleFragment.kt @@ -64,9 +64,9 @@ class SpacePeopleFragment @Inject constructor( } override fun invalidate() = withState(membersViewModel) { memberListState -> - views.appBarTitle.text = getString(R.string.bottom_action_people) val memberCount = (memberListState.roomSummary.invoke()?.otherMemberIds?.size ?: 0) + 1 - views.appBarSpaceInfo.text = resources.getQuantityString(R.plurals.room_title_members, memberCount, memberCount) + + toolbar?.subtitle = resources.getQuantityString(R.plurals.room_title_members, memberCount, memberCount) // views.listBuildingProgress.isVisible = true epoxyController.setData(memberListState) } @@ -78,17 +78,12 @@ class SpacePeopleFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + + setupToolbar(views.addRoomToSpaceToolbar) + .allowBack() setupRecyclerView() setupSearchView() - views.addRoomToSpaceToolbar.navigationIcon = drawableProvider.getDrawable( - R.drawable.ic_close_24dp, - colorProvider.getColorFromAttribute(R.attr.vctr_content_primary) - ) - views.addRoomToSpaceToolbar.setNavigationOnClickListener { - sharedActionViewModel.post(SpacePeopleSharedAction.Dismiss) - } - viewModel.observeViewEvents { handleViewEvents(it) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleListController.kt b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleListController.kt index 62bd866cb1..1fbe9bbbf9 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleListController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleListController.kt @@ -18,7 +18,6 @@ package im.vector.app.features.spaces.people import com.airbnb.epoxy.TypedEpoxyController import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence import im.vector.app.core.epoxy.dividerItem import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.epoxy.profiles.profileMatrixItemWithPowerLevel @@ -31,6 +30,7 @@ import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.roomprofile.members.RoomMemberListCategories import im.vector.app.features.roomprofile.members.RoomMemberListViewState import im.vector.app.features.roomprofile.members.RoomMemberSummaryFilter +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.span import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.util.toMatrixItem diff --git a/vector/src/main/java/im/vector/app/features/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/res/drawable/bg_feature_icon.xml b/vector/src/main/res/drawable/bg_feature_icon.xml new file mode 100644 index 0000000000..299f1a4a2e --- /dev/null +++ b/vector/src/main/res/drawable/bg_feature_icon.xml @@ -0,0 +1,9 @@ + + + diff --git a/vector/src/main/res/drawable/bg_map_user_pin.xml b/vector/src/main/res/drawable/bg_map_user_pin.xml new file mode 100644 index 0000000000..148d3cfa29 --- /dev/null +++ b/vector/src/main/res/drawable/bg_map_user_pin.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/drawable/ic_attachment_location_white.xml b/vector/src/main/res/drawable/ic_attachment_location_white.xml new file mode 100644 index 0000000000..865362312b --- /dev/null +++ b/vector/src/main/res/drawable/ic_attachment_location_white.xml @@ -0,0 +1,9 @@ + + + diff --git a/vector/src/main/res/drawable/ic_communities.xml b/vector/src/main/res/drawable/ic_communities.xml deleted file mode 100644 index f550de8106..0000000000 --- a/vector/src/main/res/drawable/ic_communities.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - diff --git a/vector/src/main/res/drawable/ic_friends_and_family.xml b/vector/src/main/res/drawable/ic_friends_and_family.xml deleted file mode 100644 index d7ac86f240..0000000000 --- a/vector/src/main/res/drawable/ic_friends_and_family.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - diff --git a/vector/src/main/res/drawable/ic_share_external.xml b/vector/src/main/res/drawable/ic_share_external.xml new file mode 100644 index 0000000000..c4b78c8a83 --- /dev/null +++ b/vector/src/main/res/drawable/ic_share_external.xml @@ -0,0 +1,5 @@ + + + diff --git a/vector/src/main/res/drawable/ic_teams.xml b/vector/src/main/res/drawable/ic_teams.xml deleted file mode 100644 index 8745cfd2d4..0000000000 --- a/vector/src/main/res/drawable/ic_teams.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - diff --git a/vector/src/main/res/drawable/ic_use_case_communities.xml b/vector/src/main/res/drawable/ic_use_case_communities.xml new file mode 100644 index 0000000000..a511da6d2e --- /dev/null +++ b/vector/src/main/res/drawable/ic_use_case_communities.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/drawable/ic_use_case_friends.xml b/vector/src/main/res/drawable/ic_use_case_friends.xml new file mode 100644 index 0000000000..e4dea36774 --- /dev/null +++ b/vector/src/main/res/drawable/ic_use_case_friends.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/drawable/ic_use_case_teams.xml b/vector/src/main/res/drawable/ic_use_case_teams.xml new file mode 100644 index 0000000000..29b3def8cb --- /dev/null +++ b/vector/src/main/res/drawable/ic_use_case_teams.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/layout/activity_call.xml b/vector/src/main/res/layout/activity_call.xml index 2238f96c9f..5734e5f92a 100644 --- a/vector/src/main/res/layout/activity_call.xml +++ b/vector/src/main/res/layout/activity_call.xml @@ -41,10 +41,10 @@ android:layout_width="@dimen/call_pip_width" android:layout_height="@dimen/call_pip_height" android:layout_marginEnd="16dp" - app:layout_goneMarginEnd="0dp" app:cardCornerRadius="@dimen/call_pip_radius" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent"> + app:layout_constraintEnd_toEndOf="parent" + app:layout_goneMarginEnd="0dp"> + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_contacts_book.xml b/vector/src/main/res/layout/fragment_contacts_book.xml index ad23fc9f8f..ac983f9c6f 100644 --- a/vector/src/main/res/layout/fragment_contacts_book.xml +++ b/vector/src/main/res/layout/fragment_contacts_book.xml @@ -19,49 +19,8 @@ - - - - - - - - - - + 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 803ad700db..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,13 +182,13 @@ 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" />