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:
ariskotsomitopoulos 2022-01-27 17:11:26 +02:00
commit b2a2fe2710
463 changed files with 7410 additions and 2478 deletions

View File

@ -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

View File

@ -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: "/"

View File

@ -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

View File

@ -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 }}

View File

@ -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:

View File

@ -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']
})

View File

@ -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)
=======================================

View File

@ -7,7 +7,7 @@
# Element Android
Element Android is an Android Matrix Client provided by [Element](https://element.io/).
Element Android is an Android Matrix Client provided by [Element](https://element.io/). The app can be run on every Android devices with Android OS Lollipop and more (API 21).
It is a total rewrite of [Riot-Android](https://github.com/vector-im/riot-android) with a new user experience.
@ -51,4 +51,4 @@ Come chat with the community in the dedicated Matrix [room](https://matrix.to/#/
Issues are triaged by community members and the Android App Team, following the [triage process](https://github.com/vector-im/element-meta/wiki/Triage-process).
We use [issue labels](https://github.com/vector-im/element-meta/wiki/Issue-labelling) to sort all incoming issues.
We use [issue labels](https://github.com/vector-im/element-meta/wiki/Issue-labelling) to sort all incoming issues.

View File

@ -1 +0,0 @@
Explore Rooms overflow menu - content update include "Create room"

View File

@ -1 +0,0 @@
Fix sync timeout after returning from background

View File

@ -1 +0,0 @@
Enabling native support for window resizing

View File

@ -1 +0,0 @@
Fix integration tests and add a comment with results (still not perfect due to github actions resource limitations)

View File

@ -1 +0,0 @@
"/kick" command is replaced with "/remove". Also replaced all occurrences in string resources

View File

@ -1 +0,0 @@
Updates the onboarding carousel images, copy and improves the handling of different device sizes

View File

@ -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.

View File

@ -1 +0,0 @@
Disabling onboarding automatic carousel transitions on user interaction

View File

@ -1 +0,0 @@
Locking phones to portrait during the FTUE onboarding

View File

@ -1 +0,0 @@
Add signing config for the release buildType. No secret added

View File

@ -1 +0,0 @@
Adds a messaging use case screen to the FTUE onboarding

View File

@ -1 +0,0 @@
Fix a wrong network error issue in the Legals screen

View File

@ -1 +0,0 @@
Remove unused module matrix-sdk-android-rx and do some cleanup

View File

@ -1 +0,0 @@
Prevent Alerts to be displayed in the automatically displayed analytics opt-in screen

View File

@ -1 +0,0 @@
Improves local echo blinking when non room events received

1
changelog.d/5062.bugfix Normal file
View File

@ -0,0 +1 @@
Show the legal mention of mapbox when sharing location

1
changelog.d/5067.bugfix Normal file
View File

@ -0,0 +1 @@
Poll cannot end in some unencrypted rooms

View File

@ -1 +0,0 @@
Fix for stuck local event messages at the bottom of the screen

View File

@ -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 : [

View File

@ -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',

View File

@ -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).

View File

@ -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

View File

@ -0,0 +1,2 @@
Hauptänderungen: Bugfixes!
Alle Änderungen: https://github.com/vector-im/element-android/releases/tag/v1.3.11

View File

@ -0,0 +1,2 @@
Hauptänderungen: Bugfixes!
Alle Änderungen: https://github.com/vector-im/element-android/releases/tag/v1.3.12

View File

@ -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

View File

@ -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

View File

@ -1 +1 @@
Element - turvaline sõnumiklient
Element

View File

@ -0,0 +1,2 @@
Principaux changements pour cette version : prise en charge des sondages (dans les labs). Nouvel affichage des prévisualisations dURL
Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.3.10

View File

@ -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

View File

@ -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

View File

@ -1 +1 @@
Element - Biztonságos üzenetküldő
Element

View File

@ -0,0 +1 @@
Element

View File

@ -1 +0,0 @@
Element - Bezpieczny Komunikator

View File

@ -1 +1 @@
Element - Безопасный мессенджер
Element

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1 +1 @@
Element - Shkëmbyes i Sigurt Mesazhesh
Element

View File

@ -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())
}
}

View File

@ -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

View File

@ -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

1
library/jsonviewer/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -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
}

View File

@ -0,0 +1 @@
<manifest package="org.billcarsonfr.jsonviewer" />

View File

@ -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 }
}
}
}

View File

@ -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()
)
}
}
}

View File

@ -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
)
)
}
}
}
}
}

View File

@ -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)
}
}
}
}
}
}

View File

@ -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)
)
}
}

View File

@ -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))
}
}
}
}

View File

@ -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()
}
}

View File

@ -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
}
}
}
}
}

View File

@ -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" />

View File

@ -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>

View File

@ -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" />

View File

@ -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="&quot;Title&quot;: &quot;example glossary&quot;" />
</LinearLayout>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,3 @@
<resources>
<string name="copy_value">Copy Value</string>
</resources>

View File

@ -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)
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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()}\""

View File

@ -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()
}
/**

View File

@ -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)
}

View File

@ -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.
*/

View File

@ -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
)

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
)

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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()
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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,

View File

@ -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,

View File

@ -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