Merge branch 'develop' into feature/aris/threads
# Conflicts: # matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt # tools/check/forbidden_strings_in_code.txt # vector/build.gradle # vector/src/main/java/im/vector/app/core/di/FragmentModule.kt # vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt # vector/src/main/java/im/vector/app/features/command/Command.kt # vector/src/main/java/im/vector/app/features/command/CommandParser.kt # vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt # vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt # vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt # vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt # vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt # vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt # vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt # vector/src/main/java/im/vector/app/features/navigation/Navigator.kt # vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt # vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt # vector/src/main/res/layout/fragment_timeline.xml # vector/src/main/res/xml/vector_settings_labs.xml
This commit is contained in:
commit
b2a2fe2710
|
@ -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
|
||||
|
|
|
@ -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: "/"
|
||||
|
|
|
@ -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
|
|
@ -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 }}
|
|
@ -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:
|
||||
|
|
|
@ -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']
|
||||
})
|
66
CHANGES.md
66
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)
|
||||
=======================================
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
Explore Rooms overflow menu - content update include "Create room"
|
|
@ -1 +0,0 @@
|
|||
Fix sync timeout after returning from background
|
|
@ -1 +0,0 @@
|
|||
Enabling native support for window resizing
|
|
@ -1 +0,0 @@
|
|||
Fix integration tests and add a comment with results (still not perfect due to github actions resource limitations)
|
|
@ -1 +0,0 @@
|
|||
"/kick" command is replaced with "/remove". Also replaced all occurrences in string resources
|
|
@ -1 +0,0 @@
|
|||
Updates the onboarding carousel images, copy and improves the handling of different device sizes
|
|
@ -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.
|
|
@ -1 +0,0 @@
|
|||
Disabling onboarding automatic carousel transitions on user interaction
|
|
@ -1 +0,0 @@
|
|||
Locking phones to portrait during the FTUE onboarding
|
|
@ -1 +0,0 @@
|
|||
Add signing config for the release buildType. No secret added
|
|
@ -1 +0,0 @@
|
|||
Adds a messaging use case screen to the FTUE onboarding
|
|
@ -1 +0,0 @@
|
|||
Fix a wrong network error issue in the Legals screen
|
|
@ -1 +0,0 @@
|
|||
Remove unused module matrix-sdk-android-rx and do some cleanup
|
|
@ -1 +0,0 @@
|
|||
Prevent Alerts to be displayed in the automatically displayed analytics opt-in screen
|
|
@ -1 +0,0 @@
|
|||
Improves local echo blinking when non room events received
|
|
@ -0,0 +1 @@
|
|||
Show the legal mention of mapbox when sharing location
|
|
@ -0,0 +1 @@
|
|||
Poll cannot end in some unencrypted rooms
|
|
@ -1 +0,0 @@
|
|||
Fix for stuck local event messages at the bottom of the screen
|
|
@ -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 : [
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
|||
Hauptänderungen: Bugfixes!
|
||||
Alle Änderungen: https://github.com/vector-im/element-android/releases/tag/v1.3.11
|
|
@ -0,0 +1,2 @@
|
|||
Hauptänderungen: Bugfixes!
|
||||
Alle Änderungen: https://github.com/vector-im/element-android/releases/tag/v1.3.12
|
|
@ -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
|
|
@ -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
|
|
@ -1 +1 @@
|
|||
Element - turvaline sõnumiklient
|
||||
Element
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -1 +1 @@
|
|||
Element - Biztonságos üzenetküldő
|
||||
Element
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Element
|
|
@ -1 +0,0 @@
|
|||
Element - Bezpieczny Komunikator
|
|
@ -1 +1 @@
|
|||
Element - Безопасный мессенджер
|
||||
Element
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -1 +1 @@
|
|||
Element - Shkëmbyes i Sigurt Mesazhesh
|
||||
Element
|
||||
|
|
|
@ -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 <E> MutableCollection<E>.removeIfCompat(predicate: (E) -> Boolean) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
removeIf(predicate)
|
||||
} else {
|
||||
removeAll(filter(predicate).toSet())
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
/build
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<manifest package="org.billcarsonfr.jsonviewer" />
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<JSonViewerState>() {
|
||||
|
||||
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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String, JSonViewerModel>()
|
||||
|
||||
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<JSonViewerModel>()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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<JSonViewerObject> = Uninitialized
|
||||
) : MavericksState
|
||||
|
||||
internal class JSonViewerViewModel(initialState: JSonViewerState) :
|
||||
MavericksViewModel<JSonViewerState>(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<JSonViewerViewModel, JSonViewerState> {
|
||||
|
||||
@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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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<ValueItem.Holder>() {
|
||||
|
||||
@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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/fragmentContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.airbnb.epoxy.EpoxyRecyclerView
|
||||
android:id="@+id/jvRecyclerView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:fadeScrollbars="false"
|
||||
android:scrollbars="vertical"
|
||||
tools:itemCount="5"
|
||||
tools:listitem="@layout/item_jv_base_value" />
|
||||
|
||||
</HorizontalScrollView>
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.airbnb.epoxy.EpoxyRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/jvRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fadeScrollbars="false"
|
||||
android:scrollbars="vertical"
|
||||
tools:itemCount="5"
|
||||
tools:listitem="@layout/item_jv_base_value" />
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/jvBaseLayout"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
tools:paddingLeft="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/jvValueText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text=""Title": "example glossary"" />
|
||||
|
||||
</LinearLayout>
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/copy_value"
|
||||
android:title="@string/copy_value" />
|
||||
|
||||
</menu>
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<color name="key_color">#FF006700</color>
|
||||
<color name="string_color">#FF040091</color>
|
||||
<color name="bool_color">#FF980000</color>
|
||||
<color name="number_color">#FF1700FF</color>
|
||||
<color name="base_color">#FF000000</color>
|
||||
<color name="secondary_color">#FFAAAAAA</color>
|
||||
|
||||
</resources>
|
|
@ -0,0 +1,3 @@
|
|||
<resources>
|
||||
<string name="copy_value">Copy Value</string>
|
||||
</resources>
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -1,7 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:endColor="?vctr_system"
|
||||
android:startColor="#000000" />
|
||||
<solid android:color="?android:colorBackground" />
|
||||
</shape>
|
|
@ -6,10 +6,12 @@
|
|||
<item name="elevation">0dp</item>
|
||||
|
||||
<!-- main text -->
|
||||
<item name="titleTextStyle">@style/Widget.Vector.TextView.ActionBarTitle</item>
|
||||
<item name="titleTextAppearance">@style/TextAppearance.Vector.Widget.ActionBarTitle</item>
|
||||
|
||||
<!-- sub text -->
|
||||
<item name="subtitleTextStyle">@style/Widget.Vector.TextView.ActionBarSubTitle</item>
|
||||
<item name="subtitleTextAppearance">@style/TextAppearance.Vector.Widget.ActionBarSubTitle</item>
|
||||
|
||||
<item name="navigationIconTint">?vctr_content_secondary</item>
|
||||
</style>
|
||||
|
||||
<!-- Default toolbar style -->
|
||||
|
@ -22,16 +24,18 @@
|
|||
|
||||
<!-- Toolbar text style -->
|
||||
<!-- main text -->
|
||||
<style name="Widget.Vector.TextView.ActionBarTitle" parent="TextAppearance.AppCompat.Widget.ActionBar.Title">
|
||||
<style name="TextAppearance.Vector.Widget.ActionBarTitle" parent="TextAppearance.AppCompat.Widget.ActionBar.Title">
|
||||
<item name="android:textColor">?vctr_content_primary</item>
|
||||
<item name="android:fontFamily">"sans-serif-medium"</item>
|
||||
<item name="android:textSize">20sp</item>
|
||||
<item name="android:fontFamily">sans-serif-medium</item>
|
||||
<item name="fontFamily">sans-serif-medium</item>
|
||||
<item name="android:textSize">18sp</item>
|
||||
</style>
|
||||
|
||||
<!-- sub text -->
|
||||
<style name="Widget.Vector.TextView.ActionBarSubTitle" parent="TextAppearance.AppCompat.Widget.ActionBar.Subtitle">
|
||||
<item name="android:textColor">?vctr_content_primary</item>
|
||||
<item name="android:fontFamily">"sans-serif-medium"</item>
|
||||
<style name="TextAppearance.Vector.Widget.ActionBarSubTitle" parent="TextAppearance.AppCompat.Widget.ActionBar.Subtitle">
|
||||
<item name="android:textColor">?vctr_content_secondary</item>
|
||||
<item name="android:fontFamily">sans-serif</item>
|
||||
<item name="fontFamily">sans-serif</item>
|
||||
<item name="android:textSize">12sp</item>
|
||||
</style>
|
||||
|
||||
|
|
|
@ -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()}\""
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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<PollAnswer>? = null
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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<String>): Cancelable
|
||||
|
||||
/**
|
||||
* Edit a text message body. Limited to "m.text" contentType
|
||||
* @param targetEvent The event to edit
|
||||
|
|
|
@ -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<String>): Cancelable
|
||||
fun sendPoll(pollType: PollType, question: String, options: List<String>): 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
|
||||
|
|
|
@ -135,7 +135,7 @@ fun TimelineEvent.getEditedEventId(): String? {
|
|||
fun TimelineEvent.getLastMessageContent(): MessageContent? {
|
||||
return when (root.getClearType()) {
|
||||
EventType.STICKER -> root.getClearContent().toModel<MessageStickerContent>()
|
||||
EventType.POLL_START -> root.getClearContent().toModel<MessagePollContent>()
|
||||
EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessagePollContent>()
|
||||
else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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 <DATA> 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<MessagePollResponseContent>(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<PollSummaryContent>()
|
||||
?.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<PowerLevelsContent>()
|
||||
?.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<PowerLevelsContent>()
|
||||
?.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,
|
||||
|
|
|
@ -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<String>): Cancelable {
|
||||
return eventEditor.editPoll(targetEvent, pollType, question, options)
|
||||
}
|
||||
|
||||
override fun editTextMessage(targetEvent: TimelineEvent,
|
||||
msgType: String,
|
||||
newBodyText: CharSequence,
|
||||
|
|
|
@ -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<String>): 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,
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue