diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 5ccd00a02b1..7eff52cc9fe 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -14,6 +14,7 @@ jobs: - name: Run code quality check suite run: ./tools/check/check_code_quality.sh +# ktlint for all the modules ktlint: name: Kotlin Linter runs-on: ubuntu-latest @@ -23,12 +24,66 @@ jobs: run: | ./gradlew ktlintCheck --continue - name: Upload reports + if: always() uses: actions/upload-artifact@v2 with: name: ktlinting-report - path: vector/build/reports/ktlint/*.* + path: | + */build/reports/ktlint/ktlint*/ktlint*.txt + - name: Handle Results + if: always() + id: ktlint-results + run: | + results="$(cat */*/build/reports/ktlint/ktlint*/ktlint*.txt */build/reports/ktlint/ktlint*/ktlint*.txt | sed -r "s/\x1B\[([0-9]{1,3}(;[0-9]{1,2})?)?[mGK]//g")" + if [ -z "$results" ]; then + echo "::set-output name=add_comment::false" + else + body="👎\`Failed${results}\`" + body="${body//'%'/'%25'}" + body="${body//$'\n'/'%0A'}" + body="${body//$'\r'/'%0D'}" + body="$( echo $body | sed 's/\/home\/runner\/work\/element-android\/element-android\//\`\`/g')" + body="$( echo $body | sed 's/\/src\/main\/java\// 🔸 /g')" + body="$( echo $body | sed 's/im\/vector\/app\///g')" + body="$( echo $body | sed 's/im\/vector\/lib\/attachmentviewer\///g')" + body="$( echo $body | sed 's/im\/vector\/lib\/multipicker\///g')" + body="$( echo $body | sed 's/im\/vector\/lib\///g')" + body="$( echo $body | sed 's/org\/matrix\/android\/sdk\///g')" + body="$( echo $body | sed 's/\/src\/androidTest\/java\// 🔸 /g')" + echo "::set-output name=add_comment::true" + echo "::set-output name=body::$body" + fi + - name: Find Comment + if: always() + uses: peter-evans/find-comment@v1 + id: fc + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: 'github-actions[bot]' + body-includes: Ktlint Results + - name: Add comment if needed + if: always() && steps.ktlint-results.outputs.add_comment == 'true' + uses: peter-evans/create-or-update-comment@v1 + with: + comment-id: ${{ steps.fc.outputs.comment-id }} + issue-number: ${{ github.event.pull_request.number }} + body: | + ### Ktlint Results + + ${{ steps.ktlint-results.outputs.body }} + edit-mode: replace + - name: Delete comment if needed + if: always() && steps.fc.outputs.comment-id != '' && steps.ktlint-results.outputs.add_comment == 'false' + uses: actions/github-script@v3 + with: + script: | + github.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: ${{ steps.fc.outputs.comment-id }} + }) -# Lint for main module and all the other modules +# Lint for main module android-lint: name: Android Linter runs-on: ubuntu-latest @@ -45,6 +100,7 @@ jobs: - name: Lint analysis run: ./gradlew clean :vector:lint --stacktrace - name: Upload reports + if: always() uses: actions/upload-artifact@v2 with: name: lint-report @@ -73,8 +129,8 @@ jobs: - name: Lint ${{ matrix.target }} release run: ./gradlew clean lint${{ matrix.target }}Release --stacktrace - name: Upload ${{ matrix.target }} linting report - uses: actions/upload-artifact@v2 if: always() + uses: actions/upload-artifact@v2 with: name: release-lint-report-${{ matrix.target }} path: | diff --git a/.github/workflows/triage-incoming.yml b/.github/workflows/triage-incoming.yml index 4ecc8244241..6a22bf5223d 100644 --- a/.github/workflows/triage-incoming.yml +++ b/.github/workflows/triage-incoming.yml @@ -7,6 +7,8 @@ on: jobs: automate-project-columns: runs-on: ubuntu-latest + # Skip in forks + if: github.repository == 'vector-im/element-android' steps: - uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488 with: diff --git a/.github/workflows/triage-move-labelled.yml b/.github/workflows/triage-move-labelled.yml index 67c4e9dbab6..e2f5cc32e91 100644 --- a/.github/workflows/triage-move-labelled.yml +++ b/.github/workflows/triage-move-labelled.yml @@ -3,11 +3,13 @@ name: Move labelled issues to correct boards and columns on: issues: types: [labeled] - + jobs: move_needs_info_issues: name: X-Needs-Info issues to Need info column on triage board runs-on: ubuntu-latest + # Skip in forks + if: github.repository == 'vector-im/element-android' steps: - uses: konradpabjan/move-labeled-or-milestoned-issue@219d384e03fa4b6460cd24f9f37d19eb033a4338 with: @@ -19,15 +21,17 @@ jobs: add_priority_design_issues_to_project: name: P1 X-Needs-Design to Design project board runs-on: ubuntu-latest + # Skip in forks if: > - contains(github.event.issue.labels.*.name, 'X-Needs-Design') && - (contains(github.event.issue.labels.*.name, 'S-Critical') && - (contains(github.event.issue.labels.*.name, 'O-Frequent') || + github.repository == 'vector-im/element-android' && + contains(github.event.issue.labels.*.name, 'X-Needs-Design') && + (contains(github.event.issue.labels.*.name, 'S-Critical') && + (contains(github.event.issue.labels.*.name, 'O-Frequent') || contains(github.event.issue.labels.*.name, 'O-Occasional')) || - 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, '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')) steps: - uses: octokit/graphql-action@v2.x id: add_to_project @@ -47,36 +51,40 @@ 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 -# if: > -# 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 }} + # 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 }} move_voice-message_issues: name: A-Voice Messages to voice message board runs-on: ubuntu-latest + # Skip in forks if: > - contains(github.event.issue.labels.*.name, 'A-Voice Messages') + github.repository == 'vector-im/element-android' && + contains(github.event.issue.labels.*.name, 'A-Voice Messages') steps: - uses: octokit/graphql-action@v2.x with: @@ -98,8 +106,10 @@ jobs: move_threads_issues: name: A-Threads to Thread board runs-on: ubuntu-latest + # Skip in forks if: > - contains(github.event.issue.labels.*.name, 'A-Threads') + github.repository == 'vector-im/element-android' && + contains(github.event.issue.labels.*.name, 'A-Threads') steps: - uses: octokit/graphql-action@v2.x with: @@ -121,8 +131,10 @@ jobs: move_message_bubbles_issues: name: A-Message-Bubbles to Message bubbles board runs-on: ubuntu-latest + # Skip in forks if: > - contains(github.event.issue.labels.*.name, 'A-Message-Bubbles') + github.repository == 'vector-im/element-android' && + contains(github.event.issue.labels.*.name, 'A-Message-Bubbles') steps: - uses: octokit/graphql-action@v2.x with: diff --git a/.github/workflows/triage-move-unlabelled.yml b/.github/workflows/triage-move-unlabelled.yml index 94bd049b919..eb90cdb5033 100644 --- a/.github/workflows/triage-move-unlabelled.yml +++ b/.github/workflows/triage-move-unlabelled.yml @@ -3,14 +3,15 @@ name: Move unlabelled from needs info columns to triaged on: issues: types: [unlabeled] - + jobs: Move_Unabeled_Issue_On_Project_Board: name: Move no longer X-Needs-Info issues to Triaged runs-on: ubuntu-latest + # Skip in forks if: > - ${{ - !contains(github.event.issue.labels.*.name, 'X-Needs-Info') }} + github.repository == 'vector-im/element-android' && + !contains(github.event.issue.labels.*.name, 'X-Needs-Info') env: BOARD_NAME: "Issue triage" OWNER: ${{ github.repository_owner }} diff --git a/.github/workflows/triage-priority-bugs.yml b/.github/workflows/triage-priority-bugs.yml index 976879a3aee..daea78de191 100644 --- a/.github/workflows/triage-priority-bugs.yml +++ b/.github/workflows/triage-priority-bugs.yml @@ -7,23 +7,25 @@ on: jobs: p1_issues_to_team_workboard: runs-on: ubuntu-latest + # Skip in forks if: > - (!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') && - !contains(github.event.issue.labels.*.name, 'A-E2EE-SAS-Verification') && - !contains(github.event.issue.labels.*.name, 'A-Spaces') && - !contains(github.event.issue.labels.*.name, 'A-Spaces-Settings') && - !contains(github.event.issue.labels.*.name, 'A-Subspaces')) && - (contains(github.event.issue.labels.*.name, 'T-Defect') && - contains(github.event.issue.labels.*.name, 'S-Critical') && - (contains(github.event.issue.labels.*.name, 'O-Frequent') || + github.repository == 'vector-im/element-android' && + (!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') && + !contains(github.event.issue.labels.*.name, 'A-E2EE-SAS-Verification') && + !contains(github.event.issue.labels.*.name, 'A-Spaces') && + !contains(github.event.issue.labels.*.name, 'A-Spaces-Settings') && + !contains(github.event.issue.labels.*.name, 'A-Subspaces')) && + (contains(github.event.issue.labels.*.name, 'T-Defect') && + contains(github.event.issue.labels.*.name, 'S-Critical') && + (contains(github.event.issue.labels.*.name, 'O-Frequent') || contains(github.event.issue.labels.*.name, 'O-Occasional')) || - 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, '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')) steps: - uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488 with: @@ -33,20 +35,22 @@ jobs: P1_issues_to_crypto_team_workboard: runs-on: ubuntu-latest + # Skip in forks if: > - (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') || - contains(github.event.issue.labels.*.name, 'A-E2EE-SAS-Verification')) && - (contains(github.event.issue.labels.*.name, 'T-Defect') && - contains(github.event.issue.labels.*.name, 'S-Critical') && - (contains(github.event.issue.labels.*.name, 'O-Frequent') || + github.repository == 'vector-im/element-android' && + (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') || + contains(github.event.issue.labels.*.name, 'A-E2EE-SAS-Verification')) && + (contains(github.event.issue.labels.*.name, 'T-Defect') && + contains(github.event.issue.labels.*.name, 'S-Critical') && + (contains(github.event.issue.labels.*.name, 'O-Frequent') || contains(github.event.issue.labels.*.name, 'O-Occasional')) || - 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, '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')) steps: - uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488 with: diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml index a2e408b50de..f99842f0671 100644 --- a/.idea/dictionaries/bmarty.xml +++ b/.idea/dictionaries/bmarty.xml @@ -36,6 +36,7 @@ ssss sygnal threepid + uisi unpublish unwedging vctr diff --git a/CHANGES.md b/CHANGES.md index fbefb75e06c..75d290d2e22 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,73 @@ +Changes in Element v1.3.13 (2022-01-11) +======================================= + +Features ✨ +---------- + - Updates onboarding splash screen to have a dedicated sign in button and removes the dual purpose sign in/up stage ([#4382](https://github.com/vector-im/element-android/issues/4382)) + - Display Analytics opt-in screen at first start-up of the app ([#4892](https://github.com/vector-im/element-android/issues/4892)) + - New attachment picker UI ([#3444](https://github.com/vector-im/element-android/issues/3444)) + - Add labs support for rendering LaTeX maths (MSC2191) ([#2133](https://github.com/vector-im/element-android/issues/2133)) + - Allow changing nick colors from the member detail screen ([#2614](https://github.com/vector-im/element-android/issues/2614)) + - Analytics: Track Errors ([#4719](https://github.com/vector-im/element-android/issues/4719)) + - Change internal timeline management. ([#4405](https://github.com/vector-im/element-android/issues/4405)) + - Translate the error observed when the user is not allowed to join a room ([#4847](https://github.com/vector-im/element-android/issues/4847)) + +Bugfixes 🐛 +---------- + - Stop using CharSequence as EpoxyAttribute because it can lead to crash if the CharSequence mutates during rendering. ([#4837](https://github.com/vector-im/element-android/issues/4837)) + - Better handling of misconfigured room encryption ([#4711](https://github.com/vector-im/element-android/issues/4711)) + - Fix message replies/quotes to respect newlines. ([#4540](https://github.com/vector-im/element-android/issues/4540)) + - Polls: unable to create a poll with more than 10 answers ([#4735](https://github.com/vector-im/element-android/issues/4735)) + - Fix for broken unread message indicator on the room list when there are no messages in the room. ([#4749](https://github.com/vector-im/element-android/issues/4749)) + - Fixes newer emojis rendering strangely when inserting from the system keyboard ([#4756](https://github.com/vector-im/element-android/issues/4756)) + - Fixing unable to change change avatar in some scenarios ([#4767](https://github.com/vector-im/element-android/issues/4767)) + - Tentative fix for the speaker being used instead of earpiece for the outgoing call ringtone on lineage os ([#4781](https://github.com/vector-im/element-android/issues/4781)) + - Fixing crashes when quickly scrolling or restoring the room timeline ([#4789](https://github.com/vector-im/element-android/issues/4789)) + - Fixing encrypted non message events showing up as notification messages (eg when a participant joins, mutes or leaves a voice call) ([#4804](https://github.com/vector-im/element-android/issues/4804)) + +SDK API changes ⚠️ +------------------ + - Introduce method onStateUpdated on Timeline.Callback ([#4405](https://github.com/vector-im/element-android/issues/4405)) + - Support tagged events in Room Account Data (MSC2437) ([#4753](https://github.com/vector-im/element-android/issues/4753)) + +Other changes +------------- + - Workaround to fetch all the pending toDevice events from a Synapse homeserver ([#4612](https://github.com/vector-im/element-android/issues/4612)) + - Toolbar is added to a views with QR code scan ([#4644](https://github.com/vector-im/element-android/issues/4644)) + - Open share UI provides by the system when sharing media or text. ([#4745](https://github.com/vector-im/element-android/issues/4745)) + - Cleaning rendering of state events in timeline ([#4747](https://github.com/vector-im/element-android/issues/4747)) + - Enabling new FTUE Auth onboarding base, includes the "I already have an account" button in the splash ([#4872](https://github.com/vector-im/element-android/issues/4872)) + - Olm lib is now hosted in MavenCentral - upgrade to 3.2.10 ([#4882](https://github.com/vector-im/element-android/issues/4882)) + - Remove deprecated experimental restricted space lab option ([#4889](https://github.com/vector-im/element-android/issues/4889)) + - Add ktlint results on github as a comment only on fail ([#4888](https://github.com/vector-im/element-android/issues/4888)) + - Fix github actions ktlint reports and publish results on PR as comment ([#4864](https://github.com/vector-im/element-android/issues/4864)) + + +Changes in Element v1.3.12 (2021-12-20) +======================================= + +Bugfixes 🐛 +---------- + - Fixing emoji related crashes on android 8.1.1 and below ([#4769](https://github.com/vector-im/element-android/issues/4769)) + + +Changes in Element v1.3.11 (2021-12-17) +======================================= + +Bugfixes 🐛 +---------- + - Fixing proximity sensor still being active after a call ([#2467](https://github.com/vector-im/element-android/issues/2467)) + - Fix name and shield are truncated in the room detail screen ([#4700](https://github.com/vector-im/element-android/issues/4700)) + - Call banner: center text vertically ([#4710](https://github.com/vector-im/element-android/issues/4710)) + - Fixes unable to render messages by allowing them to render whilst the emoji library is initialising ([#4733](https://github.com/vector-im/element-android/issues/4733)) + - Fix app crash uppon long press on a reply event ([#4742](https://github.com/vector-im/element-android/issues/4742)) + - Fixes crash when launching rooms which contain emojis in the emote content on android 12+ ([#4743](https://github.com/vector-im/element-android/issues/4743)) + +Other changes +------------- + - Avoids leaking the activity windows when loading dialogs are displaying ([#4713](https://github.com/vector-im/element-android/issues/4713)) + + Changes in Element v1.3.10 (2021-12-14) ======================================= diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dbc0ce9b725..22d12ac6631 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -139,7 +139,7 @@ If a string is not used anymore, it should be removed from the resource, but ple Instead, please comment the original string with: ```xml - + ``` The string will be removed during the next sync with Weblate. diff --git a/README.md b/README.md index a085bf7da12..0345c501460 100644 --- a/README.md +++ b/README.md @@ -46,3 +46,9 @@ If you would like to receive releases more quickly (bearing in mind that they ma Please refer to [CONTRIBUTING.md](https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md) if you want to contribute on Matrix Android projects! Come chat with the community in the dedicated Matrix [room](https://matrix.to/#/#element-android:matrix.org). + +## Triaging issues + +Issues are triaged by community members and the Android App Team, following the [triage process](https://github.com/vector-im/element-meta/wiki/Triage-process). + +We use [issue labels](https://github.com/vector-im/element-meta/wiki/Issue-labelling) to sort all incoming issues. \ No newline at end of file diff --git a/build.gradle b/build.gradle index e17f3579051..b7299b01f75 100644 --- a/build.gradle +++ b/build.gradle @@ -29,21 +29,13 @@ buildscript { // ktlint Plugin plugins { - id "org.jlleitschuh.gradle.ktlint" version "10.2.0" + id "org.jlleitschuh.gradle.ktlint" version "10.2.1" } allprojects { apply plugin: "org.jlleitschuh.gradle.ktlint" repositories { - // For olm library. - maven { - url 'https://gitlab.matrix.org/api/v4/projects/27/packages/maven' - content { - groups.olm.regex.each { includeGroupByRegex it } - groups.olm.group.each { includeGroup it } - } - } maven { url 'https://jitpack.io' content { diff --git a/dependencies.gradle b/dependencies.gradle index 4a076a23bd5..6cb5fac64c2 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -95,6 +95,8 @@ ext.libs = [ ], markwon : [ 'core' : "io.noties.markwon:core:$markwon", + 'extLatex' : "io.noties.markwon:ext-latex:$markwon", + 'inlineParser' : "io.noties.markwon:inline-parser:$markwon", 'html' : "io.noties.markwon:html:$markwon" ], airbnb : [ diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index 25a78bc0c39..3853919bcb1 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -14,13 +14,6 @@ ext.groups = [ 'com.github.Zhuinden', ] ], - olm : [ - regex: [ - ], - group: [ - 'org.matrix.android', - ] - ], jitsi : [ regex: [ ], @@ -166,6 +159,7 @@ ext.groups = [ 'org.junit.jupiter', 'org.junit.platform', 'org.jvnet.staxex', + 'org.matrix.android', 'org.mockito', 'org.mongodb', 'org.objenesis', @@ -179,6 +173,7 @@ ext.groups = [ 'org.sonatype.oss', 'org.testng', 'org.threeten', + 'ru.noties', 'xerces', 'xml-apis', ] diff --git a/docs/design.md b/docs/design.md index 2e27f00ebf5..a79f19cf3e8 100644 --- a/docs/design.md +++ b/docs/design.md @@ -50,6 +50,17 @@ It's also possible for any icon to go to the main component by right-clicking on - open the created vector drawable - optionally update the color(s) to "#FF0000" (red) to ensure that the drawable is correctly tinted at runtime. +### Images + +Android 4.3 (18+) fully supports the WebP image format which can often provide smaller image sizes without drastically impacting image quality (depending on the output encoding quality). +When importing non vector images, WebP is the preferred format. + +Images can be converted to the WebP within Android Studio by + - right clicking the image file within the project file explorer + - select `Convert to WebP` + +https://developer.android.com/studio/write/convert-webp + ## Figma links Figma links can be included in the layout, for future reference, but it is also OK to add a paragraph below here, to centralize the information diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40101150.txt b/fastlane/metadata/android/cs-CZ/changelogs/40101150.txt index e82655d352a..93093cb1a78 100644 --- a/fastlane/metadata/android/cs-CZ/changelogs/40101150.txt +++ b/fastlane/metadata/android/cs-CZ/changelogs/40101150.txt @@ -1,2 +1,2 @@ -Hlavní změny v této verzi: implementace hlasových zpráv dosupných v rámci laboratoře. +Hlavní změny v této verzi: implementace hlasových zpráv dosupných v experimentálních funkcích. Úplný seznam změn: https://github.com/vector-im/element-android/releases/tag/v1.1.15 diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40103090.txt b/fastlane/metadata/android/cs-CZ/changelogs/40103090.txt new file mode 100644 index 00000000000..fe61a48d126 --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40103090.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Přidání podpory pro návrh hlasové zprávy. Opravy mnoha chyb! +Úplný seznam změn: https://github.com/vector-im/element-android/releases/tag/v1.3.9 diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40103100.txt b/fastlane/metadata/android/cs-CZ/changelogs/40103100.txt new file mode 100644 index 00000000000..02eb5b59ef4 --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40103100.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Přidání podpory pro hlasování (v experimentálních funkcích). Nový design náhledu URL. +Úplný seznam změn: https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40103110.txt b/fastlane/metadata/android/cs-CZ/changelogs/40103110.txt new file mode 100644 index 00000000000..e765e1667db --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40103110.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Opravy chyb! +Úplný seznam změn: https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40103120.txt b/fastlane/metadata/android/cs-CZ/changelogs/40103120.txt new file mode 100644 index 00000000000..81437c716b7 --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40103120.txt @@ -0,0 +1,2 @@ +Hlavní změny v této verzi: Opravy chyb! +Úplný seznam změn: https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/de-DE/changelogs/40103050.txt b/fastlane/metadata/android/de-DE/changelogs/40103050.txt new file mode 100644 index 00000000000..a3e40e9e033 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40103050.txt @@ -0,0 +1,2 @@ +Änderungen in dieser Version: Unterstützung für Anwesenheitsstatus in Direktnachrichten (Momentan auf matrix.org deaktiviert), Android Auto funktioniert wieder. +Änderungsliste: https://github.com/vector-im/element-android/releases/tag/v1.3.5 diff --git a/fastlane/metadata/android/de-DE/changelogs/40103060.txt b/fastlane/metadata/android/de-DE/changelogs/40103060.txt new file mode 100644 index 00000000000..dcd8d3634d3 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40103060.txt @@ -0,0 +1,2 @@ +Änderungen in dieser Version: Unterstützung für Anwesenheitsstatus in Direktnachrichten (Momentan auf matrix.org deaktiviert), Android Auto funktioniert wieder. +Änderungsliste: https://github.com/vector-im/element-android/releases/tag/v1.3.6 diff --git a/fastlane/metadata/android/de-DE/changelogs/40103090.txt b/fastlane/metadata/android/de-DE/changelogs/40103090.txt new file mode 100644 index 00000000000..028df4942f5 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40103090.txt @@ -0,0 +1,2 @@ +Hauptänderungen: Verbesserungen bei Sprachnachrichten, Bugfixes. +Änderungsliste: https://github.com/vector-im/element-android/releases/tag/v1.3.9 diff --git a/fastlane/metadata/android/en-US/changelogs/40103110.txt b/fastlane/metadata/android/en-US/changelogs/40103110.txt new file mode 100644 index 00000000000..c28b303a35e --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40103110.txt @@ -0,0 +1,2 @@ +Main changes in this version: Bug fixes! +Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.11 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/40103120.txt b/fastlane/metadata/android/en-US/changelogs/40103120.txt new file mode 100644 index 00000000000..90d55f5f48e --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40103120.txt @@ -0,0 +1,2 @@ +Main changes in this version: Bug fixes! +Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.12 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/40103130.txt b/fastlane/metadata/android/en-US/changelogs/40103130.txt new file mode 100644 index 00000000000..1c0b5da2ee0 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40103130.txt @@ -0,0 +1,2 @@ +Main changes in this version: First change in onboarding screens, including Analytics opt-in. Support for Events with Math added in the labs. +Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.13 \ No newline at end of file diff --git a/fastlane/metadata/android/et/changelogs/40103090.txt b/fastlane/metadata/android/et/changelogs/40103090.txt new file mode 100644 index 00000000000..e931ba53865 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40103090.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: Häälsõnumite võimalus. Palju veaparandusi! +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases/tag/v1.3.9 diff --git a/fastlane/metadata/android/et/changelogs/40103100.txt b/fastlane/metadata/android/et/changelogs/40103100.txt new file mode 100644 index 00000000000..2cb2ae0d880 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40103100.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: katseline küsitluste tugi ja linkide eelvaate uus visuaal. +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/et/changelogs/40103110.txt b/fastlane/metadata/android/et/changelogs/40103110.txt new file mode 100644 index 00000000000..6271372e2b9 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40103110.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: pinu veaparandusi! +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/et/changelogs/40103120.txt b/fastlane/metadata/android/et/changelogs/40103120.txt new file mode 100644 index 00000000000..c1cc3ff6968 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40103120.txt @@ -0,0 +1,2 @@ +Põhilised muutused selles versioonis: pinu veaparandusi! +Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/fa/changelogs/40103090.txt b/fastlane/metadata/android/fa/changelogs/40103090.txt new file mode 100644 index 00000000000..75810a0e232 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40103090.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش: افزودن پشتیبان از چرک‌نویس‌های صوتی. رفع چندین مشکل! +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.3.9 diff --git a/fastlane/metadata/android/fa/changelogs/40103100.txt b/fastlane/metadata/android/fa/changelogs/40103100.txt new file mode 100644 index 00000000000..99c4e3faec5 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40103100.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش:‌ افزودن پشتیبانی نظرسنجی‌ها (در آزمایشگاه‌ها). طرّاحی جدید پیش‌نمای نشانی. +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/fa/changelogs/40103110.txt b/fastlane/metadata/android/fa/changelogs/40103110.txt new file mode 100644 index 00000000000..56d8ba6b911 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40103110.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش:‌ تعمیر مشکلات! +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/fa/changelogs/40103120.txt b/fastlane/metadata/android/fa/changelogs/40103120.txt new file mode 100644 index 00000000000..67976a20247 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/40103120.txt @@ -0,0 +1,2 @@ +تغییرات عمده در این نگارش:‌ تعمیر مشکلات! +گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/fr-FR/changelogs/40103090.txt b/fastlane/metadata/android/fr-FR/changelogs/40103090.txt new file mode 100644 index 00000000000..3394e5ccfad --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/40103090.txt @@ -0,0 +1,2 @@ +Principaux changements pour cette version : Ajout du support pour les brouillons de messages vocaux. Beaucoup de corrections de bugs ! +Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.3.9 diff --git a/fastlane/metadata/android/hu-HU/changelogs/40103090.txt b/fastlane/metadata/android/hu-HU/changelogs/40103090.txt new file mode 100644 index 00000000000..d4189121bb6 --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40103090.txt @@ -0,0 +1,2 @@ +Fő változás ebben a verzióban: Hang üzenet piszkozat támogatás. Sok egyéb hibajavítás. +Teljes változásnapló: https://github.com/vector-im/element-android/releases/tag/v1.3.9 diff --git a/fastlane/metadata/android/hu-HU/changelogs/40103100.txt b/fastlane/metadata/android/hu-HU/changelogs/40103100.txt new file mode 100644 index 00000000000..9e3cb21611b --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40103100.txt @@ -0,0 +1,2 @@ +Fő változás ebben a verzióban: Szavazások támogatása (a laborok között). Új URL előnézet. +Teljes változásnapló: https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/hu-HU/changelogs/40103110.txt b/fastlane/metadata/android/hu-HU/changelogs/40103110.txt new file mode 100644 index 00000000000..86cb418a6c3 --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40103110.txt @@ -0,0 +1,2 @@ +Fő változás ebben a verzióban: Hibajavítások! +Teljes változásnapló: https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/hu-HU/changelogs/40103120.txt b/fastlane/metadata/android/hu-HU/changelogs/40103120.txt new file mode 100644 index 00000000000..33fa44248d8 --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/40103120.txt @@ -0,0 +1,2 @@ +Fő változás ebben a verzióban: Hibajavítások! +Teljes változásnapló: https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/id/changelogs/40103090.txt b/fastlane/metadata/android/id/changelogs/40103090.txt new file mode 100644 index 00000000000..b371ba9fab3 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40103090.txt @@ -0,0 +1,2 @@ +Perubahan utama di versi ini: Tambahkan dukungan untuk draf pesan suara. Banyak perbaikan bug! +Changelog lengkap: https://github.com/vector-im/element-android/releases/tag/v1.3.9 diff --git a/fastlane/metadata/android/id/changelogs/40103100.txt b/fastlane/metadata/android/id/changelogs/40103100.txt new file mode 100644 index 00000000000..39d127cd93f --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40103100.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Dukungan untuk fitur poll (dalam Uji Coba), dan desain tampilan URL baru. +Changelog lanjutan: https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/id/changelogs/40103110.txt b/fastlane/metadata/android/id/changelogs/40103110.txt new file mode 100644 index 00000000000..725e58d957a --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40103110.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Perbaikan bug! +Changelog lanjutan: https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/id/changelogs/40103120.txt b/fastlane/metadata/android/id/changelogs/40103120.txt new file mode 100644 index 00000000000..9a5dc8026c6 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40103120.txt @@ -0,0 +1,2 @@ +Perubahan utama dalam versi ini: Perbaikan bug! +Changelog lanjutan: https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/id/short_description.txt b/fastlane/metadata/android/id/short_description.txt index 1cd770dd730..72c520403c4 100644 --- a/fastlane/metadata/android/id/short_description.txt +++ b/fastlane/metadata/android/id/short_description.txt @@ -1 +1 @@ -Perpesanan grup - perpesanan, panggilan suara dan video grup terenkripsi +Perpesanan grup — perpesanan, panggilan suara dan video grup terenkripsi diff --git a/fastlane/metadata/android/id/title.txt b/fastlane/metadata/android/id/title.txt index aec5dc9351a..08ad7afa677 100644 --- a/fastlane/metadata/android/id/title.txt +++ b/fastlane/metadata/android/id/title.txt @@ -1 +1 @@ -Element - Perpesanan Aman +Element — Perpesanan Aman diff --git a/fastlane/metadata/android/it-IT/changelogs/40103090.txt b/fastlane/metadata/android/it-IT/changelogs/40103090.txt new file mode 100644 index 00000000000..d91ecfe5306 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40103090.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: aggiunto supporto per le bozze dei vocali. Molte correzioni! +Cronologia completa: https://github.com/vector-im/element-android/releases/tag/v1.3.9 diff --git a/fastlane/metadata/android/it-IT/changelogs/40103100.txt b/fastlane/metadata/android/it-IT/changelogs/40103100.txt new file mode 100644 index 00000000000..d6036ff048b --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40103100.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: aggiunto supporto per i sondaggi (in labs). Nuovo design anteprime URL. +Cronologia completa: https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/it-IT/changelogs/40103110.txt b/fastlane/metadata/android/it-IT/changelogs/40103110.txt new file mode 100644 index 00000000000..2db15676dce --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40103110.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: correzioni di errori! +Cronologia completa: https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/it-IT/changelogs/40103120.txt b/fastlane/metadata/android/it-IT/changelogs/40103120.txt new file mode 100644 index 00000000000..7756f8f1864 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/40103120.txt @@ -0,0 +1,2 @@ +Modifiche principali in questa versione: correzioni di errori! +Cronologia completa: https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/nl/changelogs/40103070.txt b/fastlane/metadata/android/nl/changelogs/40103070.txt new file mode 100644 index 00000000000..c2496fa34da --- /dev/null +++ b/fastlane/metadata/android/nl/changelogs/40103070.txt @@ -0,0 +1,2 @@ +Belangrijkste wijzigingen in deze versie: Bugfixes voornamelijk met betrekking tot de meldingen. +Volledige changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.7-RC2 diff --git a/fastlane/metadata/android/nl/changelogs/40103080.txt b/fastlane/metadata/android/nl/changelogs/40103080.txt new file mode 100644 index 00000000000..8ed093a4607 --- /dev/null +++ b/fastlane/metadata/android/nl/changelogs/40103080.txt @@ -0,0 +1,2 @@ +Belangrijkste wijzigingen in deze versie: Bugfixes! +Volledige changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.8 diff --git a/fastlane/metadata/android/nl/changelogs/40103090.txt b/fastlane/metadata/android/nl/changelogs/40103090.txt new file mode 100644 index 00000000000..e4a7f63089b --- /dev/null +++ b/fastlane/metadata/android/nl/changelogs/40103090.txt @@ -0,0 +1,2 @@ +Belangrijkste wijzigingen in deze versie: Ondersteuning toevoegen voor spraakberichtconcept. Veel bugfixes! +Volledige changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.9 diff --git a/fastlane/metadata/android/nl/changelogs/40103100.txt b/fastlane/metadata/android/nl/changelogs/40103100.txt new file mode 100644 index 00000000000..883c6565770 --- /dev/null +++ b/fastlane/metadata/android/nl/changelogs/40103100.txt @@ -0,0 +1,2 @@ +Belangrijkste wijzigingen in deze versie: Ondersteuning toevoegen voor polls (in labs). Nieuw URL-voorbeeldontwerp. +Volledige changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/nl/changelogs/40103110.txt b/fastlane/metadata/android/nl/changelogs/40103110.txt new file mode 100644 index 00000000000..ae1685270bb --- /dev/null +++ b/fastlane/metadata/android/nl/changelogs/40103110.txt @@ -0,0 +1,2 @@ +Belangrijkste wijzigingen in deze versie: Bugfixes! +Volledige changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/nl/changelogs/40103120.txt b/fastlane/metadata/android/nl/changelogs/40103120.txt new file mode 100644 index 00000000000..39d3f5fb431 --- /dev/null +++ b/fastlane/metadata/android/nl/changelogs/40103120.txt @@ -0,0 +1,2 @@ +Belangrijkste wijzigingen in deze versie: Bugfixes! +Volledige changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/nl/short_description.txt b/fastlane/metadata/android/nl/short_description.txt new file mode 100644 index 00000000000..107e30f48d4 --- /dev/null +++ b/fastlane/metadata/android/nl/short_description.txt @@ -0,0 +1 @@ +Groepsberichten - versleutelde berichten, groepschat en videogesprekken diff --git a/fastlane/metadata/android/nl/title.txt b/fastlane/metadata/android/nl/title.txt new file mode 100644 index 00000000000..7b5a8872130 --- /dev/null +++ b/fastlane/metadata/android/nl/title.txt @@ -0,0 +1 @@ +Element - Veilige Berichten diff --git a/fastlane/metadata/android/pt-BR/changelogs/40103090.txt b/fastlane/metadata/android/pt-BR/changelogs/40103090.txt new file mode 100644 index 00000000000..9f67fd2d625 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40103090.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: Adicionar suporte para rascunho de mensagem de voz. Muitos consertos de bugs! +Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.3.9 diff --git a/fastlane/metadata/android/pt-BR/changelogs/40103100.txt b/fastlane/metadata/android/pt-BR/changelogs/40103100.txt new file mode 100644 index 00000000000..9912e2ccf10 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40103100.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: Adicionar suporte para sondagens (em labs). Novo design de previsualização de URL. +Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/pt-BR/changelogs/40103110.txt b/fastlane/metadata/android/pt-BR/changelogs/40103110.txt new file mode 100644 index 00000000000..a1f4d11acf1 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40103110.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: Consertos de bugs! +Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/pt-BR/changelogs/40103120.txt b/fastlane/metadata/android/pt-BR/changelogs/40103120.txt new file mode 100644 index 00000000000..b5113481527 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/40103120.txt @@ -0,0 +1,2 @@ +Principais mudanças nesta versão: Consertos de bugs! +Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/sk/changelogs/40101000.txt b/fastlane/metadata/android/sk/changelogs/40101000.txt new file mode 100644 index 00000000000..5d267bd7dc6 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101000.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Vylepšenie VoIP (audio a video hovory v priamych správach) a opravy chýb! +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.0 diff --git a/fastlane/metadata/android/sk/changelogs/40101010.txt b/fastlane/metadata/android/sk/changelogs/40101010.txt new file mode 100644 index 00000000000..164166fba86 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101010.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: zlepšenie výkonu a opravy chýb! +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.1 diff --git a/fastlane/metadata/android/sk/changelogs/40101020.txt b/fastlane/metadata/android/sk/changelogs/40101020.txt new file mode 100644 index 00000000000..379db42ccac --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101020.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: zlepšenie výkonu a opravy chýb! +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.2 diff --git a/fastlane/metadata/android/sk/changelogs/40101030.txt b/fastlane/metadata/android/sk/changelogs/40101030.txt new file mode 100644 index 00000000000..b99ebb9e99c --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101030.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: zlepšenie výkonu a opravy chýb! +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.3 diff --git a/fastlane/metadata/android/sk/changelogs/40101040.txt b/fastlane/metadata/android/sk/changelogs/40101040.txt new file mode 100644 index 00000000000..884305c478f --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101040.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: zlepšenie výkonu a opravy chýb! +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.4 diff --git a/fastlane/metadata/android/sk/changelogs/40101050.txt b/fastlane/metadata/android/sk/changelogs/40101050.txt new file mode 100644 index 00000000000..22dc0953715 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101050.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: rýchle opravy pre verziu 1.1.4 +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.5 diff --git a/fastlane/metadata/android/sk/changelogs/40101060.txt b/fastlane/metadata/android/sk/changelogs/40101060.txt new file mode 100644 index 00000000000..70ac5cad576 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101060.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: rýchle opravy pre verziu 1.1.5 +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.6 diff --git a/fastlane/metadata/android/sk/changelogs/40101070.txt b/fastlane/metadata/android/sk/changelogs/40101070.txt new file mode 100644 index 00000000000..87eccd8f456 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101070.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: beta podpora pre priestory Spaces. Kompresia videa pred odoslaním. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.7 diff --git a/fastlane/metadata/android/sk/changelogs/40101080.txt b/fastlane/metadata/android/sk/changelogs/40101080.txt new file mode 100644 index 00000000000..9484062b474 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101080.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: vylepšenie pre Priestory. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.8 diff --git a/fastlane/metadata/android/sk/changelogs/40101090.txt b/fastlane/metadata/android/sk/changelogs/40101090.txt new file mode 100644 index 00000000000..5778db23a9c --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101090.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: pridanie podpory pre sieť gitter.im. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.9 diff --git a/fastlane/metadata/android/sk/changelogs/40101100.txt b/fastlane/metadata/android/sk/changelogs/40101100.txt new file mode 100644 index 00000000000..a23198c88b6 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101100.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: aktualizácia témy a štýlu a nové funkcie pre priestory. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.10 diff --git a/fastlane/metadata/android/sk/changelogs/40101110.txt b/fastlane/metadata/android/sk/changelogs/40101110.txt new file mode 100644 index 00000000000..45a095f0a8c --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101110.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: aktualizácia témy a štýlu a nové funkcie pre priestory (oprava chyby pre verziu 1.1.10) +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.11 diff --git a/fastlane/metadata/android/sk/changelogs/40101120.txt b/fastlane/metadata/android/sk/changelogs/40101120.txt new file mode 100644 index 00000000000..ba345c3150e --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101120.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: aktualizácia témy a štýlu a oprava pádu po videohovore +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.12 diff --git a/fastlane/metadata/android/sk/changelogs/40101130.txt b/fastlane/metadata/android/sk/changelogs/40101130.txt new file mode 100644 index 00000000000..6daf3e789bc --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101130.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: hlavne aktualizácia stability a opravy chýb. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.13 diff --git a/fastlane/metadata/android/sk/changelogs/40101140.txt b/fastlane/metadata/android/sk/changelogs/40101140.txt new file mode 100644 index 00000000000..c93fe1bb15b --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101140.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: oprava problému so šifrovanými správami. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.14 diff --git a/fastlane/metadata/android/sk/changelogs/40101150.txt b/fastlane/metadata/android/sk/changelogs/40101150.txt new file mode 100644 index 00000000000..87256269ab9 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101150.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: implementácia hlasových správ v rámci nastavení laboratórií. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.15 diff --git a/fastlane/metadata/android/sk/changelogs/40101160.txt b/fastlane/metadata/android/sk/changelogs/40101160.txt new file mode 100644 index 00000000000..5e12aab2825 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40101160.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Oprava chyby pri odosielaní zašifrovanej správy, ak sa niekto v miestnosti odhlási. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.1.16 diff --git a/fastlane/metadata/android/sk/changelogs/40102000.txt b/fastlane/metadata/android/sk/changelogs/40102000.txt new file mode 100644 index 00000000000..4d0093469ba --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40102000.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Hlasová správa je predvolene povolená. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.2.0 diff --git a/fastlane/metadata/android/sk/changelogs/40102010.txt b/fastlane/metadata/android/sk/changelogs/40102010.txt new file mode 100644 index 00000000000..ac1cbc4509c --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40102010.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Mnohé vylepšenia v oblasti VoIP a Priestorov (stále v beta verzii). +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.2.1 diff --git a/fastlane/metadata/android/sk/changelogs/40103000.txt b/fastlane/metadata/android/sk/changelogs/40103000.txt new file mode 100644 index 00000000000..2a669aa744e --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40103000.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Usporiadajte svoje miestnosti pomocou Priestorov! +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.3.0 diff --git a/fastlane/metadata/android/sk/changelogs/40103010.txt b/fastlane/metadata/android/sk/changelogs/40103010.txt new file mode 100644 index 00000000000..3a12a5910ef --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40103010.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Usporiadajte svoje miestnosti pomocou Priestorov! Verzia v1.3.1 opravuje pád, ktorý sa môže vyskytnúť vo verzii v1.3.0. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.3.1 diff --git a/fastlane/metadata/android/sk/changelogs/40103020.txt b/fastlane/metadata/android/sk/changelogs/40103020.txt new file mode 100644 index 00000000000..96cefe73ed7 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40103020.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Pridanie podpory pre Android Auto. Množstvo opráv chýb! +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.3.2 diff --git a/fastlane/metadata/android/sk/changelogs/40103030.txt b/fastlane/metadata/android/sk/changelogs/40103030.txt new file mode 100644 index 00000000000..a14dba2c9d5 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40103030.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Zviditeľnite zásad servera totožností v nastaveniach. Dočasne odstránenie podpory Android Auto. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.3.3 diff --git a/fastlane/metadata/android/sk/changelogs/40103040.txt b/fastlane/metadata/android/sk/changelogs/40103040.txt new file mode 100644 index 00000000000..e2e6a98b075 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40103040.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Pridanie podpory prítomnosti pre miestnosť s priamymi správami (poznámka: prítomnosť je na matrix.org vypnutá). Opätovné pridanie podpory Android Auto. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.3.4 diff --git a/fastlane/metadata/android/sk/changelogs/40103050.txt b/fastlane/metadata/android/sk/changelogs/40103050.txt new file mode 100644 index 00000000000..f5cc73a4e20 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40103050.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Pridanie podpory prítomnosti pre miestnosť s priamymi správami (poznámka: prítomnosť je na matrix.org vypnutá). Opätovné pridanie podpory Android Auto. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.3.5 diff --git a/fastlane/metadata/android/sk/changelogs/40103060.txt b/fastlane/metadata/android/sk/changelogs/40103060.txt new file mode 100644 index 00000000000..c9a3b8bb750 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40103060.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Pridanie podpory prítomnosti pre miestnosť s priamymi správami (poznámka: prítomnosť je na matrix.org vypnutá). Opätovné pridanie podpory Android Auto. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.3.6 diff --git a/fastlane/metadata/android/sk/changelogs/40103090.txt b/fastlane/metadata/android/sk/changelogs/40103090.txt new file mode 100644 index 00000000000..d719d5055c4 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40103090.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Pridanie podpory pre návrh hlasovej správy. Oprava mnohých chýb! +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.3.9 diff --git a/fastlane/metadata/android/sk/changelogs/40103100.txt b/fastlane/metadata/android/sk/changelogs/40103100.txt new file mode 100644 index 00000000000..14a667c78d1 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40103100.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Pridanie podpory pre ankety (v laboratóriách). Nový dizajn náhľadu URL. +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/sk/changelogs/40103110.txt b/fastlane/metadata/android/sk/changelogs/40103110.txt new file mode 100644 index 00000000000..2c2ee1aa6d3 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40103110.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Opravy chýb! +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/sk/changelogs/40103120.txt b/fastlane/metadata/android/sk/changelogs/40103120.txt new file mode 100644 index 00000000000..363e4aef24e --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40103120.txt @@ -0,0 +1,2 @@ +Hlavné zmeny v tejto verzii: Opravy chýb! +Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/sk/full_description.txt b/fastlane/metadata/android/sk/full_description.txt index b4c9e987770..78661e961e4 100644 --- a/fastlane/metadata/android/sk/full_description.txt +++ b/fastlane/metadata/android/sk/full_description.txt @@ -1,30 +1,41 @@ -Element je inovatívny kolaboračný komunikátor a messenger ktorý: +Element je zabezpečený messenger a zároveň aplikácia na tímovú spoluprácu, ktorá je ideálna na skupinové konverzácie pri práci na diaľku. Táto komunikačná aplikácia využíva end-to-end šifrovanie na poskytovanie výkonných videokonferencií, zdieľania súborov a hlasových hovorov. -1. Ponecháva kontrolu nad vaším súkromím -2. Umožňuje komunikovať s kýmkoľvek v sieti Matrix a vďaka integráciám aj s rôznymi inými aplikáciami ako napríklad Slack -3. Chráni vás pred reklamami, zhromažďovaním údajov a uzavretými platformami -4. Posilňuje vašu bezpečnosť vďaka E2E šifrovaniu a krížovému podpisovaniu určenému na overovanie ostatných +Funkcie aplikácie Element zahŕňajú: +- Pokročilé nástroje na online komunikáciu +- Plne šifrované správy umožňujúce bezpečnejšiu firemnú komunikáciu aj pre pracovníkov na diaľku +- Decentralizované konverzácie založené na open source frameworku Matrix +- Bezpečné zdieľanie súborov so šifrovanými údajmi pri správe projektov +- Videochaty s funkciou Voice over IP a zdieľaním obrazovky +- Jednoduchá integrácia s obľúbenými nástrojmi na online spoluprácu, nástrojmi na riadenie projektov, službami VoIP a inými aplikáciami na tímovú komunikáciu -Element sa od ostatných komunikačných a kolaboračných aplikácií odlišuje tým, že je decentralizovaný a open-source. +Element sa úplne líši od ostatných aplikácií na zasielanie správ a spoluprácu. Funguje na Matrixe, otvorenej sieti na bezpečné posielanie správ a decentralizovanú komunikáciu. Umožňuje vlastný hosting, aby používatelia získali maximálne vlastníctvo a kontrolu nad svojimi údajmi a správami. -S Elementom sa môžete pripojiť k vlastnému serveru alebo si môžete vybrať server s dôveryhodným poskytovateľom, čím si zachováte súkromie, vlastníctvo a kontrolu nad vašimi konverzáciami a údajmi. Získate tak prístup do otvorenej siete a teda nie ste limitovaní na komunikáciu len s ostatnými Element používateľmi. A samozrejme je vaša komunikácia dobre zabezpečná. +Súkromie a šifrovanie správ +Element vás chráni pred nežiaducimi reklamami, ťažbou údajov a tzv. walled gardens. Zabezpečuje tiež všetky vaše údaje, video a hlasovú komunikáciu jeden na jedného prostredníctvom end-to-end šifrovania a overovania zariadení krížovým podpisovaním +Element vám poskytuje kontrolu nad vaším súkromím a zároveň vám umožňuje bezpečne komunikovať s kýmkoľvek v sieti Matrix alebo s inými nástrojmi na podnikovú spoluprácu vďaka integrácii s aplikáciami, ako je napríklad Slack. -Element všetko toto dokáže vďaka tomu, že pracuje podľa protokolu Matrix - štandardu na otvorenú, decentralizovanú komunikáciu. +Element môže byť na vašom vlastnom serveri. +Aby ste mali väčšiu kontrolu nad svojimi citlivými údajmi a konverzáciami, Element môže byť na vašom vlastnom serveri alebo si môžete vybrať ľubovoľný hosting založený na systéme Matrix - štandarde pre decentralizovanú komunikáciu s otvoreným zdrojovým kódom. Element vám poskytuje súkromie, súlad s bezpečnostnými predpismi a flexibilitu integrácie. -Element vám dáva kontrolu tým, že si samy vyberiete, ako budete spravovať (ang. host) vaše konverzácie. Priamo v aplikácii Element si môžete vybrať z rôznych spôsobov hostovania: +Vlastnite svoje údaje +Vy rozhodujete o tom, kde budú vaše údaje a správy uložené. Bez rizika ťažby údajov alebo prístupu tretích strán. -1. Získajte účet zdarma na verejnom servery matrix.org od vývojárov protokolu Matrix alebo si vyberte z tísíce iných serverov hostovaných dobrovoľníkmi -2. Hostujte si účet spustením vlastného servera použitím vlastného hardvéru -3. Prihláste sa k účtu na vlastnom servery objednaním služieb na platforme Element Matrix Services +Element vám dáva kontrolu rôznymi spôsobmi: +1. Získajte bezplatné konto na verejnom serveri matrix.org, ktorý hostia vývojári Matrixu, alebo si vyberte z tisícov verejných serverov, ktoré hostia dobrovoľníci. +2. Vlastný hosting účtu spustením servera na vlastnej IT infraštruktúre. +3. Zaregistrujte si účet na vlastnom serveri tak, že si jednoducho predplatíte hostingovú platformu Element Matrix Services. -Prečo si vybrať Element? +Otvorené zasielanie správ a spolupráca +Môžete komunikovať s kýmkoľvek v sieti Matrix, či už používa aplikáciu Element, inú aplikáciu Matrix alebo dokonca ak používa inú aplikáciu na zasielanie správ. -PONECHAJTE SI VAŠE ÚDAJE: Len vy rozhodujete o tom, kde si budete uchovávať vaše správy a ostatné údaje. Len vy vlastníte vaše údaje a riadite zaobchádzanie s nimi, nie nejaká megakorporácia, ktorá z nich ťaží alebo ich poskytuje tretím stranám. +Vynikajúce zabezpečenie +Skutočné end-to-end šifrovanie (správy môžu dešifrovať len účastníci konverzácie) a krížové overovanie zariadení. -OTVORENÁ KOMUNIKÁCIA a KOLABORÁCIA: Konverzovať môžete s kýmkoľvek v otvorenej sieti Matrix nezávisle na tom, či používa Element, inú kompatibilnú aplikáciu, ba dokkonca aj s tými, ktorí používajú úplne inú platformu určenú na okamžitú komunikáciu ako sú Slack, IRC alebo XMPP. +Kompletná komunikácia a integrácia +Správy, hlasové a video hovory, zdieľanie súborov, zdieľanie obrazovky a celý rad integrácií, botov a widgetov. Vytvárajte miestnosti, komunity, zostaňte v kontakte a vybavujte veci. -VEĽMI VYSOKÉ ZABEZPEČENIE: Skutočné šifrovanie od zariadenia k zariadeniu (len diskutujúci môžu dešifrovať správy) a krížové podpisovanie určené na overovanie jednotlivých zariadení členov konverzácií. +Nadviažte tam, kde ste skončili +Buďte v kontakte, nech ste kdekoľvek, vďaka plne synchronizovanej histórii správ vo všetkých zariadeniach a na webe na adrese https://app.element.io. -KOMPLETNÁ KOMUNIKÁCIA: Okamžité správy, telefonáty a video hovory, zdieľanie súborov, zdieľanie obrazovky a veľké množstvo integrácií, botov a widgetov. Vytvorte si vlastné miestnosti, založte komunity, ostante v kontakte a vyriešte problémy. - -KDEKOĽVEK SA NACHÁDZATE: Ostante v kontakte kdekoľvek ste s plne synchronizovanou históriou konverzácií naprieč všetkými vašimi zariadeniami a aj cez web na adrese https://app.element.io. +Otvorený zdroj +Element Android je projekt s otvoreným zdrojovým kódom, ktorého hostiteľom je GitHub. Nahlasujte chyby a/alebo prispievajte k jeho vývoju na adrese https://github.com/vector-im/element-android. diff --git a/fastlane/metadata/android/sk/title.txt b/fastlane/metadata/android/sk/title.txt index dd02c784e8e..fa7155e82ed 100644 --- a/fastlane/metadata/android/sk/title.txt +++ b/fastlane/metadata/android/sk/title.txt @@ -1 +1 @@ -Element (kedysi Riot.im) +Element - Bezpečný messenger diff --git a/fastlane/metadata/android/sq/changelogs/40103090.txt b/fastlane/metadata/android/sq/changelogs/40103090.txt new file mode 100644 index 00000000000..2dae814fc12 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40103090.txt @@ -0,0 +1,2 @@ +Ndryshimet kryesore në këtë version: Shtim mbulimi për skica mesazhesh zanore. Mjaft ndreqje të metash! +Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.3.9 diff --git a/fastlane/metadata/android/sv-SE/changelogs/40103090.txt b/fastlane/metadata/android/sv-SE/changelogs/40103090.txt new file mode 100644 index 00000000000..dce7ffe5a7d --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40103090.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Lägg till stöd för röstmeddelandeutkast. Många buggfixar! +Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.3.9 diff --git a/fastlane/metadata/android/sv-SE/changelogs/40103100.txt b/fastlane/metadata/android/sv-SE/changelogs/40103100.txt new file mode 100644 index 00000000000..d2ea16da98b --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40103100.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Lägg till stöd för omröstningar (i experiment). Ny design för URL-förhandsgranskning. +Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/sv-SE/changelogs/40103110.txt b/fastlane/metadata/android/sv-SE/changelogs/40103110.txt new file mode 100644 index 00000000000..ae1fcddda9d --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40103110.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Buggfixar! +Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/sv-SE/changelogs/40103120.txt b/fastlane/metadata/android/sv-SE/changelogs/40103120.txt new file mode 100644 index 00000000000..b9d73b692b6 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40103120.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Buggfixar! +Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/uk/changelogs/40103090.txt b/fastlane/metadata/android/uk/changelogs/40103090.txt new file mode 100644 index 00000000000..37f8959d4cb --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40103090.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: підтримка чернеток голосових повідомлень. Багато виправлень помилок! +Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.3.9 diff --git a/fastlane/metadata/android/uk/changelogs/40103100.txt b/fastlane/metadata/android/uk/changelogs/40103100.txt new file mode 100644 index 00000000000..99e4be65ebe --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40103100.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: Додано підтримку опитувань (в експериментальних). Новий вигляд попереднього перегляду посилань. +Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/uk/changelogs/40103110.txt b/fastlane/metadata/android/uk/changelogs/40103110.txt new file mode 100644 index 00000000000..cc5af09cdac --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40103110.txt @@ -0,0 +1,2 @@ +Основні зміни у цій версії: Виправлення помилок! +Повний перелік змін: https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/uk/changelogs/40103120.txt b/fastlane/metadata/android/uk/changelogs/40103120.txt new file mode 100644 index 00000000000..a37498b4f13 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40103120.txt @@ -0,0 +1,2 @@ +Основні зміни у цій версії: Виправлення помилок! +Повний перелік змін: https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/fastlane/metadata/android/zh-CN/changelogs/40103090.txt b/fastlane/metadata/android/zh-CN/changelogs/40103090.txt new file mode 100644 index 00000000000..7eb68d61e4c --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/40103090.txt @@ -0,0 +1,2 @@ +版本的主要变化:增加了对语音信息草稿的支持。许多修正! +完整更新日志:https://github.com/vector-im/element-android/releases/tag/v1.3.9 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103090.txt b/fastlane/metadata/android/zh-TW/changelogs/40103090.txt new file mode 100644 index 00000000000..c74a27acbfd --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40103090.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:新增對語音訊息草稿的支援。許多臭蟲修復! +完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.9 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103100.txt b/fastlane/metadata/android/zh-TW/changelogs/40103100.txt new file mode 100644 index 00000000000..70d93e833db --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40103100.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:新增對投票(在實驗室中)的支援。新的 URL 預覽設計。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.10 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103110.txt b/fastlane/metadata/android/zh-TW/changelogs/40103110.txt new file mode 100644 index 00000000000..d5450f4c6ac --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40103110.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:臭蟲修復! +完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.11 diff --git a/fastlane/metadata/android/zh-TW/changelogs/40103120.txt b/fastlane/metadata/android/zh-TW/changelogs/40103120.txt new file mode 100644 index 00000000000..0ee60318c12 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40103120.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:臭蟲修復! +完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.12 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index cf2b23094e4..ee6ba9a3ac4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=b75392c5625a88bccd58a574552a5a323edca82dab5942d2d41097f809c6bcce -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.1-all.zip +distributionSha256Sum=c9490e938b221daf0094982288e4038deed954a3f12fb54cbf270ddf4e37d879 +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/library/ui-styles/src/main/res/drawable/bg_carousel_page_1.xml b/library/ui-styles/src/main/res/drawable/bg_carousel_page_1.xml new file mode 100644 index 00000000000..fa3aea4cab8 --- /dev/null +++ b/library/ui-styles/src/main/res/drawable/bg_carousel_page_1.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/library/ui-styles/src/main/res/drawable/bg_carousel_page_2.xml b/library/ui-styles/src/main/res/drawable/bg_carousel_page_2.xml new file mode 100644 index 00000000000..f696823a6e2 --- /dev/null +++ b/library/ui-styles/src/main/res/drawable/bg_carousel_page_2.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/library/ui-styles/src/main/res/drawable/bg_carousel_page_3.xml b/library/ui-styles/src/main/res/drawable/bg_carousel_page_3.xml new file mode 100644 index 00000000000..b114f9c804f --- /dev/null +++ b/library/ui-styles/src/main/res/drawable/bg_carousel_page_3.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/library/ui-styles/src/main/res/drawable/bg_carousel_page_4.xml b/library/ui-styles/src/main/res/drawable/bg_carousel_page_4.xml new file mode 100644 index 00000000000..e8ee3644319 --- /dev/null +++ b/library/ui-styles/src/main/res/drawable/bg_carousel_page_4.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/library/ui-styles/src/debug/res/drawable/ic_debug_icon.xml b/library/ui-styles/src/main/res/drawable/ic_debug_icon.xml similarity index 100% rename from library/ui-styles/src/debug/res/drawable/ic_debug_icon.xml rename to library/ui-styles/src/main/res/drawable/ic_debug_icon.xml diff --git a/library/ui-styles/src/main/res/values-sw600dp/dimens.xml b/library/ui-styles/src/main/res/values-sw600dp/dimens.xml index 204d663d9cc..f399a350b13 100644 --- a/library/ui-styles/src/main/res/values-sw600dp/dimens.xml +++ b/library/ui-styles/src/main/res/values-sw600dp/dimens.xml @@ -2,4 +2,8 @@ 400dp + + + 0.25 + 0.75 \ No newline at end of file diff --git a/library/ui-styles/src/main/res/values/dimens.xml b/library/ui-styles/src/main/res/values/dimens.xml index 864f3d3d7ff..a2a6b34b0f3 100644 --- a/library/ui-styles/src/main/res/values/dimens.xml +++ b/library/ui-styles/src/main/res/values/dimens.xml @@ -42,4 +42,13 @@ 8dp + + + 56dp + 52dp + 1dp + + + 0.05 + 0.95 \ No newline at end of file diff --git a/library/ui-styles/src/main/res/values/stylable_pool_result_line.xml b/library/ui-styles/src/main/res/values/stylable_pool_result_line.xml deleted file mode 100644 index 93e98511068..00000000000 --- a/library/ui-styles/src/main/res/values/stylable_pool_result_line.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt index 2a0abd3d24b..669e27edfdb 100644 --- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt +++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt @@ -152,6 +152,13 @@ class FlowSession(private val session: Session) { } } + fun liveUserAccountData(type: String): Flow> { + return session.accountDataService().getLiveUserAccountDataEvent(type).asFlow() + .startWith(session.coroutineDispatchers.io) { + session.accountDataService().getUserAccountDataEvent(type).toOptional() + } + } + fun liveRoomAccountData(types: Set): Flow> { return session.accountDataService().getLiveRoomAccountDataEvents(types).asFlow() .startWith(session.coroutineDispatchers.io) { diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 477f971e04a..936609c1d77 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -31,7 +31,7 @@ android { // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' - buildConfigField "String", "SDK_VERSION", "\"1.3.10\"" + buildConfigField "String", "SDK_VERSION", "\"1.3.13\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" resValue "string", "git_sdk_revision", "\"${gitRevision()}\"" @@ -140,8 +140,8 @@ dependencies { implementation libs.arrow.core implementation libs.arrow.instances - // olm lib is now hosted by maven at https://gitlab.matrix.org/api/v4/projects/27/packages/maven - implementation 'org.matrix.android:olm:3.2.7' + // olm lib is now hosted in MavenCentral + implementation 'org.matrix.android:olm-sdk:3.2.10' // DI implementation libs.dagger.dagger @@ -158,7 +158,7 @@ dependencies { implementation libs.apache.commonsImaging // Phone number https://github.com/google/libphonenumber - implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.39' + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.40' testImplementation libs.tests.junit testImplementation 'org.robolectric:robolectric:4.7.3' diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/LiveDataTestObserver.java b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/LiveDataTestObserver.java index 26920fbb35d..18de66e69e2 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/LiveDataTestObserver.java +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/LiveDataTestObserver.java @@ -208,4 +208,4 @@ public static LiveDataTestObserver test(LiveData liveData) { liveData.observeForever(observer); return observer; } -} \ No newline at end of file +} \ No newline at end of file diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt index 8e21828562f..3cb699378fa 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt @@ -145,36 +145,9 @@ class CommonTestHelper(context: Context) { * @param nbOfMessages the number of time the message will be sent */ fun sendTextMessage(room: Room, message: String, nbOfMessages: Int, timeout: Long = TestConstants.timeOutMillis): List { - val sentEvents = ArrayList(nbOfMessages) val timeline = room.createTimeline(null, TimelineSettings(10)) timeline.start() - waitWithLatch(timeout + 1_000L * nbOfMessages) { latch -> - val timelineListener = object : Timeline.Listener { - override fun onTimelineFailure(throwable: Throwable) { - } - - override fun onNewTimelineEvents(eventIds: List) { - // noop - } - - override fun onTimelineUpdated(snapshot: List) { - val newMessages = snapshot - .filter { it.root.sendState == SendState.SYNCED } - .filter { it.root.getClearType() == EventType.MESSAGE } - .filter { it.root.getClearContent().toModel()?.body?.startsWith(message) == true } - - Timber.v("New synced message size: ${newMessages.size}") - if (newMessages.size == nbOfMessages) { - sentEvents.addAll(newMessages) - // Remove listener now, if not at the next update sendEvents could change - timeline.removeListener(this) - latch.countDown() - } - } - } - timeline.addListener(timelineListener) - sendTextMessagesBatched(room, message, nbOfMessages) - } + val sentEvents = sendTextMessagesBatched(timeline, room, message, nbOfMessages, timeout) timeline.dispose() // Check that all events has been created assertEquals("Message number do not match $sentEvents", nbOfMessages.toLong(), sentEvents.size.toLong()) @@ -182,9 +155,10 @@ class CommonTestHelper(context: Context) { } /** - * Will send nb of messages provided by count parameter but waits a bit every 10 messages to avoid gap in sync + * Will send nb of messages provided by count parameter but waits every 10 messages to avoid gap in sync */ - private fun sendTextMessagesBatched(room: Room, message: String, count: Int) { + private fun sendTextMessagesBatched(timeline: Timeline, room: Room, message: String, count: Int, timeout: Long): List { + val sentEvents = ArrayList(count) (1 until count + 1) .map { "$message #$it" } .chunked(10) @@ -192,8 +166,34 @@ class CommonTestHelper(context: Context) { batchedMessages.forEach { formattedMessage -> room.sendTextMessage(formattedMessage) } - Thread.sleep(1_000L) + waitWithLatch(timeout) { latch -> + val timelineListener = object : Timeline.Listener { + + override fun onTimelineUpdated(snapshot: List) { + val allSentMessages = snapshot + .filter { it.root.sendState == SendState.SYNCED } + .filter { it.root.getClearType() == EventType.MESSAGE } + .filter { it.root.getClearContent().toModel()?.body?.startsWith(message) == true } + + val hasSyncedAllBatchedMessages = allSentMessages + .map { + it.root.getClearContent().toModel()?.body + } + .containsAll(batchedMessages) + + if (allSentMessages.size == count) { + sentEvents.addAll(allSentMessages) + } + if (hasSyncedAllBatchedMessages) { + timeline.removeListener(this) + latch.countDown() + } + } + } + timeline.addListener(timelineListener) + } } + return sentEvents } // PRIVATE METHODS ***************************************************************************** @@ -332,13 +332,6 @@ class CommonTestHelper(context: Context) { fun createEventListener(latch: CountDownLatch, predicate: (List) -> Boolean): Timeline.Listener { return object : Timeline.Listener { - override fun onTimelineFailure(throwable: Throwable) { - // noop - } - - override fun onNewTimelineEvents(eventIds: List) { - // noop - } override fun onTimelineUpdated(snapshot: List) { if (predicate(snapshot)) { diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt index ccea6f53b9a..71796192a8d 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt @@ -246,8 +246,7 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) { val bobRoomSummariesLive = bob.getRoomSummariesLive(roomSummaryQueryParams { }) val newRoomObserver = object : Observer> { override fun onChanged(t: List?) { - val indexOfFirst = t?.indexOfFirst { it.roomId == roomId } ?: -1 - if (indexOfFirst != -1) { + if (t?.any { it.roomId == roomId }.orFalse()) { bobRoomSummariesLive.removeObserver(this) latch.countDown() } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt index 1ed2f899773..8625e97902c 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt @@ -49,6 +49,7 @@ class MarkdownParserTest : InstrumentedTest { * Create the same parser than in the RoomModule */ private val markdownParser = MarkdownParser( + Parser.builder().build(), Parser.builder().build(), HtmlRenderer.builder().softbreak("
").build(), TextPillsUtils( diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineBackToPreviousLastForwardTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineBackToPreviousLastForwardTest.kt deleted file mode 100644 index 7628f287c97..00000000000 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineBackToPreviousLastForwardTest.kt +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.session.room.timeline - -import org.amshove.kluent.shouldBeFalse -import org.amshove.kluent.shouldBeTrue -import org.junit.Assert.assertTrue -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -import org.junit.runners.MethodSorters -import org.matrix.android.sdk.InstrumentedTest -import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.model.message.MessageContent -import org.matrix.android.sdk.api.session.room.timeline.Timeline -import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings -import org.matrix.android.sdk.common.CommonTestHelper -import org.matrix.android.sdk.common.CryptoTestHelper -import org.matrix.android.sdk.common.checkSendOrder -import timber.log.Timber -import java.util.concurrent.CountDownLatch - -@RunWith(JUnit4::class) -@FixMethodOrder(MethodSorters.JVM) -class TimelineBackToPreviousLastForwardTest : InstrumentedTest { - - private val commonTestHelper = CommonTestHelper(context()) - private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) - - /** - * This test ensure that if we have a chunk in the timeline which is due to a sync, and we click to permalink of an - * even contained in a previous lastForward chunk, we will be able to go back to the live - */ - @Test - fun backToPreviousLastForwardTest() { - val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false) - - val aliceSession = cryptoTestData.firstSession - val bobSession = cryptoTestData.secondSession!! - val aliceRoomId = cryptoTestData.roomId - - aliceSession.cryptoService().setWarnOnUnknownDevices(false) - bobSession.cryptoService().setWarnOnUnknownDevices(false) - - val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! - val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!! - - val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(30)) - bobTimeline.start() - - var roomCreationEventId: String? = null - - run { - val lock = CountDownLatch(1) - val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> - Timber.e("Bob timeline updated: with ${snapshot.size} events:") - snapshot.forEach { - Timber.w(" event ${it.root}") - } - - roomCreationEventId = snapshot.lastOrNull()?.root?.eventId - // Ok, we have the 8 first messages of the initial sync (room creation and bob join event) - snapshot.size == 8 - } - - bobTimeline.addListener(eventsListener) - commonTestHelper.await(lock) - bobTimeline.removeAllListeners() - - bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() - bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() - } - - // Bob stop to sync - bobSession.stopSync() - - val messageRoot = "First messages from Alice" - - // Alice sends 30 messages - commonTestHelper.sendTextMessage( - roomFromAlicePOV, - messageRoot, - 30) - - // Bob start to sync - bobSession.startSync(true) - - run { - val lock = CountDownLatch(1) - val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> - Timber.e("Bob timeline updated: with ${snapshot.size} events:") - snapshot.forEach { - Timber.w(" event ${it.root}") - } - - // Ok, we have the 10 last messages from Alice. - snapshot.size == 10 && - snapshot.all { it.root.content.toModel()?.body?.startsWith(messageRoot).orFalse() } - } - - bobTimeline.addListener(eventsListener) - commonTestHelper.await(lock) - bobTimeline.removeAllListeners() - - bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue() - bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() - } - - // Bob navigate to the first event (room creation event), so inside the previous last forward chunk - run { - val lock = CountDownLatch(1) - val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> - Timber.e("Bob timeline updated: with ${snapshot.size} events:") - snapshot.forEach { - Timber.w(" event ${it.root}") - } - - // The event is in db, so it is fetch and auto pagination occurs, half of the number of events we have for this chunk (?) - snapshot.size == 4 - } - - bobTimeline.addListener(eventsListener) - - // Restart the timeline to the first sent event, which is already in the database, so pagination should start automatically - assertTrue(roomFromBobPOV.getTimeLineEvent(roomCreationEventId!!) != null) - - bobTimeline.restartWithEventId(roomCreationEventId) - - commonTestHelper.await(lock) - bobTimeline.removeAllListeners() - - bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue() - bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() - } - - // Bob scroll to the future - run { - val lock = CountDownLatch(1) - val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> - Timber.e("Bob timeline updated: with ${snapshot.size} events:") - snapshot.forEach { - Timber.w(" event ${it.root}") - } - - // Bob can see the first event of the room (so Back pagination has worked) - snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE && - // 8 for room creation item, and 30 for the forward pagination - snapshot.size == 38 && - snapshot.checkSendOrder(messageRoot, 30, 0) - } - - bobTimeline.addListener(eventsListener) - - bobTimeline.paginate(Timeline.Direction.FORWARDS, 50) - - commonTestHelper.await(lock) - bobTimeline.removeAllListeners() - - bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() - bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() - } - bobTimeline.dispose() - - cryptoTestData.cleanUp(commonTestHelper) - } -} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt index bc9722c922b..05a43de0ac6 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.session.room.timeline +import kotlinx.coroutines.runBlocking +import org.amshove.kluent.internal.assertEquals import org.amshove.kluent.shouldBeFalse import org.amshove.kluent.shouldBeTrue import org.junit.FixMethodOrder @@ -123,54 +125,29 @@ class TimelineForwardPaginationTest : InstrumentedTest { // Alice paginates BACKWARD and FORWARD of 50 events each // Then she can only navigate FORWARD run { - val lock = CountDownLatch(1) - val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot -> - Timber.e("Alice timeline updated: with ${snapshot.size} events:") - snapshot.forEach { - Timber.w(" event ${it.root.content}") - } - - // Alice can see the first event of the room (so Back pagination has worked) - snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE && - // 6 for room creation item (backward pagination), 1 for the context, and 50 for the forward pagination - snapshot.size == 57 // 6 + 1 + 50 + val snapshot = runBlocking { + aliceTimeline.awaitPaginate(Timeline.Direction.BACKWARDS, 50) + aliceTimeline.awaitPaginate(Timeline.Direction.FORWARDS, 50) } - - aliceTimeline.addListener(aliceEventsListener) - - // Restart the timeline to the first sent event - // We ask to load event backward and forward - aliceTimeline.paginate(Timeline.Direction.BACKWARDS, 50) - aliceTimeline.paginate(Timeline.Direction.FORWARDS, 50) - - commonTestHelper.await(lock) - aliceTimeline.removeAllListeners() - aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue() aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + + assertEquals(EventType.STATE_ROOM_CREATE, snapshot.lastOrNull()?.root?.getClearType()) + // 6 for room creation item (backward pagination), 1 for the context, and 50 for the forward pagination + // 6 + 1 + 50 + assertEquals(57, snapshot.size) } // Alice paginates once again FORWARD for 50 events // All the timeline is retrieved, she cannot paginate anymore in both direction run { - val lock = CountDownLatch(1) - val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot -> - Timber.e("Alice timeline updated: with ${snapshot.size} events:") - snapshot.forEach { - Timber.w(" event ${it.root.content}") - } - // 6 for room creation item (backward pagination),and numberOfMessagesToSend (all the message of the room) - snapshot.size == 6 + numberOfMessagesToSend && - snapshot.checkSendOrder(message, numberOfMessagesToSend, 0) - } - - aliceTimeline.addListener(aliceEventsListener) - // Ask for a forward pagination - aliceTimeline.paginate(Timeline.Direction.FORWARDS, 50) - - commonTestHelper.await(lock) - aliceTimeline.removeAllListeners() + val snapshot = runBlocking { + aliceTimeline.awaitPaginate(Timeline.Direction.FORWARDS, 50) + } + // 6 for room creation item (backward pagination),and numberOfMessagesToSend (all the message of the room) + snapshot.size == 6 + numberOfMessagesToSend && + snapshot.checkSendOrder(message, numberOfMessagesToSend, 0) // The timeline is fully loaded aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt index e865fe17da8..c6fdec150d5 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt @@ -168,10 +168,8 @@ class TimelinePreviousLastForwardTest : InstrumentedTest { bobTimeline.addListener(eventsListener) - // Restart the timeline to the first sent event, and paginate in both direction + // Restart the timeline to the first sent event bobTimeline.restartWithEventId(firstMessageFromAliceId) - bobTimeline.paginate(Timeline.Direction.BACKWARDS, 50) - bobTimeline.paginate(Timeline.Direction.FORWARDS, 50) commonTestHelper.await(lock) bobTimeline.removeAllListeners() diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt new file mode 100644 index 00000000000..b75df9b5a21 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt @@ -0,0 +1,104 @@ +/* + * 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.session.room.timeline + +import kotlinx.coroutines.runBlocking +import org.amshove.kluent.internal.assertEquals +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.events.model.isTextMessage +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.TestConstants + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class TimelineSimpleBackPaginationTest : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + + @Test + fun timeline_backPaginate_shouldReachEndOfTimeline() { + val numberOfMessagesToSent = 200 + + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false) + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession!! + val roomId = cryptoTestData.roomId + + aliceSession.cryptoService().setWarnOnUnknownDevices(false) + bobSession.cryptoService().setWarnOnUnknownDevices(false) + + val roomFromAlicePOV = aliceSession.getRoom(roomId)!! + val roomFromBobPOV = bobSession.getRoom(roomId)!! + + // Alice sends X messages + val message = "Message from Alice" + commonTestHelper.sendTextMessage( + roomFromAlicePOV, + message, + numberOfMessagesToSent) + + val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(30)) + bobTimeline.start() + + commonTestHelper.waitWithLatch(timeout = TestConstants.timeOutMillis * 10) { + val listener = object : Timeline.Listener { + + override fun onStateUpdated(direction: Timeline.Direction, state: Timeline.PaginationState) { + if (direction == Timeline.Direction.FORWARDS) { + return + } + if (state.hasMoreToLoad && !state.loading) { + bobTimeline.paginate(Timeline.Direction.BACKWARDS, 30) + } else if (!state.hasMoreToLoad) { + bobTimeline.removeListener(this) + it.countDown() + } + } + } + bobTimeline.addListener(listener) + bobTimeline.paginate(Timeline.Direction.BACKWARDS, 30) + } + assertEquals(false, bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS)) + assertEquals(false, bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS)) + + val onlySentEvents = runBlocking { + bobTimeline.getSnapshot() + } + .filter { + it.root.isTextMessage() + }.filter { + (it.root.content.toModel())?.body?.startsWith(message).orFalse() + } + assertEquals(numberOfMessagesToSent, onlySentEvents.size) + + bobTimeline.dispose() + cryptoTestData.cleanUp(commonTestHelper) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineTest.kt deleted file mode 100644 index 9be0a5d5af5..00000000000 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineTest.kt +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.session.room.timeline - -import com.zhuinden.monarchy.Monarchy -import org.matrix.android.sdk.InstrumentedTest - -internal class TimelineTest : InstrumentedTest { - - companion object { - private const val ROOM_ID = "roomId" - } - - private lateinit var monarchy: Monarchy - -// @Before -// fun setup() { -// Timber.plant(Timber.DebugTree()) -// Realm.init(context()) -// val testConfiguration = RealmConfiguration.Builder().name("test-realm") -// .modules(SessionRealmModule()).build() -// -// Realm.deleteRealm(testConfiguration) -// monarchy = Monarchy.Builder().setRealmConfiguration(testConfiguration).build() -// RoomDataHelper.fakeInitialSync(monarchy, ROOM_ID) -// } -// -// private fun createTimeline(initialEventId: String? = null): Timeline { -// val taskExecutor = TaskExecutor(testCoroutineDispatchers) -// val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy) -// val paginationTask = FakePaginationTask @Inject constructor(tokenChunkEventPersistor) -// val getContextOfEventTask = FakeGetContextOfEventTask @Inject constructor(tokenChunkEventPersistor) -// val roomMemberExtractor = SenderRoomMemberExtractor(ROOM_ID) -// val timelineEventFactory = TimelineEventFactory(roomMemberExtractor, EventRelationExtractor()) -// return DefaultTimeline( -// ROOM_ID, -// initialEventId, -// monarchy.realmConfiguration, -// taskExecutor, -// getContextOfEventTask, -// timelineEventFactory, -// paginationTask, -// null) -// } -// -// @Test -// fun backPaginate_shouldLoadMoreEvents_whenPaginateIsCalled() { -// val timeline = createTimeline() -// timeline.start() -// val paginationCount = 30 -// var initialLoad = 0 -// val latch = CountDownLatch(2) -// var timelineEvents: List = emptyList() -// timeline.listener = object : Timeline.Listener { -// override fun onTimelineUpdated(snapshot: List) { -// if (snapshot.isNotEmpty()) { -// if (initialLoad == 0) { -// initialLoad = snapshot.size -// } -// timelineEvents = snapshot -// latch.countDown() -// timeline.paginate(Timeline.Direction.BACKWARDS, paginationCount) -// } -// } -// } -// latch.await() -// timelineEvents.size shouldBeEqualTo initialLoad + paginationCount -// timeline.dispose() -// } -} diff --git a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/DisplayMaths.kt b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/DisplayMaths.kt new file mode 100644 index 00000000000..b8ee36e7240 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/DisplayMaths.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.commonmark.ext.maths + +import org.commonmark.node.CustomBlock + +class DisplayMaths(private val delimiter: DisplayDelimiter) : CustomBlock() { + enum class DisplayDelimiter { + DOUBLE_DOLLAR, + SQUARE_BRACKET_ESCAPED + } +} diff --git a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/InlineMaths.kt b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/InlineMaths.kt new file mode 100644 index 00000000000..962b1b8cbf3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/InlineMaths.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.commonmark.ext.maths + +import org.commonmark.node.CustomNode +import org.commonmark.node.Delimited + +class InlineMaths(private val delimiter: InlineDelimiter) : CustomNode(), Delimited { + enum class InlineDelimiter { + SINGLE_DOLLAR, + ROUND_BRACKET_ESCAPED + } + + override fun getOpeningDelimiter(): String { + return when (delimiter) { + InlineDelimiter.SINGLE_DOLLAR -> "$" + InlineDelimiter.ROUND_BRACKET_ESCAPED -> "\\(" + } + } + + override fun getClosingDelimiter(): String { + return when (delimiter) { + InlineDelimiter.SINGLE_DOLLAR -> "$" + InlineDelimiter.ROUND_BRACKET_ESCAPED -> "\\)" + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/MathsExtension.kt b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/MathsExtension.kt new file mode 100644 index 00000000000..18c0fc4284b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/MathsExtension.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.commonmark.ext.maths + +import org.commonmark.Extension +import org.commonmark.ext.maths.internal.DollarMathsDelimiterProcessor +import org.commonmark.ext.maths.internal.MathsHtmlNodeRenderer +import org.commonmark.parser.Parser +import org.commonmark.renderer.html.HtmlRenderer + +class MathsExtension private constructor() : Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension { + override fun extend(parserBuilder: Parser.Builder) { + parserBuilder.customDelimiterProcessor(DollarMathsDelimiterProcessor()) + } + + override fun extend(rendererBuilder: HtmlRenderer.Builder) { + rendererBuilder.nodeRendererFactory { context -> MathsHtmlNodeRenderer(context) } + } + + companion object { + fun create(): Extension { + return MathsExtension() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/DollarMathsDelimiterProcessor.kt b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/DollarMathsDelimiterProcessor.kt new file mode 100644 index 00000000000..cfd03fa8f16 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/DollarMathsDelimiterProcessor.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.commonmark.ext.maths.internal + +import org.commonmark.ext.maths.DisplayMaths +import org.commonmark.ext.maths.InlineMaths +import org.commonmark.node.Text +import org.commonmark.parser.delimiter.DelimiterProcessor +import org.commonmark.parser.delimiter.DelimiterRun + +class DollarMathsDelimiterProcessor : DelimiterProcessor { + override fun getOpeningCharacter() = '$' + + override fun getClosingCharacter() = '$' + + override fun getMinLength() = 1 + + override fun getDelimiterUse(opener: DelimiterRun, closer: DelimiterRun): Int { + return if (opener.length() == 1 && closer.length() == 1) 1 // inline + else if (opener.length() == 2 && closer.length() == 2) 2 // display + else 0 + } + + override fun process(opener: Text, closer: Text, delimiterUse: Int) { + val maths = if (delimiterUse == 1) { + InlineMaths(InlineMaths.InlineDelimiter.SINGLE_DOLLAR) + } else { + DisplayMaths(DisplayMaths.DisplayDelimiter.DOUBLE_DOLLAR) + } + var tmp = opener.next + while (tmp != null && tmp !== closer) { + val next = tmp.next + maths.appendChild(tmp) + tmp = next + } + opener.insertAfter(maths) + } +} diff --git a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsHtmlNodeRenderer.kt b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsHtmlNodeRenderer.kt new file mode 100644 index 00000000000..94652ed7ad8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsHtmlNodeRenderer.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.commonmark.ext.maths.internal + +import org.commonmark.ext.maths.DisplayMaths +import org.commonmark.node.Node +import org.commonmark.node.Text +import org.commonmark.renderer.html.HtmlNodeRendererContext +import org.commonmark.renderer.html.HtmlWriter +import java.util.Collections + +class MathsHtmlNodeRenderer(private val context: HtmlNodeRendererContext) : MathsNodeRenderer() { + private val html: HtmlWriter = context.writer + override fun render(node: Node) { + val display = node.javaClass == DisplayMaths::class.java + val contents = node.firstChild // should be the only child + val latex = (contents as Text).literal + val attributes = context.extendAttributes(node, if (display) "div" else "span", Collections.singletonMap("data-mx-maths", + latex)) + html.tag(if (display) "div" else "span", attributes) + html.tag("code") + context.render(contents) + html.tag("/code") + html.tag(if (display) "/div" else "/span") + } +} diff --git a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsNodeRenderer.kt b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsNodeRenderer.kt new file mode 100644 index 00000000000..55cdc05c398 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsNodeRenderer.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.commonmark.ext.maths.internal + +import org.commonmark.ext.maths.DisplayMaths +import org.commonmark.ext.maths.InlineMaths +import org.commonmark.node.Node +import org.commonmark.renderer.NodeRenderer +import java.util.HashSet + +abstract class MathsNodeRenderer : NodeRenderer { + override fun getNodeTypes(): Set> { + val types: MutableSet> = HashSet() + types.add(InlineMaths::class.java) + types.add(DisplayMaths::class.java) + return types + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/RoomEncryptionTrustLevel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/RoomEncryptionTrustLevel.kt index f381ae8455a..8ba99ad70b4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/RoomEncryptionTrustLevel.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/RoomEncryptionTrustLevel.kt @@ -27,5 +27,8 @@ enum class RoomEncryptionTrustLevel { Warning, // All devices in the room are verified -> the app should display a green shield - Trusted + Trusted, + + // e2e is active but with an unsupported algorithm + E2EWithUnsupportedAlgorithm } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/EventStreamService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/EventStreamService.kt new file mode 100644 index 00000000000..a1316a54445 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/EventStreamService.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session + +interface EventStreamService { + + fun addEventStreamListener(streamListener: LiveEventListener) + + fun removeEventStreamListener(streamListener: LiveEventListener) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/LiveEventListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/LiveEventListener.kt new file mode 100644 index 00000000000..6fda65953ac --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/LiveEventListener.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.util.JsonDict + +interface LiveEventListener { + + fun onLiveEvent(roomId: String, event: Event) + + fun onPaginatedEvent(roomId: String, event: Event) + + fun onEventDecrypted(eventId: String, roomId: String, clearEvent: JsonDict) + + fun onEventDecryptionError(eventId: String, roomId: String, throwable: Throwable) + + fun onLiveToDeviceEvent(event: Event) + + // Maybe later add more, like onJoin, onLeave.. +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt index 3f817ec4d2b..36ab0073142 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt @@ -84,7 +84,9 @@ interface Session : SyncStatusService, HomeServerCapabilitiesService, SecureStorageService, - AccountService { + AccountService, + ToDeviceService, + EventStreamService { val coroutineDispatchers: MatrixCoroutineDispatchers diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/ToDeviceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/ToDeviceService.kt new file mode 100644 index 00000000000..45fd39fa954 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/ToDeviceService.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session + +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import java.util.UUID + +interface ToDeviceService { + + /** + * Send an event to a specific list of devices + */ + suspend fun sendToDevice(eventType: String, contentMap: MXUsersDevicesMap, txnId: String? = UUID.randomUUID().toString()) + + suspend fun sendToDevice(eventType: String, userId: String, deviceId: String, content: Content, txnId: String? = UUID.randomUUID().toString()) { + sendToDevice(eventType, mapOf(userId to listOf(deviceId)), content, txnId) + } + + suspend fun sendToDevice(eventType: String, targets: Map>, content: Content, txnId: String? = UUID.randomUUID().toString()) + + suspend fun sendEncryptedToDevice(eventType: String, targets: Map>, content: Content, txnId: String? = UUID.randomUUID().toString()) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataTypes.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataTypes.kt index 69b15ff7d4c..91167d896f0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataTypes.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataTypes.kt @@ -27,4 +27,5 @@ object UserAccountDataTypes { const val TYPE_ALLOWED_WIDGETS = "im.vector.setting.allowed_widgets" const val TYPE_IDENTITY_SERVER = "m.identity_server" const val TYPE_ACCEPTED_TERMS = "m.accepted_terms" + const val TYPE_OVERRIDE_COLORS = "im.vector.setting.override_colors" } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/accountdata/RoomAccountDataTypes.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/accountdata/RoomAccountDataTypes.kt index 96eb86c0d65..312fb7e1645 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/accountdata/RoomAccountDataTypes.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/accountdata/RoomAccountDataTypes.kt @@ -21,4 +21,5 @@ object RoomAccountDataTypes { const val EVENT_TYPE_TAG = "m.tag" const val EVENT_TYPE_FULLY_READ = "m.fully_read" const val EVENT_TYPE_SPACE_ORDER = "org.matrix.msc3230.space_order" // m.space_order + const val EVENT_TYPE_TAGGED_EVENTS = "m.tagged_events" } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt index 6581247b90a..445d16b72bc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt @@ -27,9 +27,12 @@ interface RoomCryptoService { fun shouldEncryptForInvitedMembers(): Boolean /** - * Enable encryption of the room + * Enable encryption of the room. + * @param Use force to ensure that this algorithm will be used. Otherwise this call + * will throw if encryption is already setup or if the algorithm is not supported. Only to + * be used by admins to fix misconfigured encryption. */ - suspend fun enableEncryption(algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM) + suspend fun enableEncryption(algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM, force: Boolean = false) /** * Ensures all members of the room are loaded and outbound session keys are shared. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomEncryptionAlgorithm.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomEncryptionAlgorithm.kt new file mode 100644 index 00000000000..f6812169294 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomEncryptionAlgorithm.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model + +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM + +sealed class RoomEncryptionAlgorithm { + + abstract class SupportedAlgorithm(val alg: String) : RoomEncryptionAlgorithm() + + object Megolm : SupportedAlgorithm(MXCRYPTO_ALGORITHM_MEGOLM) + + data class UnsupportedAlgorithm(val name: String?) : RoomEncryptionAlgorithm() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt index 10cad026bcb..c793a04f9de 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt @@ -62,7 +62,8 @@ data class RoomSummary( val roomType: String? = null, val spaceParents: List? = null, val spaceChildren: List? = null, - val flattenParentIds: List = emptyList() + val flattenParentIds: List = emptyList(), + val roomEncryptionAlgorithm: RoomEncryptionAlgorithm? = null ) { val isVersioned: Boolean diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt index 5b387c3413e..606500c4e72 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt @@ -56,6 +56,15 @@ interface SendService { */ fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String = MessageType.MSGTYPE_TEXT): Cancelable + /** + * Method to quote an events content. + * @param quotedEvent The event to which we will quote it's content. + * @param text the text message to send + * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present + * @return a [Cancelable] + */ + fun sendQuotedTextMessage(quotedEvent: TimelineEvent, text: String, autoMarkdown: Boolean): Cancelable + /** * Method to send a media asynchronously. * @param attachment the media to send diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt index 06c88db8316..241e5f3b9b8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt @@ -71,14 +71,10 @@ interface Timeline { fun paginate(direction: Direction, count: Int) /** - * Returns the number of sending events + * This is the same than the regular paginate method but waits for the results instead + * of relying on the timeline listener. */ - fun pendingEventCount(): Int - - /** - * Returns the number of failed sending events. - */ - fun failedToDeliverEventCount(): Int + suspend fun awaitPaginate(direction: Direction, count: Int): List /** * Returns the index of a built event or null. @@ -86,14 +82,14 @@ interface Timeline { fun getIndexOfEvent(eventId: String?): Int? /** - * Returns the built [TimelineEvent] at index or null + * Returns the current pagination state for the direction. */ - fun getTimelineEventAtIndex(index: Int): TimelineEvent? + fun getPaginationState(direction: Direction): PaginationState /** - * Returns the built [TimelineEvent] with eventId or null + * Returns a snapshot of the timeline in his current state. */ - fun getTimelineEventWithId(eventId: String?): TimelineEvent? + fun getSnapshot(): List interface Listener { /** @@ -101,19 +97,33 @@ interface Timeline { * The latest event is the first in the list * @param snapshot the most up to date snapshot */ - fun onTimelineUpdated(snapshot: List) + fun onTimelineUpdated(snapshot: List) = Unit /** * Called whenever an error we can't recover from occurred */ - fun onTimelineFailure(throwable: Throwable) + fun onTimelineFailure(throwable: Throwable) = Unit /** * Called when new events come through the sync */ - fun onNewTimelineEvents(eventIds: List) + fun onNewTimelineEvents(eventIds: List) = Unit + + /** + * Called when the pagination state has changed in one direction + */ + fun onStateUpdated(direction: Direction, state: PaginationState) = Unit } + /** + * Pagination state + */ + data class PaginationState( + val hasMoreToLoad: Boolean = true, + val loading: Boolean = false, + val inError: Boolean = false + ) + /** * This is used to paginate in one or another direction. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt index 932439c81c6..45dc322420f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt @@ -47,6 +47,10 @@ data class TimelineEvent( */ val localId: Long, val eventId: String, + /** + * This display index is the position in the current chunk. + * It's not unique on the timeline as it's reset on each chunk. + */ val displayIndex: Int, val senderInfo: SenderInfo, val annotations: EventAnnotationsSummary? = null, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt index 5338e7e92f5..82eced43711 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt @@ -37,7 +37,6 @@ internal class CryptoSessionInfoProvider @Inject constructor( fun isRoomEncrypted(roomId: String): Boolean { val encryptionEvent = monarchy.fetchCopied { realm -> EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION) - .contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"") .isEmpty(EventEntityFields.STATE_KEY) .findFirst() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index 7d9c3514100..7dd8cc73ae2 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -90,6 +90,7 @@ import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.extensions.foldToCallback import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.StreamEventsManager import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskThread @@ -168,14 +169,15 @@ internal class DefaultCryptoService @Inject constructor( private val coroutineDispatchers: MatrixCoroutineDispatchers, private val taskExecutor: TaskExecutor, private val cryptoCoroutineScope: CoroutineScope, - private val eventDecryptor: EventDecryptor + private val eventDecryptor: EventDecryptor, + private val liveEventManager: Lazy ) : CryptoService { private val isStarting = AtomicBoolean(false) private val isStarted = AtomicBoolean(false) fun onStateEvent(roomId: String, event: Event) { - when (event.getClearType()) { + when (event.type) { EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) @@ -183,10 +185,13 @@ internal class DefaultCryptoService @Inject constructor( } fun onLiveEvent(roomId: String, event: Event) { - when (event.getClearType()) { - EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) - EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) - EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) + // handle state events + if (event.isStateEvent()) { + when (event.type) { + EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) + EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) + EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) + } } } @@ -429,7 +434,17 @@ internal class DefaultCryptoService @Inject constructor( val currentCount = syncResponse.deviceOneTimeKeysCount.signedCurve25519 ?: 0 oneTimeKeysUploader.updateOneTimeKeyCount(currentCount) } - if (isStarted()) { + // There is a limit of to_device events returned per sync. + // If we are in a case of such limited to_device sync we can't try to generate/upload + // new otk now, because there might be some pending olm pre-key to_device messages that would fail if we rotate + // the old otk too early. In this case we want to wait for the pending to_device before doing anything + // As per spec: + // If there is a large queue of send-to-device messages, the server should limit the number sent in each /sync response. + // 100 messages is recommended as a reasonable limit. + // The limit is not part of the spec, so it's probably safer to handle that when there are no more to_device ( so we are sure + // that there are no pending to_device + val toDevices = syncResponse.toDevice?.events.orEmpty() + if (isStarted() && toDevices.isEmpty()) { // Make sure we process to-device messages before generating new one-time-keys #2782 deviceListManager.refreshOutdatedDeviceLists() // The presence of device_unused_fallback_key_types indicates that the server supports fallback keys. @@ -563,26 +578,31 @@ internal class DefaultCryptoService @Inject constructor( // (for now at least. Maybe we should alert the user somehow?) val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId) - if (!existingAlgorithm.isNullOrEmpty() && existingAlgorithm != algorithm) { - Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId") + if (existingAlgorithm == algorithm) { + // ignore + Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Ignoring m.room.encryption for same alg ($algorithm) in $roomId") return false } val encryptingClass = MXCryptoAlgorithms.hasEncryptorClassForAlgorithm(algorithm) + // Always store even if not supported + cryptoStore.storeRoomAlgorithm(roomId, algorithm) + if (!encryptingClass) { Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Unable to encrypt room $roomId with $algorithm") return false } - cryptoStore.storeRoomAlgorithm(roomId, algorithm!!) - - val alg: IMXEncrypting = when (algorithm) { + val alg: IMXEncrypting? = when (algorithm) { MXCRYPTO_ALGORITHM_MEGOLM -> megolmEncryptionFactory.create(roomId) - else -> olmEncryptionFactory.create(roomId) + MXCRYPTO_ALGORITHM_OLM -> olmEncryptionFactory.create(roomId) + else -> null } - roomEncryptorsStore.put(roomId, alg) + if (alg != null) { + roomEncryptorsStore.put(roomId, alg) + } // if encryption was not previously enabled in this room, we will have been // ignoring new device events for these users so far. We may well have @@ -772,6 +792,7 @@ internal class DefaultCryptoService @Inject constructor( } } } + liveEventManager.get().dispatchOnLiveToDevice(event) } /** @@ -914,6 +935,7 @@ internal class DefaultCryptoService @Inject constructor( } private fun onRoomHistoryVisibilityEvent(roomId: String, event: Event) { + if (!event.isStateEvent()) return val eventContent = event.content.toModel() eventContent?.historyVisibility?.let { cryptoStore.setShouldEncryptForInvitedMembers(roomId, it != RoomHistoryVisibility.JOINED) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt index 8bbc71543cf..2ee24dfbb06 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm +import dagger.Lazy import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCoroutineDispatchers @@ -43,6 +44,7 @@ import org.matrix.android.sdk.internal.crypto.model.rest.ForwardedRoomKeyContent import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.session.StreamEventsManager import timber.log.Timber private val loggerTag = LoggerTag("MXMegolmDecryption", LoggerTag.CRYPTO) @@ -56,7 +58,8 @@ internal class MXMegolmDecryption(private val userId: String, private val cryptoStore: IMXCryptoStore, private val sendToDeviceTask: SendToDeviceTask, private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val cryptoCoroutineScope: CoroutineScope + private val cryptoCoroutineScope: CoroutineScope, + private val liveEventManager: Lazy ) : IMXDecrypting, IMXWithHeldExtension { var newSessionListener: NewSessionListener? = null @@ -108,12 +111,15 @@ internal class MXMegolmDecryption(private val userId: String, claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"), forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain .orEmpty() - ) + ).also { + liveEventManager.get().dispatchLiveEventDecrypted(event, it) + } } else { throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) } }, { throwable -> + liveEventManager.get().dispatchLiveEventDecryptionFailed(event, throwable) if (throwable is MXCryptoError.OlmError) { // TODO Check the value of .message if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") { @@ -133,6 +139,11 @@ internal class MXMegolmDecryption(private val userId: String, if (requestKeysOnFail) { requestKeysForEvent(event, false) } + + throw MXCryptoError.Base( + MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX, + "UNKNOWN_MESSAGE_INDEX", + null) } val reason = String.format(MXCryptoError.OLM_REASON, throwable.olmException.message) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt index 29f9d193f84..3eba04b9f18 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm +import dagger.Lazy import kotlinx.coroutines.CoroutineScope import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.crypto.DeviceListManager @@ -26,6 +27,7 @@ import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.StreamEventsManager import javax.inject.Inject internal class MXMegolmDecryptionFactory @Inject constructor( @@ -38,7 +40,8 @@ internal class MXMegolmDecryptionFactory @Inject constructor( private val cryptoStore: IMXCryptoStore, private val sendToDeviceTask: SendToDeviceTask, private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val cryptoCoroutineScope: CoroutineScope + private val cryptoCoroutineScope: CoroutineScope, + private val eventsManager: Lazy ) { fun create(): MXMegolmDecryption { @@ -52,6 +55,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor( cryptoStore, sendToDeviceTask, coroutineDispatchers, - cryptoCoroutineScope) + cryptoCoroutineScope, + eventsManager) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptionEventContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptionEventContent.kt index b64cd97ff60..dd76ae1d8e1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptionEventContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptionEventContent.kt @@ -27,7 +27,7 @@ data class EncryptionEventContent( * Required. The encryption algorithm to be used to encrypt messages sent in this room. Must be 'm.megolm.v1.aes-sha2'. */ @Json(name = "algorithm") - val algorithm: String, + val algorithm: String?, /** * How long the session should be used before changing it. 604800000 (a week) is the recommended default. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt index 9b75f88f917..82fb5653777 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt @@ -230,7 +230,7 @@ internal interface IMXCryptoStore { * @param roomId the id of the room. * @param algorithm the algorithm. */ - fun storeRoomAlgorithm(roomId: String, algorithm: String) + fun storeRoomAlgorithm(roomId: String, algorithm: String?) /** * Provides the algorithm used in a dedicated room. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt index 40678a6ce64..33578ba06af 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt @@ -629,7 +629,7 @@ internal class RealmCryptoStore @Inject constructor( } } - override fun storeRoomAlgorithm(roomId: String, algorithm: String) { + override fun storeRoomAlgorithm(roomId: String, algorithm: String?) { doRealmTransaction(realmConfiguration) { CryptoRoomEntity.getOrCreate(it, roomId).algorithm = algorithm } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt deleted file mode 100644 index 7341d4656a4..00000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.database - -import io.realm.Realm -import io.realm.RealmConfiguration -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.SessionLifecycleObserver -import org.matrix.android.sdk.internal.database.helper.nextDisplayIndex -import org.matrix.android.sdk.internal.database.model.ChunkEntity -import org.matrix.android.sdk.internal.database.model.ChunkEntityFields -import org.matrix.android.sdk.internal.database.model.EventEntity -import org.matrix.android.sdk.internal.database.model.RoomEntity -import org.matrix.android.sdk.internal.database.model.TimelineEventEntity -import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields -import org.matrix.android.sdk.internal.database.model.deleteOnCascade -import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection -import org.matrix.android.sdk.internal.task.TaskExecutor -import timber.log.Timber -import javax.inject.Inject - -private const val MAX_NUMBER_OF_EVENTS_IN_DB = 35_000L -private const val MIN_NUMBER_OF_EVENTS_BY_CHUNK = 300 - -/** - * This class makes sure to stay under a maximum number of events as it makes Realm to be unusable when listening to events - * when the database is getting too big. This will try incrementally to remove the biggest chunks until we get below the threshold. - * We make sure to still have a minimum number of events so it's not becoming unusable. - * So this won't work for users with a big number of very active rooms. - */ -internal class DatabaseCleaner @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration, - private val taskExecutor: TaskExecutor) : SessionLifecycleObserver { - - override fun onSessionStarted(session: Session) { - taskExecutor.executorScope.launch(Dispatchers.Default) { - awaitTransaction(realmConfiguration) { realm -> - val allRooms = realm.where(RoomEntity::class.java).findAll() - Timber.v("There are ${allRooms.size} rooms in this session") - cleanUp(realm, MAX_NUMBER_OF_EVENTS_IN_DB / 2L) - } - } - } - - private fun cleanUp(realm: Realm, threshold: Long) { - val numberOfEvents = realm.where(EventEntity::class.java).findAll().size - val numberOfTimelineEvents = realm.where(TimelineEventEntity::class.java).findAll().size - Timber.v("Number of events in db: $numberOfEvents | Number of timeline events in db: $numberOfTimelineEvents") - if (threshold <= MIN_NUMBER_OF_EVENTS_BY_CHUNK || numberOfTimelineEvents < MAX_NUMBER_OF_EVENTS_IN_DB) { - Timber.v("Db is low enough") - } else { - val thresholdChunks = realm.where(ChunkEntity::class.java) - .greaterThan(ChunkEntityFields.NUMBER_OF_TIMELINE_EVENTS, threshold) - .findAll() - - Timber.v("There are ${thresholdChunks.size} chunks to clean with more than $threshold events") - for (chunk in thresholdChunks) { - val maxDisplayIndex = chunk.nextDisplayIndex(PaginationDirection.FORWARDS) - val thresholdDisplayIndex = maxDisplayIndex - threshold - val eventsToRemove = chunk.timelineEvents.where().lessThan(TimelineEventEntityFields.DISPLAY_INDEX, thresholdDisplayIndex).findAll() - Timber.v("There are ${eventsToRemove.size} events to clean in chunk: ${chunk.identifier()} from room ${chunk.room?.first()?.roomId}") - chunk.numberOfTimelineEvents = chunk.numberOfTimelineEvents - eventsToRemove.size - eventsToRemove.forEach { - val canDeleteRoot = it.root?.stateKey == null - it.deleteOnCascade(canDeleteRoot) - } - // We reset the prevToken so we will need to fetch again. - chunk.prevToken = null - } - cleanUp(realm, (threshold / 1.5).toLong()) - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 2256d931001..1f45ac2a753 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -25,6 +25,8 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent import org.matrix.android.sdk.api.session.room.model.VersioningState import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent import org.matrix.android.sdk.api.session.room.model.tag.RoomTag +import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent +import org.matrix.android.sdk.internal.database.model.ChunkEntityFields import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntityFields import org.matrix.android.sdk.internal.database.model.EditionOfEventFields @@ -54,7 +56,7 @@ internal class RealmSessionStoreMigration @Inject constructor( ) : RealmMigration { companion object { - const val SESSION_STORE_SCHEMA_VERSION = 19L + const val SESSION_STORE_SCHEMA_VERSION = 21L } /** @@ -86,6 +88,8 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion <= 16) migrateTo17(realm) if (oldVersion <= 17) migrateTo18(realm) if (oldVersion <= 18) migrateTo19(realm) + if (oldVersion <= 19) migrateTo20(realm) + if (oldVersion <= 20) migrateTo21(realm) } private fun migrateTo1(realm: DynamicRealm) { @@ -390,4 +394,55 @@ internal class RealmSessionStoreMigration @Inject constructor( } } } + + private fun migrateTo20(realm: DynamicRealm) { + Timber.d("Step 19 -> 20") + + realm.schema.get("ChunkEntity")?.apply { + if (hasField("numberOfTimelineEvents")) { + removeField("numberOfTimelineEvents") + } + var cleanOldChunks = false + if (!hasField(ChunkEntityFields.NEXT_CHUNK.`$`)) { + cleanOldChunks = true + addRealmObjectField(ChunkEntityFields.NEXT_CHUNK.`$`, this) + } + if (!hasField(ChunkEntityFields.PREV_CHUNK.`$`)) { + cleanOldChunks = true + addRealmObjectField(ChunkEntityFields.PREV_CHUNK.`$`, this) + } + if (cleanOldChunks) { + val chunkEntities = realm.where("ChunkEntity").equalTo(ChunkEntityFields.IS_LAST_FORWARD, false).findAll() + chunkEntities.deleteAllFromRealm() + } + } + } + + private fun migrateTo21(realm: DynamicRealm) { + Timber.d("Step 20 -> 21") + + realm.schema.get("RoomSummaryEntity") + ?.addField(RoomSummaryEntityFields.E2E_ALGORITHM, String::class.java) + ?.transform { obj -> + + val encryptionContentAdapter = MoshiProvider.providesMoshi().adapter(EncryptionEventContent::class.java) + + val encryptionEvent = realm.where("CurrentStateEventEntity") + .equalTo(CurrentStateEventEntityFields.ROOM_ID, obj.getString(RoomSummaryEntityFields.ROOM_ID)) + .equalTo(CurrentStateEventEntityFields.TYPE, EventType.STATE_ROOM_ENCRYPTION) + .findFirst() + + val encryptionEventRoot = encryptionEvent?.getObject(CurrentStateEventEntityFields.ROOT.`$`) + val algorithm = encryptionEventRoot + ?.getString(EventEntityFields.CONTENT)?.let { + encryptionContentAdapter.fromJson(it)?.algorithm + } + + obj.setString(RoomSummaryEntityFields.E2E_ALGORITHM, algorithm) + obj.setBoolean(RoomSummaryEntityFields.IS_ENCRYPTED, encryptionEvent != null) + encryptionEventRoot?.getLong(EventEntityFields.ORIGIN_SERVER_TS)?.let { + obj.setLong(RoomSummaryEntityFields.ENCRYPTION_EVENT_TS, it) + } + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt index f74e4b0f4c6..c21bf74d934 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt @@ -110,7 +110,7 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String, true } } - numberOfTimelineEvents++ + // numberOfTimelineEvents++ timelineEvents.add(timelineEventEntity) } @@ -191,3 +191,29 @@ internal fun ChunkEntity.nextDisplayIndex(direction: PaginationDirection): Int { } } } + +internal fun ChunkEntity.doesNextChunksVerifyCondition(linkCondition: (ChunkEntity) -> Boolean): Boolean { + var nextChunkToCheck = this.nextChunk + while (nextChunkToCheck != null) { + if (linkCondition(nextChunkToCheck)) { + return true + } + nextChunkToCheck = nextChunkToCheck.nextChunk + } + return false +} + +internal fun ChunkEntity.isMoreRecentThan(chunkToCheck: ChunkEntity): Boolean { + if (this.isLastForward) return true + if (chunkToCheck.isLastForward) return false + // Check if the chunk to check is linked to this one + if (chunkToCheck.doesNextChunksVerifyCondition { it == this }) { + return true + } + // Otherwise check if this chunk is linked to last forward + if (this.doesNextChunksVerifyCondition { it.isLastForward }) { + return true + } + // We don't know, so we assume it's false + return false +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/TimelineEventEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/TimelineEventEntityHelper.kt index 3993e8e7991..1d2cbcad51d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/TimelineEventEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/TimelineEventEntityHelper.kt @@ -28,3 +28,13 @@ internal fun TimelineEventEntity.Companion.nextId(realm: Realm): Long { currentIdNum.toLong() + 1 } } + +internal fun TimelineEventEntity.isMoreRecentThan(eventToCheck: TimelineEventEntity): Boolean { + val currentChunk = this.chunk?.first() ?: return false + val chunkToCheck = eventToCheck.chunk?.firstOrNull() ?: return false + return if (currentChunk == chunkToCheck) { + this.displayIndex >= eventToCheck.displayIndex + } else { + currentChunk.isMoreRecentThan(chunkToCheck) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt index 3a15e0acf01..63b326096a8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt @@ -16,12 +16,15 @@ package org.matrix.android.sdk.internal.database.mapper +import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.session.room.model.RoomEncryptionAlgorithm import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import org.matrix.android.sdk.api.session.room.model.SpaceParentInfo import org.matrix.android.sdk.api.session.room.model.tag.RoomTag import org.matrix.android.sdk.api.session.typing.TypingUsersTracker +import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.presence.toUserPresence import javax.inject.Inject @@ -68,7 +71,9 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa isEncrypted = roomSummaryEntity.isEncrypted, encryptionEventTs = roomSummaryEntity.encryptionEventTs, breadcrumbsIndex = roomSummaryEntity.breadcrumbsIndex, - roomEncryptionTrustLevel = roomSummaryEntity.roomEncryptionTrustLevel, + roomEncryptionTrustLevel = if (roomSummaryEntity.isEncrypted && roomSummaryEntity.e2eAlgorithm != MXCRYPTO_ALGORITHM_MEGOLM) { + RoomEncryptionTrustLevel.E2EWithUnsupportedAlgorithm + } else roomSummaryEntity.roomEncryptionTrustLevel, inviterId = roomSummaryEntity.inviterId, hasFailedSending = roomSummaryEntity.hasFailedSending, roomType = roomSummaryEntity.roomType, @@ -99,7 +104,13 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa worldReadable = it.childSummaryEntity?.joinRules == RoomJoinRules.PUBLIC ) }, - flattenParentIds = roomSummaryEntity.flattenParentIds?.split("|") ?: emptyList() + flattenParentIds = roomSummaryEntity.flattenParentIds?.split("|") ?: emptyList(), + roomEncryptionAlgorithm = when (val alg = roomSummaryEntity.e2eAlgorithm) { + // I should probably use #hasEncryptorClassForAlgorithm but it says it supports + // OLM which is some legacy? Now only megolm allowed in rooms + MXCRYPTO_ALGORITHM_MEGOLM -> RoomEncryptionAlgorithm.Megolm + else -> RoomEncryptionAlgorithm.UnsupportedAlgorithm(alg) + } ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt index 68533a3c199..ecb602019ab 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt @@ -27,9 +27,10 @@ import org.matrix.android.sdk.internal.extensions.clearWith internal open class ChunkEntity(@Index var prevToken: String? = null, // Because of gaps we can have several chunks with nextToken == null @Index var nextToken: String? = null, + var prevChunk: ChunkEntity? = null, + var nextChunk: ChunkEntity? = null, var stateEvents: RealmList = RealmList(), var timelineEvents: RealmList = RealmList(), - var numberOfTimelineEvents: Long = 0, // Only one chunk will have isLastForward == true @Index var isLastForward: Boolean = false, @Index var isLastBackward: Boolean = false diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt index 836fc4efaf9..ce2d1efc1d2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt @@ -40,8 +40,6 @@ internal open class EventEntity(@Index var eventId: String = "", var unsignedData: String? = null, var redacts: String? = null, var decryptionResultJson: String? = null, - var decryptionErrorCode: String? = null, - var decryptionErrorReason: String? = null, var ageLocalTs: Long? = null ) : RealmObject() { @@ -55,6 +53,16 @@ internal open class EventEntity(@Index var eventId: String = "", sendStateStr = value.name } + var decryptionErrorCode: String? = null + set(value) { + if (value != field) field = value + } + + var decryptionErrorReason: String? = null + set(value) { + if (value != field) field = value + } + companion object fun setDecryptionResult(result: MXEventDecryptionResult, clearEvent: JsonDict? = null) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt index 67672f03add..febedc34563 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt @@ -205,6 +205,11 @@ internal open class RoomSummaryEntity( if (value != field) field = value } + var e2eAlgorithm: String? = null + set(value) { + if (value != field) field = value + } + var encryptionEventTs: Long? = 0 set(value) { if (value != field) field = value diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt index 30bbde70c2e..185f0e2dcc4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt @@ -46,7 +46,5 @@ internal fun TimelineEventEntity.deleteOnCascade(canDeleteRoot: Boolean) { if (canDeleteRoot) { root?.deleteFromRealm() } - annotations?.deleteOnCascade() - readReceipts?.deleteOnCascade() deleteFromRealm() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt index 60096777d9f..c9c96b9cc10 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.database.query import io.realm.Realm import io.realm.RealmConfiguration import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.internal.database.helper.isMoreRecentThan import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity @@ -33,28 +34,26 @@ internal fun isEventRead(realmConfiguration: RealmConfiguration, if (LocalEcho.isLocalEchoId(eventId)) { return true } + // If we don't know if the event has been read, we assume it's not var isEventRead = false Realm.getInstance(realmConfiguration).use { realm -> - val liveChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) ?: return@use - val eventToCheck = liveChunk.timelineEvents.find(eventId) + val latestEvent = TimelineEventEntity.latestEvent(realm, roomId, true) + // If latest event is from you we are sure the event is read + if (latestEvent?.root?.sender == userId) { + return true + } + val eventToCheck = TimelineEventEntity.where(realm, roomId, eventId).findFirst() isEventRead = when { - eventToCheck == null -> hasReadMissingEvent( - realm = realm, - latestChunkEntity = liveChunk, - roomId = roomId, - userId = userId, - eventId = eventId - ) + eventToCheck == null -> false eventToCheck.root?.sender == userId -> true else -> { val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst() ?: return@use - val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.displayIndex ?: Int.MIN_VALUE - eventToCheck.displayIndex <= readReceiptIndex + val readReceiptEvent = TimelineEventEntity.where(realm, roomId, readReceipt.eventId).findFirst() ?: return@use + readReceiptEvent.isMoreRecentThan(eventToCheck) } } } - return isEventRead } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultEventStreamService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultEventStreamService.kt new file mode 100644 index 00000000000..ed21e9f1c62 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultEventStreamService.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session + +import org.matrix.android.sdk.api.session.EventStreamService +import org.matrix.android.sdk.api.session.LiveEventListener +import javax.inject.Inject + +internal class DefaultEventStreamService @Inject constructor( + private val streamEventsManager: StreamEventsManager +) : EventStreamService { + + override fun addEventStreamListener(streamListener: LiveEventListener) { + streamEventsManager.addLiveEventListener(streamListener) + } + + override fun removeEventStreamListener(streamListener: LiveEventListener) { + streamEventsManager.removeLiveEventListener(streamListener) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt index c07ff48cf48..1e533158a76 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt @@ -27,8 +27,10 @@ import org.matrix.android.sdk.api.auth.data.SessionParams import org.matrix.android.sdk.api.failure.GlobalError import org.matrix.android.sdk.api.federation.FederationService import org.matrix.android.sdk.api.pushrules.PushRuleService +import org.matrix.android.sdk.api.session.EventStreamService import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.SessionLifecycleObserver +import org.matrix.android.sdk.api.session.ToDeviceService import org.matrix.android.sdk.api.session.account.AccountService import org.matrix.android.sdk.api.session.accountdata.SessionAccountDataService import org.matrix.android.sdk.api.session.cache.CacheService @@ -133,6 +135,8 @@ internal class DefaultSession @Inject constructor( private val spaceService: Lazy, private val openIdService: Lazy, private val presenceService: Lazy, + private val toDeviceService: Lazy, + private val eventStreamService: Lazy, @UnauthenticatedWithCertificate private val unauthenticatedWithCertificateOkHttpClient: Lazy ) : Session, @@ -152,7 +156,9 @@ internal class DefaultSession @Inject constructor( HomeServerCapabilitiesService by homeServerCapabilitiesService.get(), ProfileService by profileService.get(), PresenceService by presenceService.get(), - AccountService by accountService.get() { + AccountService by accountService.get(), + ToDeviceService by toDeviceService.get(), + EventStreamService by eventStreamService.get() { override val sharedSecretStorageService: SharedSecretStorageService get() = _sharedSecretStorageService.get() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultToDeviceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultToDeviceService.kt new file mode 100644 index 00000000000..1615b8eef9a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultToDeviceService.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session + +import org.matrix.android.sdk.api.session.ToDeviceService +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import javax.inject.Inject + +internal class DefaultToDeviceService @Inject constructor( + private val sendToDeviceTask: SendToDeviceTask, + private val messageEncrypter: MessageEncrypter, + private val cryptoStore: IMXCryptoStore +) : ToDeviceService { + + override suspend fun sendToDevice(eventType: String, targets: Map>, content: Content, txnId: String?) { + val sendToDeviceMap = MXUsersDevicesMap() + targets.forEach { (userId, deviceIdList) -> + deviceIdList.forEach { deviceId -> + sendToDeviceMap.setObject(userId, deviceId, content) + } + } + sendToDevice(eventType, sendToDeviceMap, txnId) + } + + override suspend fun sendToDevice(eventType: String, contentMap: MXUsersDevicesMap, txnId: String?) { + sendToDeviceTask.executeRetry( + SendToDeviceTask.Params( + eventType = eventType, + contentMap = contentMap, + transactionId = txnId + ), + 3 + ) + } + + override suspend fun sendEncryptedToDevice(eventType: String, targets: Map>, content: Content, txnId: String?) { + val payloadJson = mapOf( + "type" to eventType, + "content" to content + ) + val sendToDeviceMap = MXUsersDevicesMap() + + // Should I do an ensure olm session? + targets.forEach { (userId, deviceIdList) -> + deviceIdList.forEach { deviceId -> + cryptoStore.getUserDevice(userId, deviceId)?.let { deviceInfo -> + sendToDeviceMap.setObject(userId, deviceId, messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))) + } + } + } + + sendToDevice(EventType.ENCRYPTED, sendToDeviceMap, txnId) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt index ebc2176a130..531dea1d5a6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt @@ -32,8 +32,10 @@ import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.data.SessionParams import org.matrix.android.sdk.api.auth.data.sessionId import org.matrix.android.sdk.api.crypto.MXCryptoConfig +import org.matrix.android.sdk.api.session.EventStreamService import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.SessionLifecycleObserver +import org.matrix.android.sdk.api.session.ToDeviceService import org.matrix.android.sdk.api.session.accountdata.SessionAccountDataService import org.matrix.android.sdk.api.session.events.EventService import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService @@ -47,7 +49,6 @@ import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorage import org.matrix.android.sdk.internal.crypto.tasks.DefaultRedactEventTask import org.matrix.android.sdk.internal.crypto.tasks.RedactEventTask import org.matrix.android.sdk.internal.crypto.verification.VerificationMessageProcessor -import org.matrix.android.sdk.internal.database.DatabaseCleaner import org.matrix.android.sdk.internal.database.EventInsertLiveObserver import org.matrix.android.sdk.internal.database.RealmSessionProvider import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory @@ -339,10 +340,6 @@ internal abstract class SessionModule { @IntoSet abstract fun bindIdentityService(service: DefaultIdentityService): SessionLifecycleObserver - @Binds - @IntoSet - abstract fun bindDatabaseCleaner(cleaner: DatabaseCleaner): SessionLifecycleObserver - @Binds @IntoSet abstract fun bindRealmSessionProvider(provider: RealmSessionProvider): SessionLifecycleObserver @@ -379,6 +376,12 @@ internal abstract class SessionModule { @Binds abstract fun bindOpenIdTokenService(service: DefaultOpenIdService): OpenIdService + @Binds + abstract fun bindToDeviceService(service: DefaultToDeviceService): ToDeviceService + + @Binds + abstract fun bindEventStreamService(service: DefaultEventStreamService): EventStreamService + @Binds abstract fun bindTypingUsersTracker(tracker: DefaultTypingUsersTracker): TypingUsersTracker diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt new file mode 100644 index 00000000000..bb0ca114454 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.LiveEventListener +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult +import timber.log.Timber +import javax.inject.Inject + +@SessionScope +internal class StreamEventsManager @Inject constructor() { + + private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + + private val listeners = mutableListOf() + + fun addLiveEventListener(listener: LiveEventListener) { + listeners.add(listener) + } + + fun removeLiveEventListener(listener: LiveEventListener) { + listeners.remove(listener) + } + + fun dispatchLiveEventReceived(event: Event, roomId: String, initialSync: Boolean) { + Timber.v("## dispatchLiveEventReceived ${event.eventId}") + coroutineScope.launch { + if (!initialSync) { + listeners.forEach { + tryOrNull { + it.onLiveEvent(roomId, event) + } + } + } + } + } + + fun dispatchPaginatedEventReceived(event: Event, roomId: String) { + Timber.v("## dispatchPaginatedEventReceived ${event.eventId}") + coroutineScope.launch { + listeners.forEach { + tryOrNull { + it.onPaginatedEvent(roomId, event) + } + } + } + } + + fun dispatchLiveEventDecrypted(event: Event, result: MXEventDecryptionResult) { + Timber.v("## dispatchLiveEventDecrypted ${event.eventId}") + coroutineScope.launch { + listeners.forEach { + tryOrNull { + it.onEventDecrypted(event.eventId ?: "", event.roomId ?: "", result.clearEvent) + } + } + } + } + + fun dispatchLiveEventDecryptionFailed(event: Event, error: Throwable) { + Timber.v("## dispatchLiveEventDecryptionFailed ${event.eventId}") + coroutineScope.launch { + listeners.forEach { + tryOrNull { + it.onEventDecryptionError(event.eventId ?: "", event.roomId ?: "", error) + } + } + } + } + + fun dispatchOnLiveToDevice(event: Event) { + Timber.v("## dispatchOnLiveToDevice ${event.eventId}") + coroutineScope.launch { + listeners.forEach { + tryOrNull { + it.onLiveToDeviceEvent(event) + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt index 1b0ccbb4895..b988f2253cd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt @@ -109,18 +109,23 @@ internal class FileUploader @Inject constructor( filename: String?, mimeType: String?, progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { - val inputStream = withContext(Dispatchers.IO) { - context.contentResolver.openInputStream(uri) - } ?: throw FileNotFoundException() - val workingFile = temporaryFileCreator.create() - workingFile.outputStream().use { - inputStream.copyTo(it) - } + val workingFile = context.copyUriToTempFile(uri) return uploadFile(workingFile, filename, mimeType, progressListener).also { tryOrNull { workingFile.delete() } } } + private suspend fun Context.copyUriToTempFile(uri: Uri): File { + return withContext(Dispatchers.IO) { + val inputStream = contentResolver.openInputStream(uri) ?: throw FileNotFoundException() + val workingFile = temporaryFileCreator.create() + workingFile.outputStream().use { + inputStream.copyTo(it) + } + workingFile + } + } + private suspend fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): ContentUploadResponse { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt index a19832c5230..caf41586579 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt @@ -68,7 +68,7 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto } override suspend fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String) { - withContext(coroutineDispatchers.main) { + withContext(coroutineDispatchers.io) { val response = fileUploader.uploadFromUri(newAvatarUri, fileName, MimeTypes.Jpeg) setAvatarUrlTask.execute(SetAvatarUrlTask.Params(userId = userId, newAvatarUrl = response.contentUri)) userStore.updateAvatar(userId, response.contentUri) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt index cb4bcdb606a..1fe7503141d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt @@ -119,15 +119,15 @@ internal class DefaultRoom(override val roomId: String, } } - override suspend fun enableEncryption(algorithm: String) { + override suspend fun enableEncryption(algorithm: String, force: Boolean) { when { - isEncrypted() -> { + (!force && isEncrypted() && encryptionAlgorithm() == MXCRYPTO_ALGORITHM_MEGOLM) -> { throw IllegalStateException("Encryption is already enabled for this room") } - algorithm != MXCRYPTO_ALGORITHM_MEGOLM -> { + (!force && algorithm != MXCRYPTO_ALGORITHM_MEGOLM) -> { throw InvalidParameterException("Only MXCRYPTO_ALGORITHM_MEGOLM algorithm is supported") } - else -> { + else -> { val params = SendStateTask.Params( roomId = roomId, stateKey = null, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index dbd0ae6f060..64f6bc0b30d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -19,6 +19,9 @@ package org.matrix.android.sdk.internal.session.room import dagger.Binds import dagger.Module import dagger.Provides +import org.commonmark.Extension +import org.commonmark.ext.maths.MathsExtension +import org.commonmark.node.BlockQuote import org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer import org.matrix.android.sdk.api.session.file.FileService @@ -98,12 +101,29 @@ import org.matrix.android.sdk.internal.session.room.version.DefaultRoomVersionUp import org.matrix.android.sdk.internal.session.room.version.RoomVersionUpgradeTask import org.matrix.android.sdk.internal.session.space.DefaultSpaceService import retrofit2.Retrofit +import javax.inject.Qualifier + +/** + * Used to inject the simple commonmark Parser + */ +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class SimpleCommonmarkParser + +/** + * Used to inject the advanced commonmark Parser + */ +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class AdvancedCommonmarkParser @Module internal abstract class RoomModule { @Module companion object { + private val extensions: List = listOf(MathsExtension.create()) + @Provides @JvmStatic @SessionScope @@ -119,9 +139,21 @@ internal abstract class RoomModule { } @Provides + @AdvancedCommonmarkParser @JvmStatic - fun providesParser(): Parser { - return Parser.builder().build() + fun providesAdvancedParser(): Parser { + return Parser.builder().extensions(extensions).build() + } + + @Provides + @SimpleCommonmarkParser + @JvmStatic + fun providesSimpleParser(): Parser { + // The simple parser disables all blocks but quotes. + // Inline parsing(bold, italic, etc) is also enabled and is not easy to disable in commonmark currently. + return Parser.builder() + .enabledBlockTypes(setOf(BlockQuote::class.java)) + .build() } @Provides @@ -129,6 +161,7 @@ internal abstract class RoomModule { fun providesHtmlRenderer(): HtmlRenderer { return HtmlRenderer .builder() + .extensions(extensions) .softbreak("
") .build() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt index 28f55a01eeb..b30c66c82ed 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt @@ -34,12 +34,10 @@ import org.matrix.android.sdk.internal.database.query.isEventRead import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.task.TaskExecutor internal class DefaultReadService @AssistedInject constructor( @Assisted private val roomId: String, @SessionDatabase private val monarchy: Monarchy, - private val taskExecutor: TaskExecutor, private val setReadMarkersTask: SetReadMarkersTask, private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, @UserId private val userId: String diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/CancelSendTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/CancelSendTracker.kt index 5f6ebc68c2b..fe180536c89 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/CancelSendTracker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/CancelSendTracker.kt @@ -44,10 +44,10 @@ internal class CancelSendTracker @Inject constructor() { } fun isCancelRequestedFor(eventId: String?, roomId: String?): Boolean { - val index = synchronized(cancellingRequests) { - cancellingRequests.indexOfFirst { it.localId == eventId && it.roomId == roomId } + val found = synchronized(cancellingRequests) { + cancellingRequests.any { it.localId == eventId && it.roomId == roomId } } - return index != -1 + return found } fun markCancelled(eventId: String, roomId: String) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt index d3162aef796..fb2fb3950af 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -97,6 +97,12 @@ internal class DefaultSendService @AssistedInject constructor( .let { sendEvent(it) } } + override fun sendQuotedTextMessage(quotedEvent: TimelineEvent, text: String, autoMarkdown: Boolean): Cancelable { + return localEchoEventFactory.createQuotedTextEvent(roomId, quotedEvent, text, autoMarkdown) + .also { createLocalEcho(it) } + .let { sendEvent(it) } + } + override fun sendPoll(question: String, options: List): Cancelable { return localEchoEventFactory.createPollEvent(roomId, question, options) .also { createLocalEcho(it) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index 85b22628d78..c4caedc4077 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -198,20 +198,23 @@ internal class LocalEchoEventFactory @Inject constructor( eventReplaced: TimelineEvent, originalEvent: TimelineEvent, newBodyText: String, - newBodyAutoMarkdown: Boolean, + autoMarkdown: Boolean, msgType: String, compatibilityText: String): Event { val permalink = permalinkFactory.createPermalink(roomId, originalEvent.root.eventId ?: "", false) val userLink = originalEvent.root.senderId?.let { permalinkFactory.createPermalink(it, false) } ?: "" val body = bodyForReply(originalEvent.getLastMessageContent(), originalEvent.isReply()) - val replyFormatted = REPLY_PATTERN.format( + // As we always supply formatted body for replies we should force the MarkdownParser to produce html. + val newBodyFormatted = markdownParser.parse(newBodyText, force = true, advanced = autoMarkdown).takeFormatted() + // Body of the original message may not have formatted version, so may also have to convert to html. + val bodyFormatted = body.formattedText ?: markdownParser.parse(body.text, force = true, advanced = autoMarkdown).takeFormatted() + val replyFormatted = buildFormattedReply( permalink, userLink, originalEvent.senderInfo.disambiguatedDisplayName, - // Remove inner mx_reply tags if any - body.takeFormatted().replace(MX_REPLY_REGEX, ""), - createTextContent(newBodyText, newBodyAutoMarkdown).takeFormatted() + bodyFormatted, + newBodyFormatted ) // // > <@alice:example.org> This is the original body @@ -391,13 +394,17 @@ internal class LocalEchoEventFactory @Inject constructor( val userLink = permalinkFactory.createPermalink(userId, false) ?: return null val body = bodyForReply(eventReplied.getLastMessageContent(), eventReplied.isReply()) - val replyFormatted = REPLY_PATTERN.format( + + // As we always supply formatted body for replies we should force the MarkdownParser to produce html. + val replyTextFormatted = markdownParser.parse(replyText, force = true, advanced = autoMarkdown).takeFormatted() + // Body of the original message may not have formatted version, so may also have to convert to html. + val bodyFormatted = body.formattedText ?: markdownParser.parse(body.text, force = true, advanced = autoMarkdown).takeFormatted() + val replyFormatted = buildFormattedReply( permalink, userLink, userId, - // Remove inner mx_reply tags if any - body.takeFormatted().replace(MX_REPLY_REGEX, ""), - createTextContent(replyText, autoMarkdown).takeFormatted() + bodyFormatted, + replyTextFormatted ) // // > <@alice:example.org> This is the original body @@ -415,6 +422,16 @@ internal class LocalEchoEventFactory @Inject constructor( return createMessageEvent(roomId, content) } + private fun buildFormattedReply(permalink: String, userLink: String, userId: String, bodyFormatted: String, newBodyFormatted: String): String { + return REPLY_PATTERN.format( + permalink, + userLink, + userId, + // Remove inner mx_reply tags if any + bodyFormatted.replace(MX_REPLY_REGEX, ""), + newBodyFormatted + ) + } private fun buildReplyFallback(body: TextContent, originalSenderId: String?, newBodyText: String): String { return buildString { append("> <") @@ -498,6 +515,38 @@ internal class LocalEchoEventFactory @Inject constructor( localEchoRepository.createLocalEcho(event) } + fun createQuotedTextEvent( + roomId: String, + quotedEvent: TimelineEvent, + text: String, + autoMarkdown: Boolean, + ): Event { + val messageContent = quotedEvent.getLastMessageContent() + val textMsg = messageContent?.body + val quoteText = legacyRiotQuoteText(textMsg, text) + return createFormattedTextEvent(roomId, markdownParser.parse(quoteText, force = true, advanced = autoMarkdown), MessageType.MSGTYPE_TEXT) + } + + private fun legacyRiotQuoteText(quotedText: String?, myText: String): String { + val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray() + return buildString { + if (messageParagraphs != null) { + for (i in messageParagraphs.indices) { + if (messageParagraphs[i].isNotBlank()) { + append("> ") + append(messageParagraphs[i]) + } + + if (i != messageParagraphs.lastIndex) { + append("\n\n") + } + } + } + append("\n\n") + append(myText) + } + } + companion object { // //
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt index c99d482300a..ef7945cf8cc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt @@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.session.room.send import org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer +import org.matrix.android.sdk.internal.session.room.AdvancedCommonmarkParser +import org.matrix.android.sdk.internal.session.room.SimpleCommonmarkParser import org.matrix.android.sdk.internal.session.room.send.pills.TextPillsUtils import javax.inject.Inject @@ -27,22 +29,30 @@ import javax.inject.Inject * If any change is required, please add a test covering the problem and make sure all the tests are still passing. */ internal class MarkdownParser @Inject constructor( - private val parser: Parser, + @AdvancedCommonmarkParser private val advancedParser: Parser, + @SimpleCommonmarkParser private val simpleParser: Parser, private val htmlRenderer: HtmlRenderer, private val textPillsUtils: TextPillsUtils ) { - private val mdSpecialChars = "[`_\\-*>.\\[\\]#~]".toRegex() + private val mdSpecialChars = "[`_\\-*>.\\[\\]#~$]".toRegex() - fun parse(text: CharSequence): TextContent { + /** + * Parses some input text and produces html. + * @param text An input CharSequence to be parsed. + * @param force Skips the check for detecting if the input contains markdown and always converts to html. + * @param advanced Whether to use the full markdown support or the simple version. + * @return TextContent containing the plain text and the formatted html if generated. + */ + fun parse(text: CharSequence, force: Boolean = false, advanced: Boolean = true): TextContent { val source = textPillsUtils.processSpecialSpansToMarkdown(text) ?: text.toString() // If no special char are detected, just return plain text - if (source.contains(mdSpecialChars).not()) { + if (!force && source.contains(mdSpecialChars).not()) { return TextContent(source) } - val document = parser.parse(source) + val document = if (advanced) advancedParser.parse(source) else simpleParser.parse(source) val htmlText = htmlRenderer.render(document) // Cleanup extra paragraph diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt index 3556cabb335..a7887d77f87 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.summary import io.realm.Realm import io.realm.kotlin.createObject +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel @@ -37,13 +38,11 @@ import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.sync.model.RoomSyncSummary import org.matrix.android.sdk.api.session.sync.model.RoomSyncUnreadNotifications import org.matrix.android.sdk.internal.crypto.EventDecryptor -import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService +import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity -import org.matrix.android.sdk.internal.database.model.EventEntity -import org.matrix.android.sdk.internal.database.model.EventEntityFields import org.matrix.android.sdk.internal.database.model.GroupSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity @@ -56,7 +55,6 @@ import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.getOrNull import org.matrix.android.sdk.internal.database.query.isEventRead import org.matrix.android.sdk.internal.database.query.where -import org.matrix.android.sdk.internal.database.query.whereType import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.extensions.clearWith import org.matrix.android.sdk.internal.query.process @@ -122,10 +120,8 @@ internal class RoomSummaryUpdater @Inject constructor( Timber.v("## Space: Updating summary room [$roomId] roomType: [$roomType]") // Don't use current state for this one as we are only interested in having MXCRYPTO_ALGORITHM_MEGOLM event in the room - val encryptionEvent = EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION) - .contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"") - .isNotNull(EventEntityFields.STATE_KEY) - .findFirst() + val encryptionEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ENCRYPTION, stateKey = "")?.root + Timber.v("## CRYPTO: currentEncryptionEvent is $encryptionEvent") val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) @@ -136,7 +132,7 @@ internal class RoomSummaryUpdater @Inject constructor( roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 || // avoid this call if we are sure there are unread events - !isEventRead(realm.configuration, userId, roomId, latestPreviewableEvent?.eventId) + latestPreviewableEvent?.let { !isEventRead(realm.configuration, userId, roomId, it.eventId) } ?: false roomSummaryEntity.setDisplayName(roomDisplayNameResolver.resolve(realm, roomId)) roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(realm, roomId) @@ -151,6 +147,11 @@ internal class RoomSummaryUpdater @Inject constructor( .orEmpty() roomSummaryEntity.updateAliases(roomAliases) roomSummaryEntity.isEncrypted = encryptionEvent != null + + roomSummaryEntity.e2eAlgorithm = ContentMapper.map(encryptionEvent?.content) + ?.toModel() + ?.algorithm + roomSummaryEntity.encryptionEventTs = encryptionEvent?.originServerTs if (roomSummaryEntity.membership == Membership.INVITE && inviterId != null) { @@ -236,7 +237,7 @@ internal class RoomSummaryUpdater @Inject constructor( .findFirst() ?.let { childSum -> lookupMap.entries.firstOrNull { it.key.roomId == lookedUp.roomId }?.let { entry -> - if (entry.value.indexOfFirst { it.roomId == childSum.roomId } == -1) { + if (entry.value.none { it.roomId == childSum.roomId }) { // add looked up as a parent entry.value.add(childSum) } @@ -299,7 +300,7 @@ internal class RoomSummaryUpdater @Inject constructor( .process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships()) .findFirst() ?.let { parentSum -> - if (lookupMap[parentSum]?.indexOfFirst { it.roomId == lookedUp.roomId } == -1) { + if (lookupMap[parentSum]?.none { it.roomId == lookedUp.roomId }.orFalse()) { // add lookedup as a parent lookupMap[parentSum]?.add(lookedUp) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/taggedevents/TaggedEventsContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/taggedevents/TaggedEventsContent.kt new file mode 100644 index 00000000000..1b19d27e1d4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/taggedevents/TaggedEventsContent.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.taggedevents + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Keys are event IDs, values are event information. + */ +typealias TaggedEvent = Map + +/** + * Keys are tagged event names (eg. m.favourite), values are the related events. + */ +typealias TaggedEvents = Map + +/** + * Class used to parse the content of a m.tagged_events type event. + * This kind of event defines the tagged events in a room. + * + * The content of this event is a tags key whose value is an object mapping the name of each tag + * to another object. The JSON object associated with each tag is an object where the keys are the + * event IDs and values give information about the events. + * + * Ref: https://github.com/matrix-org/matrix-doc/pull/2437 + */ +@JsonClass(generateAdapter = true) +data class TaggedEventsContent( + @Json(name = "tags") + var tags: TaggedEvents = emptyMap() +) { + val favouriteEvents + get() = tags[TAG_FAVOURITE].orEmpty() + + val hiddenEvents + get() = tags[TAG_HIDDEN].orEmpty() + + fun tagEvent(eventId: String, info: TaggedEventInfo, tag: String) { + val taggedEvents = tags[tag].orEmpty().plus(eventId to info) + tags = tags.plus(tag to taggedEvents) + } + + fun untagEvent(eventId: String, tag: String) { + val taggedEvents = tags[tag]?.minus(eventId).orEmpty() + tags = tags.plus(tag to taggedEvents) + } + + companion object { + const val TAG_FAVOURITE = "m.favourite" + const val TAG_HIDDEN = "m.hidden" + } +} + +@JsonClass(generateAdapter = true) +data class TaggedEventInfo( + @Json(name = "keywords") + val keywords: List? = null, + + @Json(name = "origin_server_ts") + val originServerTs: Long? = null, + + @Json(name = "tagged_at") + val taggedAt: Long? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 2744b5129e0..71823cd4585 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,738 +16,338 @@ package org.matrix.android.sdk.internal.session.room.timeline -import io.realm.OrderedCollectionChangeSet -import io.realm.OrderedRealmCollectionChangeListener import io.realm.Realm import io.realm.RealmConfiguration -import io.realm.RealmQuery -import io.realm.RealmResults -import io.realm.Sort -import kotlinx.coroutines.runBlocking -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.extensions.orFalse +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.android.asCoroutineDispatcher +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.sample +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.internal.closeQuietly +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings -import org.matrix.android.sdk.api.util.CancelableBag -import org.matrix.android.sdk.internal.database.RealmSessionProvider -import org.matrix.android.sdk.internal.database.mapper.EventMapper import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper -import org.matrix.android.sdk.internal.database.model.ChunkEntity -import org.matrix.android.sdk.internal.database.model.RoomEntity -import org.matrix.android.sdk.internal.database.model.TimelineEventEntity -import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields -import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendStates -import org.matrix.android.sdk.internal.database.query.where -import org.matrix.android.sdk.internal.database.query.whereRoomId import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.configureWith -import org.matrix.android.sdk.internal.util.Debouncer +import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer import org.matrix.android.sdk.internal.util.createBackgroundHandler -import org.matrix.android.sdk.internal.util.createUIHandler import timber.log.Timber -import java.util.Collections import java.util.UUID import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference -import kotlin.math.max - -private const val MIN_FETCHING_COUNT = 30 - -internal class DefaultTimeline( - private val roomId: String, - private var initialEventId: String? = null, - private val realmConfiguration: RealmConfiguration, - private val taskExecutor: TaskExecutor, - private val contextOfEventTask: GetContextOfEventTask, - private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, - private val paginationTask: PaginationTask, - private val timelineEventMapper: TimelineEventMapper, - private val settings: TimelineSettings, - private val timelineInput: TimelineInput, - private val eventDecryptor: TimelineEventDecryptor, - private val realmSessionProvider: RealmSessionProvider, - private val loadRoomMembersTask: LoadRoomMembersTask, - private val threadsAwarenessHandler: ThreadsAwarenessHandler, - private val readReceiptHandler: ReadReceiptHandler -) : Timeline, - TimelineInput.Listener, - UIEchoManager.Listener { + +internal class DefaultTimeline(private val roomId: String, + private val initialEventId: String?, + private val realmConfiguration: RealmConfiguration, + private val loadRoomMembersTask: LoadRoomMembersTask, + private val readReceiptHandler: ReadReceiptHandler, + private val settings: TimelineSettings, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + paginationTask: PaginationTask, + getEventTask: GetContextOfEventTask, + fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, + timelineEventMapper: TimelineEventMapper, + timelineInput: TimelineInput, + threadsAwarenessHandler: ThreadsAwarenessHandler, + eventDecryptor: TimelineEventDecryptor) : Timeline { companion object { - val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD") + val BACKGROUND_HANDLER = createBackgroundHandler("DefaultTimeline_Thread") } - private val listeners = CopyOnWriteArrayList() - private val isStarted = AtomicBoolean(false) - private val isReady = AtomicBoolean(false) - private val mainHandler = createUIHandler() - private val backgroundRealm = AtomicReference() - private val cancelableBag = CancelableBag() - private val debouncer = Debouncer(mainHandler) - - private lateinit var timelineEvents: RealmResults - private lateinit var sendingEvents: RealmResults - - private var prevDisplayIndex: Int? = null - private var nextDisplayIndex: Int? = null - - private val uiEchoManager = UIEchoManager(settings, this) - - private val builtEvents = Collections.synchronizedList(ArrayList()) - private val builtEventsIdMap = Collections.synchronizedMap(HashMap()) - private val backwardsState = AtomicReference(TimelineState()) - private val forwardsState = AtomicReference(TimelineState()) - override val timelineID = UUID.randomUUID().toString() - override val isLive - get() = !hasMoreToLoad(Timeline.Direction.FORWARDS) - - private val eventsChangeListener = OrderedRealmCollectionChangeListener> { results, changeSet -> - if (!results.isLoaded || !results.isValid) { - return@OrderedRealmCollectionChangeListener - } - Timber.v("## SendEvent: [${System.currentTimeMillis()}] DB update for room $roomId") - handleUpdates(results, changeSet) - } + private val listeners = CopyOnWriteArrayList() + private val isStarted = AtomicBoolean(false) + private val forwardState = AtomicReference(Timeline.PaginationState()) + private val backwardState = AtomicReference(Timeline.PaginationState()) - // Public methods ****************************************************************************** + private val backgroundRealm = AtomicReference() + private val timelineDispatcher = BACKGROUND_HANDLER.asCoroutineDispatcher() + private val timelineScope = CoroutineScope(SupervisorJob() + timelineDispatcher) + private val sequencer = SemaphoreCoroutineSequencer() + private val postSnapshotSignalFlow = MutableSharedFlow(0) + + private val strategyDependencies = LoadTimelineStrategy.Dependencies( + timelineSettings = settings, + realm = backgroundRealm, + eventDecryptor = eventDecryptor, + paginationTask = paginationTask, + fetchTokenAndPaginateTask = fetchTokenAndPaginateTask, + getContextOfEventTask = getEventTask, + timelineInput = timelineInput, + timelineEventMapper = timelineEventMapper, + threadsAwarenessHandler = threadsAwarenessHandler, + onEventsUpdated = this::sendSignalToPostSnapshot, + onLimitedTimeline = this::onLimitedTimeline, + onNewTimelineEvents = this::onNewTimelineEvents + ) + + private var strategy: LoadTimelineStrategy = buildStrategy(LoadTimelineStrategy.Mode.Live) + + override val isLive: Boolean + get() = !getPaginationState(Timeline.Direction.FORWARDS).hasMoreToLoad - override fun paginate(direction: Timeline.Direction, count: Int) { - BACKGROUND_HANDLER.post { - if (!canPaginate(direction)) { - return@post - } - Timber.v("Paginate $direction of $count items") - val startDisplayIndex = if (direction == Timeline.Direction.BACKWARDS) prevDisplayIndex else nextDisplayIndex - val shouldPostSnapshot = paginateInternal(startDisplayIndex, direction, count) - if (shouldPostSnapshot) { - postSnapshot() + override fun addListener(listener: Timeline.Listener): Boolean { + listeners.add(listener) + timelineScope.launch { + val snapshot = strategy.buildSnapshot() + withContext(coroutineDispatchers.main) { + tryOrNull { listener.onTimelineUpdated(snapshot) } } } + return true } - override fun pendingEventCount(): Int { - return realmSessionProvider.withRealm { - RoomEntity.where(it, roomId).findFirst()?.sendingTimelineEvents?.count() ?: 0 - } + override fun removeListener(listener: Timeline.Listener): Boolean { + return listeners.remove(listener) } - override fun failedToDeliverEventCount(): Int { - return realmSessionProvider.withRealm { - TimelineEventEntity.findAllInRoomWithSendStates(it, roomId, SendState.HAS_FAILED_STATES).count() - } + override fun removeAllListeners() { + listeners.clear() } override fun start() { - if (isStarted.compareAndSet(false, true)) { - Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId") - timelineInput.listeners.add(this) - BACKGROUND_HANDLER.post { - eventDecryptor.start() - val realm = Realm.getInstance(realmConfiguration) - backgroundRealm.set(realm) - - val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() - ?: throw IllegalStateException("Can't open a timeline without a room") - - // We don't want to filter here because some sending events that are not displayed - // are still used for ui echo (relation like reaction) - sendingEvents = roomEntity.sendingTimelineEvents.where()/*.filterEventsWithSettings()*/.findAll() - sendingEvents.addChangeListener { events -> - uiEchoManager.onSentEventsInDatabase(events.map { it.eventId }) + timelineScope.launch { + loadRoomMembersIfNeeded() + } + timelineScope.launch { + sequencer.post { + if (isStarted.compareAndSet(false, true)) { + val realm = Realm.getInstance(realmConfiguration) + ensureReadReceiptAreLoaded(realm) + backgroundRealm.set(realm) + listenToPostSnapshotSignals() + openAround(initialEventId) postSnapshot() } - - timelineEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll() - timelineEvents.addChangeListener(eventsChangeListener) - handleInitialLoad() - loadRoomMembersTask - .configureWith(LoadRoomMembersTask.Params(roomId)) - .executeBy(taskExecutor) - - // Ensure ReadReceipt from init sync are loaded - ensureReadReceiptAreLoaded(realm) - - isReady.set(true) } } } - private fun ensureReadReceiptAreLoaded(realm: Realm) { - readReceiptHandler.getContentFromInitSync(roomId) - ?.also { - Timber.w("INIT_SYNC Insert when opening timeline RR for room $roomId") - } - ?.let { readReceiptContent -> - realm.executeTransactionAsync { - readReceiptHandler.handle(it, roomId, readReceiptContent, false, null) - readReceiptHandler.onContentFromInitSyncHandled(roomId) - } - } - } - override fun dispose() { - if (isStarted.compareAndSet(true, false)) { - isReady.set(false) - timelineInput.listeners.remove(this) - Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId") - cancelableBag.cancel() - BACKGROUND_HANDLER.removeCallbacksAndMessages(null) - BACKGROUND_HANDLER.post { - if (this::sendingEvents.isInitialized) { - sendingEvents.removeAllChangeListeners() - } - if (this::timelineEvents.isInitialized) { - timelineEvents.removeAllChangeListeners() + timelineScope.coroutineContext.cancelChildren() + timelineScope.launch { + sequencer.post { + if (isStarted.compareAndSet(true, false)) { + strategy.onStop() + backgroundRealm.get().closeQuietly() } - clearAllValues() - backgroundRealm.getAndSet(null).also { - it?.close() - } - eventDecryptor.destroy() } } } override fun restartWithEventId(eventId: String?) { - dispose() - initialEventId = eventId - start() - postSnapshot() - } - - override fun getTimelineEventAtIndex(index: Int): TimelineEvent? { - return builtEvents.getOrNull(index) - } - - override fun getIndexOfEvent(eventId: String?): Int? { - return builtEventsIdMap[eventId] - } - - override fun getTimelineEventWithId(eventId: String?): TimelineEvent? { - return builtEventsIdMap[eventId]?.let { - getTimelineEventAtIndex(it) - } - } - - override fun hasMoreToLoad(direction: Timeline.Direction): Boolean { - return hasMoreInCache(direction) || !hasReachedEnd(direction) - } - - override fun addListener(listener: Timeline.Listener): Boolean { - if (listeners.contains(listener)) { - return false - } - return listeners.add(listener).also { + timelineScope.launch { + openAround(eventId) postSnapshot() } } - override fun removeListener(listener: Timeline.Listener): Boolean { - return listeners.remove(listener) - } - - override fun removeAllListeners() { - listeners.clear() + override fun hasMoreToLoad(direction: Timeline.Direction): Boolean { + return getPaginationState(direction).hasMoreToLoad } - override fun onNewTimelineEvents(roomId: String, eventIds: List) { - if (isLive && this.roomId == roomId) { - listeners.forEach { - it.onNewTimelineEvents(eventIds) + override fun paginate(direction: Timeline.Direction, count: Int) { + timelineScope.launch { + val postSnapshot = loadMore(count, direction, fetchOnServerIfNeeded = true) + if (postSnapshot) { + postSnapshot() } } } - override fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) { - if (roomId != this.roomId || !isLive) return - uiEchoManager.onLocalEchoCreated(timelineEvent) - listeners.forEach { - tryOrNull { - it.onNewTimelineEvents(listOf(timelineEvent.eventId)) - } + override suspend fun awaitPaginate(direction: Timeline.Direction, count: Int): List { + withContext(timelineDispatcher) { + loadMore(count, direction, fetchOnServerIfNeeded = true) } - postSnapshot() + return getSnapshot() } - override fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState) { - if (roomId != this.roomId || !isLive) return - if (uiEchoManager.onSendStateUpdated(eventId, sendState)) { - postSnapshot() - } + override fun getSnapshot(): List { + return strategy.buildSnapshot() } - override fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?): Boolean { - return tryOrNull { - builtEventsIdMap[eventId]?.let { builtIndex -> - // Update the relation of existing event - builtEvents[builtIndex]?.let { te -> - val rebuiltEvent = builder(te) - // If rebuilt event is filtered its returned as null and should be removed. - if (rebuiltEvent == null) { - builtEventsIdMap.remove(eventId) - builtEventsIdMap.entries.filter { it.value > builtIndex }.forEach { it.setValue(it.value - 1) } - builtEvents.removeAt(builtIndex) - } else { - builtEvents[builtIndex] = rebuiltEvent - } - true - } - } - } ?: false + override fun getIndexOfEvent(eventId: String?): Int? { + if (eventId == null) return null + return strategy.getBuiltEventIndex(eventId) } -// Private methods ***************************************************************************** - - private fun hasMoreInCache(direction: Timeline.Direction) = getState(direction).hasMoreInCache - - private fun hasReachedEnd(direction: Timeline.Direction) = getState(direction).hasReachedEnd - - private fun updateLoadingStates(results: RealmResults) { - val lastCacheEvent = results.lastOrNull() - val firstCacheEvent = results.firstOrNull() - val chunkEntity = getLiveChunk() - - updateState(Timeline.Direction.FORWARDS) { - it.copy( - hasMoreInCache = !builtEventsIdMap.containsKey(firstCacheEvent?.eventId), - hasReachedEnd = chunkEntity?.isLastForward ?: false - ) - } - updateState(Timeline.Direction.BACKWARDS) { - it.copy( - hasMoreInCache = !builtEventsIdMap.containsKey(lastCacheEvent?.eventId), - hasReachedEnd = chunkEntity?.isLastBackward ?: false || lastCacheEvent?.root?.type == EventType.STATE_ROOM_CREATE - ) - } + override fun getPaginationState(direction: Timeline.Direction): Timeline.PaginationState { + return if (direction == Timeline.Direction.BACKWARDS) { + backwardState + } else { + forwardState + }.get() } - /** - * This has to be called on TimelineThread as it accesses realm live results - * @return true if createSnapshot should be posted - */ - private fun paginateInternal(startDisplayIndex: Int?, - direction: Timeline.Direction, - count: Int): Boolean { - if (count == 0) { + private suspend fun loadMore(count: Int, direction: Timeline.Direction, fetchOnServerIfNeeded: Boolean): Boolean { + val baseLogMessage = "loadMore(count: $count, direction: $direction, roomId: $roomId, fetchOnServer: $fetchOnServerIfNeeded)" + Timber.v("$baseLogMessage started") + if (!isStarted.get()) { + throw IllegalStateException("You should call start before using timeline") + } + val currentState = getPaginationState(direction) + if (!currentState.hasMoreToLoad) { + Timber.v("$baseLogMessage : nothing more to load") return false } - updateState(direction) { it.copy(requestedPaginationCount = count, isPaginating = true) } - val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong()) - val shouldFetchMore = builtCount < count && !hasReachedEnd(direction) - if (shouldFetchMore) { - val newRequestedCount = count - builtCount - updateState(direction) { it.copy(requestedPaginationCount = newRequestedCount) } - val fetchingCount = max(MIN_FETCHING_COUNT, newRequestedCount) - executePaginationTask(direction, fetchingCount) - } else { - updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } + if (currentState.loading) { + Timber.v("$baseLogMessage : already loading") + return false } - return !shouldFetchMore - } - - private fun createSnapshot(): List { - return buildSendingEvents() + builtEvents.toList() - } - - private fun buildSendingEvents(): List { - val builtSendingEvents = mutableListOf() - if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) { - uiEchoManager.getInMemorySendingEvents() - .updateWithUiEchoInto(builtSendingEvents) - sendingEvents - .filter { timelineEvent -> - builtSendingEvents.none { it.eventId == timelineEvent.eventId } - } - .map { timelineEventMapper.map(it) } - .updateWithUiEchoInto(builtSendingEvents) + updateState(direction) { + it.copy(loading = true) } - return builtSendingEvents - } - - private fun List.updateWithUiEchoInto(target: MutableList) { - target.addAll( - // Get most up to date send state (in memory) - map { uiEchoManager.updateSentStateWithUiEcho(it) } - ) - } - - private fun canPaginate(direction: Timeline.Direction): Boolean { - return isReady.get() && !getState(direction).isPaginating && hasMoreToLoad(direction) - } - - private fun getState(direction: Timeline.Direction): TimelineState { - return when (direction) { - Timeline.Direction.FORWARDS -> forwardsState.get() - Timeline.Direction.BACKWARDS -> backwardsState.get() + val loadMoreResult = strategy.loadMore(count, direction, fetchOnServerIfNeeded) + Timber.v("$baseLogMessage: result $loadMoreResult") + val hasMoreToLoad = loadMoreResult != LoadMoreResult.REACHED_END + updateState(direction) { + it.copy(loading = false, hasMoreToLoad = hasMoreToLoad) } + return true } - private fun updateState(direction: Timeline.Direction, update: (TimelineState) -> TimelineState) { - val stateReference = when (direction) { - Timeline.Direction.FORWARDS -> forwardsState - Timeline.Direction.BACKWARDS -> backwardsState + private suspend fun openAround(eventId: String?) = withContext(timelineDispatcher) { + val baseLogMessage = "openAround(eventId: $eventId)" + Timber.v("$baseLogMessage started") + if (!isStarted.get()) { + throw IllegalStateException("You should call start before using timeline") } - val currentValue = stateReference.get() - val newValue = update(currentValue) - stateReference.set(newValue) - } - - /** - * This has to be called on TimelineThread as it accesses realm live results - */ - private fun handleInitialLoad() { - var shouldFetchInitialEvent = false - val currentInitialEventId = initialEventId - val initialDisplayIndex = if (currentInitialEventId == null) { - timelineEvents.firstOrNull()?.displayIndex + strategy.onStop() + strategy = if (eventId == null) { + buildStrategy(LoadTimelineStrategy.Mode.Live) } else { - val initialEvent = timelineEvents.where() - .equalTo(TimelineEventEntityFields.EVENT_ID, initialEventId) - .findFirst() + buildStrategy(LoadTimelineStrategy.Mode.Permalink(eventId)) + } + initPaginationStates(eventId) + strategy.onStart() + loadMore( + count = strategyDependencies.timelineSettings.initialSize, + direction = Timeline.Direction.BACKWARDS, + fetchOnServerIfNeeded = false + ) + Timber.v("$baseLogMessage finished") + } - shouldFetchInitialEvent = initialEvent == null - initialEvent?.displayIndex + private fun initPaginationStates(eventId: String?) { + updateState(Timeline.Direction.FORWARDS) { + it.copy(loading = false, hasMoreToLoad = eventId != null) } - prevDisplayIndex = initialDisplayIndex - nextDisplayIndex = initialDisplayIndex - if (currentInitialEventId != null && shouldFetchInitialEvent) { - fetchEvent(currentInitialEventId) - } else { - val count = timelineEvents.size.coerceAtMost(settings.initialSize) - if (initialEventId == null) { - paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count) - } else { - paginateInternal(initialDisplayIndex, Timeline.Direction.FORWARDS, (count / 2).coerceAtLeast(1)) - paginateInternal(initialDisplayIndex?.minus(1), Timeline.Direction.BACKWARDS, (count / 2).coerceAtLeast(1)) - } + updateState(Timeline.Direction.BACKWARDS) { + it.copy(loading = false, hasMoreToLoad = true) } - postSnapshot() } - /** - * This has to be called on TimelineThread as it accesses realm live results - */ - private fun handleUpdates(results: RealmResults, changeSet: OrderedCollectionChangeSet) { - // If changeSet has deletion we are having a gap, so we clear everything - if (changeSet.deletionRanges.isNotEmpty()) { - clearAllValues() - } - var postSnapshot = false - changeSet.insertionRanges.forEach { range -> - val (startDisplayIndex, direction) = if (range.startIndex == 0) { - Pair(results[range.length - 1]!!.displayIndex, Timeline.Direction.FORWARDS) - } else { - Pair(results[range.startIndex]!!.displayIndex, Timeline.Direction.BACKWARDS) - } - val state = getState(direction) - if (state.isPaginating) { - // We are getting new items from pagination - postSnapshot = paginateInternal(startDisplayIndex, direction, state.requestedPaginationCount) + private fun sendSignalToPostSnapshot(withThrottling: Boolean) { + timelineScope.launch { + if (withThrottling) { + postSnapshotSignalFlow.emit(Unit) } else { - // We are getting new items from sync - buildTimelineEvents(startDisplayIndex, direction, range.length.toLong()) - postSnapshot = true - } - } - changeSet.changes.forEach { index -> - val eventEntity = results[index] - eventEntity?.eventId?.let { eventId -> - postSnapshot = rebuildEvent(eventId) { - buildTimelineEvent(eventEntity) - } || postSnapshot + postSnapshot() } } - if (postSnapshot) { - postSnapshot() - } } - /** - * This has to be called on TimelineThread as it accesses realm live results - */ - private fun executePaginationTask(direction: Timeline.Direction, limit: Int) { - val currentChunk = getLiveChunk() - val token = if (direction == Timeline.Direction.BACKWARDS) currentChunk?.prevToken else currentChunk?.nextToken - if (token == null) { - if (direction == Timeline.Direction.BACKWARDS || - (direction == Timeline.Direction.FORWARDS && currentChunk?.hasBeenALastForwardChunk().orFalse())) { - // We are in the case where event exists, but we do not know the token. - // Fetch (again) the last event to get a token - val lastKnownEventId = if (direction == Timeline.Direction.FORWARDS) { - timelineEvents.firstOrNull()?.eventId - } else { - timelineEvents.lastOrNull()?.eventId - } - if (lastKnownEventId == null) { - updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } - } else { - val params = FetchTokenAndPaginateTask.Params( - roomId = roomId, - limit = limit, - direction = direction.toPaginationDirection(), - lastKnownEventId = lastKnownEventId - ) - cancelableBag += fetchTokenAndPaginateTask - .configureWith(params) { - this.callback = createPaginationCallback(limit, direction) - } - .executeBy(taskExecutor) + @Suppress("EXPERIMENTAL_API_USAGE") + private fun listenToPostSnapshotSignals() { + postSnapshotSignalFlow + .sample(150) + .onEach { + postSnapshot() } - } else { - updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } - } - } else { - val params = PaginationTask.Params( - roomId = roomId, - from = token, - direction = direction.toPaginationDirection(), - limit = limit - ) - Timber.v("Should fetch $limit items $direction") - cancelableBag += paginationTask - .configureWith(params) { - this.callback = createPaginationCallback(limit, direction) - } - .executeBy(taskExecutor) - } + .launchIn(timelineScope) } - // For debug purpose only - private fun dumpAndLogChunks() { - val liveChunk = getLiveChunk() - Timber.w("Live chunk: $liveChunk") - - Realm.getInstance(realmConfiguration).use { realm -> - ChunkEntity.where(realm, roomId).findAll() - .also { Timber.w("Found ${it.size} chunks") } - .forEach { - Timber.w("") - Timber.w("ChunkEntity: $it") - Timber.w("prevToken: ${it.prevToken}") - Timber.w("nextToken: ${it.nextToken}") - Timber.w("isLastBackward: ${it.isLastBackward}") - Timber.w("isLastForward: ${it.isLastForward}") - it.timelineEvents.forEach { tle -> - Timber.w(" TLE: ${tle.root?.content}") - } - } + private fun onLimitedTimeline() { + timelineScope.launch { + initPaginationStates(null) + loadMore(settings.initialSize, Timeline.Direction.BACKWARDS, false) + postSnapshot() } } - /** - * This has to be called on TimelineThread as it accesses realm live results - */ - private fun getTokenLive(direction: Timeline.Direction): String? { - val chunkEntity = getLiveChunk() ?: return null - return if (direction == Timeline.Direction.BACKWARDS) chunkEntity.prevToken else chunkEntity.nextToken - } - - /** - * This has to be called on TimelineThread as it accesses realm live results - * Return the current Chunk - */ - private fun getLiveChunk(): ChunkEntity? { - return timelineEvents.firstOrNull()?.chunk?.firstOrNull() - } - - /** - * This has to be called on TimelineThread as it accesses realm live results - * @return the number of items who have been added - */ - private fun buildTimelineEvents(startDisplayIndex: Int?, - direction: Timeline.Direction, - count: Long): Int { - if (count < 1 || startDisplayIndex == null) { - return 0 - } - val start = System.currentTimeMillis() - val offsetResults = getOffsetResults(startDisplayIndex, direction, count) - if (offsetResults.isEmpty()) { - return 0 - } - val offsetIndex = offsetResults.last()!!.displayIndex - if (direction == Timeline.Direction.BACKWARDS) { - prevDisplayIndex = offsetIndex - 1 - } else { - nextDisplayIndex = offsetIndex + 1 - } - - // Prerequisite to in order for the ThreadsAwarenessHandler to work properly - fetchRootThreadEventsIfNeeded(offsetResults) - - offsetResults.forEach { eventEntity -> - - val timelineEvent = buildTimelineEvent(eventEntity) - val transactionId = timelineEvent.root.unsignedData?.transactionId - uiEchoManager.onSyncedEvent(transactionId) - - if (timelineEvent.isEncrypted() && - timelineEvent.root.mxDecryptionResult == null) { - timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineID)) } + private suspend fun postSnapshot() { + val snapshot = strategy.buildSnapshot() + Timber.v("Post snapshot of ${snapshot.size} events") + withContext(coroutineDispatchers.main) { + listeners.forEach { + tryOrNull { it.onTimelineUpdated(snapshot) } } - - val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size - builtEvents.add(position, timelineEvent) - // Need to shift :/ - builtEventsIdMap.entries.filter { it.value >= position }.forEach { it.setValue(it.value + 1) } - builtEventsIdMap[eventEntity.eventId] = position } - val time = System.currentTimeMillis() - start - Timber.v("Built ${offsetResults.size} items from db in $time ms") - // For the case where wo reach the lastForward chunk - updateLoadingStates(timelineEvents) - return offsetResults.size - } - - /** - * This function is responsible to fetch and store the root event of a thread event - * in order to be able to display the event to the user appropriately - */ - private fun fetchRootThreadEventsIfNeeded(offsetResults: RealmResults) = runBlocking { - val eventEntityList = offsetResults - .mapNotNull { - it?.root - }.map { - EventMapper.map(it) - } - threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(eventEntityList) } - private fun buildTimelineEvent(eventEntity: TimelineEventEntity): TimelineEvent { - return timelineEventMapper.map( - timelineEventEntity = eventEntity, - buildReadReceipts = settings.buildReadReceipts - ).let { timelineEvent -> - // eventually enhance with ui echo? - uiEchoManager.decorateEventWithReactionUiEcho(timelineEvent) ?: timelineEvent + private fun onNewTimelineEvents(eventIds: List) { + timelineScope.launch(coroutineDispatchers.main) { + listeners.forEach { + tryOrNull { it.onNewTimelineEvents(eventIds) } + } } } - /** - * This has to be called on TimelineThread as it accesses realm live results - */ - private fun getOffsetResults(startDisplayIndex: Int, - direction: Timeline.Direction, - count: Long): RealmResults { - val offsetQuery = timelineEvents.where() - if (direction == Timeline.Direction.BACKWARDS) { - offsetQuery - .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) - .lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex) - } else { - offsetQuery - .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) - .greaterThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex) + private fun updateState(direction: Timeline.Direction, update: (Timeline.PaginationState) -> Timeline.PaginationState) { + val stateReference = when (direction) { + Timeline.Direction.FORWARDS -> forwardState + Timeline.Direction.BACKWARDS -> backwardState } - return offsetQuery - .limit(count) - .findAll() - } - - private fun buildEventQuery(realm: Realm): RealmQuery { - return if (initialEventId == null) { - TimelineEventEntity - .whereRoomId(realm, roomId = roomId) - .equalTo(TimelineEventEntityFields.CHUNK.IS_LAST_FORWARD, true) - } else { - TimelineEventEntity - .whereRoomId(realm, roomId = roomId) - .`in`("${TimelineEventEntityFields.CHUNK.TIMELINE_EVENTS}.${TimelineEventEntityFields.EVENT_ID}", arrayOf(initialEventId)) + val currentValue = stateReference.get() + val newValue = update(currentValue) + stateReference.set(newValue) + if (newValue != currentValue) { + postPaginationState(direction, newValue) } } - private fun fetchEvent(eventId: String) { - val params = GetContextOfEventTask.Params(roomId, eventId) - cancelableBag += contextOfEventTask.configureWith(params) { - callback = object : MatrixCallback { - override fun onSuccess(data: TokenChunkEventPersistor.Result) { - postSnapshot() - } - - override fun onFailure(failure: Throwable) { - postFailure(failure) - } + private fun postPaginationState(direction: Timeline.Direction, state: Timeline.PaginationState) { + timelineScope.launch(coroutineDispatchers.main) { + Timber.v("Post $direction pagination state: $state ") + listeners.forEach { + tryOrNull { it.onStateUpdated(direction, state) } } } - .executeBy(taskExecutor) } - private fun postSnapshot() { - BACKGROUND_HANDLER.post { - if (isReady.get().not()) { - return@post - } - updateLoadingStates(timelineEvents) - val snapshot = createSnapshot() - val runnable = Runnable { - listeners.forEach { - it.onTimelineUpdated(snapshot) - } - } - debouncer.debounce("post_snapshot", runnable, 1) - } + private fun buildStrategy(mode: LoadTimelineStrategy.Mode): LoadTimelineStrategy { + return LoadTimelineStrategy( + roomId = roomId, + timelineId = timelineID, + mode = mode, + dependencies = strategyDependencies + ) } - private fun postFailure(throwable: Throwable) { - if (isReady.get().not()) { - return + private suspend fun loadRoomMembersIfNeeded() { + val loadRoomMembersParam = LoadRoomMembersTask.Params(roomId) + try { + loadRoomMembersTask.execute(loadRoomMembersParam) + } catch (failure: Throwable) { + Timber.v("Failed to load room members. Retry in 10s.") + delay(10_000L) + loadRoomMembersIfNeeded() } - val runnable = Runnable { - listeners.forEach { - it.onTimelineFailure(throwable) - } - } - mainHandler.post(runnable) } - private fun clearAllValues() { - prevDisplayIndex = null - nextDisplayIndex = null - builtEvents.clear() - builtEventsIdMap.clear() - backwardsState.set(TimelineState()) - forwardsState.set(TimelineState()) - } - - private fun createPaginationCallback(limit: Int, direction: Timeline.Direction): MatrixCallback { - return object : MatrixCallback { - override fun onSuccess(data: TokenChunkEventPersistor.Result) { - when (data) { - TokenChunkEventPersistor.Result.SUCCESS -> { - Timber.v("Success fetching $limit items $direction from pagination request") - } - TokenChunkEventPersistor.Result.REACHED_END -> { - postSnapshot() + private fun ensureReadReceiptAreLoaded(realm: Realm) { + readReceiptHandler.getContentFromInitSync(roomId) + ?.also { + Timber.w("INIT_SYNC Insert when opening timeline RR for room $roomId") + } + ?.let { readReceiptContent -> + realm.executeTransactionAsync { + readReceiptHandler.handle(it, roomId, readReceiptContent, false, null) + readReceiptHandler.onContentFromInitSyncHandled(roomId) } - TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE -> - // Database won't be updated, so we force pagination request - BACKGROUND_HANDLER.post { - executePaginationTask(direction, limit) - } } - } - - override fun onFailure(failure: Throwable) { - updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } - postSnapshot() - Timber.v("Failure fetching $limit items $direction from pagination request") - } - } - } - - // Extension methods *************************************************************************** - - private fun Timeline.Direction.toPaginationDirection(): PaginationDirection { - return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt index 75e7e774df4..126374b430c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -23,6 +23,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.realm.Sort import io.realm.kotlin.where +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.session.events.model.isImageMessage import org.matrix.android.sdk.api.session.events.model.isVideoMessage import org.matrix.android.sdk.api.session.room.timeline.Timeline @@ -54,7 +55,8 @@ internal class DefaultTimelineService @AssistedInject constructor( private val timelineEventMapper: TimelineEventMapper, private val loadRoomMembersTask: LoadRoomMembersTask, private val threadsAwarenessHandler: ThreadsAwarenessHandler, - private val readReceiptHandler: ReadReceiptHandler + private val readReceiptHandler: ReadReceiptHandler, + private val coroutineDispatchers: MatrixCoroutineDispatchers ) : TimelineService { @AssistedFactory @@ -66,19 +68,18 @@ internal class DefaultTimelineService @AssistedInject constructor( return DefaultTimeline( roomId = roomId, initialEventId = eventId, + settings = settings, realmConfiguration = monarchy.realmConfiguration, - taskExecutor = taskExecutor, - contextOfEventTask = contextOfEventTask, + coroutineDispatchers = coroutineDispatchers, paginationTask = paginationTask, timelineEventMapper = timelineEventMapper, - settings = settings, timelineInput = timelineInput, eventDecryptor = eventDecryptor, fetchTokenAndPaginateTask = fetchTokenAndPaginateTask, - realmSessionProvider = realmSessionProvider, loadRoomMembersTask = loadRoomMembersTask, - threadsAwarenessHandler = threadsAwarenessHandler, - readReceiptHandler = readReceiptHandler + readReceiptHandler = readReceiptHandler, + getEventTask = contextOfEventTask, + threadsAwarenessHandler = threadsAwarenessHandler ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadMoreResult.kt similarity index 76% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineState.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadMoreResult.kt index 0143d9bab3d..c419e8325ee 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineState.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadMoreResult.kt @@ -16,9 +16,8 @@ package org.matrix.android.sdk.internal.session.room.timeline -internal data class TimelineState( - val hasReachedEnd: Boolean = false, - val hasMoreInCache: Boolean = true, - val isPaginating: Boolean = false, - val requestedPaginationCount: Int = 0 -) +internal enum class LoadMoreResult { + REACHED_END, + SUCCESS, + FAILURE +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt new file mode 100644 index 00000000000..528b564e8b1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt @@ -0,0 +1,232 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +import io.realm.OrderedCollectionChangeSet +import io.realm.OrderedRealmCollectionChangeListener +import io.realm.Realm +import io.realm.RealmResults +import kotlinx.coroutines.CompletableDeferred +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper +import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.model.ChunkEntityFields +import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler +import java.util.concurrent.atomic.AtomicReference + +/** + * This class is responsible for keeping an instance of chunkEntity and timelineChunk according to the strategy. + * There is 2 different mode: Live and Permalink. + * In Live, we will query for the live chunk (isLastForward = true). + * In Permalink, we will query for the chunk including the eventId we are looking for. + * Once we got a ChunkEntity we wrap it with TimelineChunk class so we dispatch any methods for loading data. + */ + +internal class LoadTimelineStrategy( + private val roomId: String, + private val timelineId: String, + private val mode: Mode, + private val dependencies: Dependencies) { + + sealed interface Mode { + object Live : Mode + data class Permalink(val originEventId: String) : Mode + + fun originEventId(): String? { + return if (this is Permalink) { + originEventId + } else { + null + } + } + } + + data class Dependencies( + val timelineSettings: TimelineSettings, + val realm: AtomicReference, + val eventDecryptor: TimelineEventDecryptor, + val paginationTask: PaginationTask, + val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, + val getContextOfEventTask: GetContextOfEventTask, + val timelineInput: TimelineInput, + val timelineEventMapper: TimelineEventMapper, + val threadsAwarenessHandler: ThreadsAwarenessHandler, + val onEventsUpdated: (Boolean) -> Unit, + val onLimitedTimeline: () -> Unit, + val onNewTimelineEvents: (List) -> Unit + ) + + private var getContextLatch: CompletableDeferred? = null + private var chunkEntity: RealmResults? = null + private var timelineChunk: TimelineChunk? = null + + private val chunkEntityListener = OrderedRealmCollectionChangeListener { _: RealmResults, changeSet: OrderedCollectionChangeSet -> + // Can be call either when you open a permalink on an unknown event + // or when there is a gap in the timeline. + val shouldRebuildChunk = changeSet.insertions.isNotEmpty() + if (shouldRebuildChunk) { + timelineChunk?.close(closeNext = true, closePrev = true) + timelineChunk = chunkEntity?.createTimelineChunk() + // If we are waiting for a result of get context, post completion + getContextLatch?.complete(Unit) + // If we have a gap, just tell the timeline about it. + if (timelineChunk?.hasReachedLastForward().orFalse()) { + dependencies.onLimitedTimeline() + } + } + } + + private val uiEchoManagerListener = object : UIEchoManager.Listener { + override fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?): Boolean { + return timelineChunk?.rebuildEvent(eventId, builder, searchInNext = true, searchInPrev = true).orFalse() + } + } + + private val timelineInputListener = object : TimelineInput.Listener { + + override fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) { + if (roomId != this@LoadTimelineStrategy.roomId) { + return + } + if (uiEchoManager.onLocalEchoCreated(timelineEvent)) { + dependencies.onNewTimelineEvents(listOf(timelineEvent.eventId)) + dependencies.onEventsUpdated(false) + } + } + + override fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState) { + if (roomId != this@LoadTimelineStrategy.roomId) { + return + } + if (uiEchoManager.onSendStateUpdated(eventId, sendState)) { + dependencies.onEventsUpdated(false) + } + } + + override fun onNewTimelineEvents(roomId: String, eventIds: List) { + if (roomId == this@LoadTimelineStrategy.roomId && hasReachedLastForward()) { + dependencies.onNewTimelineEvents(eventIds) + } + } + } + + private val uiEchoManager = UIEchoManager(uiEchoManagerListener) + private val sendingEventsDataSource: SendingEventsDataSource = RealmSendingEventsDataSource( + roomId = roomId, + realm = dependencies.realm, + uiEchoManager = uiEchoManager, + timelineEventMapper = dependencies.timelineEventMapper, + onEventsUpdated = dependencies.onEventsUpdated + ) + + fun onStart() { + dependencies.eventDecryptor.start() + dependencies.timelineInput.listeners.add(timelineInputListener) + val realm = dependencies.realm.get() + sendingEventsDataSource.start() + chunkEntity = getChunkEntity(realm).also { + it.addChangeListener(chunkEntityListener) + timelineChunk = it.createTimelineChunk() + } + } + + fun onStop() { + dependencies.eventDecryptor.destroy() + dependencies.timelineInput.listeners.remove(timelineInputListener) + chunkEntity?.removeChangeListener(chunkEntityListener) + sendingEventsDataSource.stop() + timelineChunk?.close(closeNext = true, closePrev = true) + getContextLatch?.cancel() + chunkEntity = null + timelineChunk = null + } + + suspend fun loadMore(count: Int, direction: Timeline.Direction, fetchOnServerIfNeeded: Boolean = true): LoadMoreResult { + if (mode is Mode.Permalink && timelineChunk == null) { + val params = GetContextOfEventTask.Params(roomId, mode.originEventId) + try { + getContextLatch = CompletableDeferred() + dependencies.getContextOfEventTask.execute(params) + // waits for the query to be fulfilled + getContextLatch?.await() + getContextLatch = null + } catch (failure: Throwable) { + return LoadMoreResult.FAILURE + } + } + return timelineChunk?.loadMore(count, direction, fetchOnServerIfNeeded) ?: LoadMoreResult.FAILURE + } + + fun getBuiltEventIndex(eventId: String): Int? { + return timelineChunk?.getBuiltEventIndex(eventId, searchInNext = true, searchInPrev = true) + } + + fun getBuiltEvent(eventId: String): TimelineEvent? { + return timelineChunk?.getBuiltEvent(eventId, searchInNext = true, searchInPrev = true) + } + + fun buildSnapshot(): List { + return buildSendingEvents() + timelineChunk?.builtItems(includesNext = true, includesPrev = true).orEmpty() + } + + private fun buildSendingEvents(): List { + return if (hasReachedLastForward()) { + sendingEventsDataSource.buildSendingEvents() + } else { + emptyList() + } + } + + private fun getChunkEntity(realm: Realm): RealmResults { + return if (mode is Mode.Permalink) { + ChunkEntity.findAllIncludingEvents(realm, listOf(mode.originEventId)) + } else { + ChunkEntity.where(realm, roomId) + .equalTo(ChunkEntityFields.IS_LAST_FORWARD, true) + .findAll() + } + } + + private fun hasReachedLastForward(): Boolean { + return timelineChunk?.hasReachedLastForward().orFalse() + } + + private fun RealmResults.createTimelineChunk(): TimelineChunk? { + return firstOrNull()?.let { + return TimelineChunk( + chunkEntity = it, + timelineSettings = dependencies.timelineSettings, + roomId = roomId, + timelineId = timelineId, + eventDecryptor = dependencies.eventDecryptor, + paginationTask = dependencies.paginationTask, + fetchTokenAndPaginateTask = dependencies.fetchTokenAndPaginateTask, + timelineEventMapper = dependencies.timelineEventMapper, + uiEchoManager = uiEchoManager, + threadsAwarenessHandler = dependencies.threadsAwarenessHandler, + initialEventId = mode.originEventId(), + onBuiltEvents = dependencies.onEventsUpdated + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt new file mode 100644 index 00000000000..a98de1c595a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +import io.realm.Realm +import io.realm.RealmChangeListener +import io.realm.RealmList +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.query.where +import java.util.concurrent.atomic.AtomicReference + +internal interface SendingEventsDataSource { + fun start() + fun stop() + fun buildSendingEvents(): List +} + +internal class RealmSendingEventsDataSource( + private val roomId: String, + private val realm: AtomicReference, + private val uiEchoManager: UIEchoManager, + private val timelineEventMapper: TimelineEventMapper, + private val onEventsUpdated: (Boolean) -> Unit +) : SendingEventsDataSource { + + private var roomEntity: RoomEntity? = null + private var sendingTimelineEvents: RealmList? = null + private var frozenSendingTimelineEvents: RealmList? = null + + private val sendingTimelineEventsListener = RealmChangeListener> { events -> + uiEchoManager.onSentEventsInDatabase(events.map { it.eventId }) + frozenSendingTimelineEvents = sendingTimelineEvents?.freeze() + onEventsUpdated(false) + } + + override fun start() { + val safeRealm = realm.get() + roomEntity = RoomEntity.where(safeRealm, roomId = roomId).findFirst() + sendingTimelineEvents = roomEntity?.sendingTimelineEvents + sendingTimelineEvents?.addChangeListener(sendingTimelineEventsListener) + } + + override fun stop() { + sendingTimelineEvents?.removeChangeListener(sendingTimelineEventsListener) + sendingTimelineEvents = null + roomEntity = null + } + + override fun buildSendingEvents(): List { + val builtSendingEvents = mutableListOf() + uiEchoManager.getInMemorySendingEvents() + .addWithUiEcho(builtSendingEvents) + frozenSendingTimelineEvents + ?.filter { timelineEvent -> + builtSendingEvents.none { it.eventId == timelineEvent.eventId } + } + ?.map { + timelineEventMapper.map(it) + }?.addWithUiEcho(builtSendingEvents) + + return builtSendingEvents + } + + private fun List.addWithUiEcho(target: MutableList) { + target.addAll( + map { uiEchoManager.updateSentStateWithUiEcho(it) } + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt new file mode 100644 index 00000000000..14cba2a4b8d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt @@ -0,0 +1,479 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +import io.realm.OrderedCollectionChangeSet +import io.realm.OrderedRealmCollectionChangeListener +import io.realm.RealmObjectChangeListener +import io.realm.RealmQuery +import io.realm.RealmResults +import io.realm.Sort +import kotlinx.coroutines.CompletableDeferred +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.internal.database.mapper.EventMapper +import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper +import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.model.ChunkEntityFields +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields +import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler +import timber.log.Timber +import java.util.Collections +import java.util.concurrent.atomic.AtomicBoolean + +/** + * This is a wrapper around a ChunkEntity in the database. + * It does mainly listen to the db timeline events. + * It also triggers pagination to the server when needed, or dispatch to the prev or next chunk if any. + */ +internal class TimelineChunk(private val chunkEntity: ChunkEntity, + private val timelineSettings: TimelineSettings, + private val roomId: String, + private val timelineId: String, + private val eventDecryptor: TimelineEventDecryptor, + private val paginationTask: PaginationTask, + private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, + private val timelineEventMapper: TimelineEventMapper, + private val uiEchoManager: UIEchoManager? = null, + private val threadsAwarenessHandler: ThreadsAwarenessHandler, + private val initialEventId: String?, + private val onBuiltEvents: (Boolean) -> Unit) { + + private val isLastForward = AtomicBoolean(chunkEntity.isLastForward) + private val isLastBackward = AtomicBoolean(chunkEntity.isLastBackward) + private var prevChunkLatch: CompletableDeferred? = null + private var nextChunkLatch: CompletableDeferred? = null + + private val chunkObjectListener = RealmObjectChangeListener { _, changeSet -> + if (changeSet == null) return@RealmObjectChangeListener + if (changeSet.isDeleted.orFalse()) { + return@RealmObjectChangeListener + } + Timber.v("on chunk (${chunkEntity.identifier()}) changed: ${changeSet.changedFields?.joinToString(",")}") + if (changeSet.isFieldChanged(ChunkEntityFields.IS_LAST_FORWARD)) { + isLastForward.set(chunkEntity.isLastForward) + } + if (changeSet.isFieldChanged(ChunkEntityFields.IS_LAST_BACKWARD)) { + isLastBackward.set(chunkEntity.isLastBackward) + } + if (changeSet.isFieldChanged(ChunkEntityFields.NEXT_CHUNK.`$`)) { + nextChunk = createTimelineChunk(chunkEntity.nextChunk) + nextChunkLatch?.complete(Unit) + } + if (changeSet.isFieldChanged(ChunkEntityFields.PREV_CHUNK.`$`)) { + prevChunk = createTimelineChunk(chunkEntity.prevChunk) + prevChunkLatch?.complete(Unit) + } + } + + private val timelineEventsChangeListener = + OrderedRealmCollectionChangeListener { results: RealmResults, changeSet: OrderedCollectionChangeSet -> + Timber.v("on timeline events chunk update") + val frozenResults = results.freeze() + handleDatabaseChangeSet(frozenResults, changeSet) + } + + private var timelineEventEntities: RealmResults = chunkEntity.sortedTimelineEvents() + private val builtEvents: MutableList = Collections.synchronizedList(ArrayList()) + private val builtEventsIndexes: MutableMap = Collections.synchronizedMap(HashMap()) + + private var nextChunk: TimelineChunk? = null + private var prevChunk: TimelineChunk? = null + + init { + timelineEventEntities.addChangeListener(timelineEventsChangeListener) + chunkEntity.addChangeListener(chunkObjectListener) + } + + fun hasReachedLastForward(): Boolean { + return if (isLastForward.get()) { + true + } else { + nextChunk?.hasReachedLastForward().orFalse() + } + } + + fun builtItems(includesNext: Boolean, includesPrev: Boolean): List { + val deepBuiltItems = ArrayList(builtEvents.size) + if (includesNext) { + val nextEvents = nextChunk?.builtItems(includesNext = true, includesPrev = false).orEmpty() + deepBuiltItems.addAll(nextEvents) + } + deepBuiltItems.addAll(builtEvents) + if (includesPrev) { + val prevEvents = prevChunk?.builtItems(includesNext = false, includesPrev = true).orEmpty() + deepBuiltItems.addAll(prevEvents) + } + return deepBuiltItems + } + + /** + * This will take care of loading and building events of this chunk for the given direction and count. + * If @param fetchFromServerIfNeeded is true, it will try to fetch more events on server to get the right amount of data. + * This method will also post a snapshot as soon the data is built from db to avoid waiting for server response. + */ + suspend fun loadMore(count: Int, direction: Timeline.Direction, fetchOnServerIfNeeded: Boolean = true): LoadMoreResult { + if (direction == Timeline.Direction.FORWARDS && nextChunk != null) { + return nextChunk?.loadMore(count, direction, fetchOnServerIfNeeded) ?: LoadMoreResult.FAILURE + } else if (direction == Timeline.Direction.BACKWARDS && prevChunk != null) { + return prevChunk?.loadMore(count, direction, fetchOnServerIfNeeded) ?: LoadMoreResult.FAILURE + } + val loadFromStorageCount = loadFromStorage(count, direction) + Timber.v("Has loaded $loadFromStorageCount items from storage in $direction") + val offsetCount = count - loadFromStorageCount + return if (direction == Timeline.Direction.FORWARDS && isLastForward.get()) { + LoadMoreResult.REACHED_END + } else if (direction == Timeline.Direction.BACKWARDS && isLastBackward.get()) { + LoadMoreResult.REACHED_END + } else if (offsetCount == 0) { + LoadMoreResult.SUCCESS + } else { + delegateLoadMore(fetchOnServerIfNeeded, offsetCount, direction) + } + } + + private suspend fun delegateLoadMore(fetchFromServerIfNeeded: Boolean, offsetCount: Int, direction: Timeline.Direction): LoadMoreResult { + return if (direction == Timeline.Direction.FORWARDS) { + val nextChunkEntity = chunkEntity.nextChunk + when { + nextChunkEntity != null -> { + if (nextChunk == null) { + nextChunk = createTimelineChunk(nextChunkEntity) + } + nextChunk?.loadMore(offsetCount, direction, fetchFromServerIfNeeded) ?: LoadMoreResult.FAILURE + } + fetchFromServerIfNeeded -> { + fetchFromServer(offsetCount, chunkEntity.nextToken, direction) + } + else -> { + LoadMoreResult.SUCCESS + } + } + } else { + val prevChunkEntity = chunkEntity.prevChunk + when { + prevChunkEntity != null -> { + if (prevChunk == null) { + prevChunk = createTimelineChunk(prevChunkEntity) + } + prevChunk?.loadMore(offsetCount, direction, fetchFromServerIfNeeded) ?: LoadMoreResult.FAILURE + } + fetchFromServerIfNeeded -> { + fetchFromServer(offsetCount, chunkEntity.prevToken, direction) + } + else -> { + LoadMoreResult.SUCCESS + } + } + } + } + + fun getBuiltEventIndex(eventId: String, searchInNext: Boolean, searchInPrev: Boolean): Int? { + val builtEventIndex = builtEventsIndexes[eventId] + if (builtEventIndex != null) { + return getOffsetIndex() + builtEventIndex + } + if (searchInNext) { + val nextBuiltEventIndex = nextChunk?.getBuiltEventIndex(eventId, searchInNext = true, searchInPrev = false) + if (nextBuiltEventIndex != null) { + return nextBuiltEventIndex + } + } + if (searchInPrev) { + val prevBuiltEventIndex = prevChunk?.getBuiltEventIndex(eventId, searchInNext = false, searchInPrev = true) + if (prevBuiltEventIndex != null) { + return prevBuiltEventIndex + } + } + return null + } + + fun getBuiltEvent(eventId: String, searchInNext: Boolean, searchInPrev: Boolean): TimelineEvent? { + val builtEventIndex = builtEventsIndexes[eventId] + if (builtEventIndex != null) { + return builtEvents.getOrNull(builtEventIndex) + } + if (searchInNext) { + val nextBuiltEvent = nextChunk?.getBuiltEvent(eventId, searchInNext = true, searchInPrev = false) + if (nextBuiltEvent != null) { + return nextBuiltEvent + } + } + if (searchInPrev) { + val prevBuiltEvent = prevChunk?.getBuiltEvent(eventId, searchInNext = false, searchInPrev = true) + if (prevBuiltEvent != null) { + return prevBuiltEvent + } + } + return null + } + + fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?, searchInNext: Boolean, searchInPrev: Boolean): Boolean { + return tryOrNull { + val builtIndex = getBuiltEventIndex(eventId, searchInNext = false, searchInPrev = false) + if (builtIndex == null) { + val foundInPrev = searchInPrev && prevChunk?.rebuildEvent(eventId, builder, searchInNext = false, searchInPrev = true).orFalse() + if (foundInPrev) { + return true + } + if (searchInNext) { + return prevChunk?.rebuildEvent(eventId, builder, searchInPrev = false, searchInNext = true).orFalse() + } + return false + } + // Update the relation of existing event + builtEvents.getOrNull(builtIndex)?.let { te -> + val rebuiltEvent = builder(te) + builtEvents[builtIndex] = rebuiltEvent!! + true + } + } + ?: false + } + + fun close(closeNext: Boolean, closePrev: Boolean) { + if (closeNext) { + nextChunk?.close(closeNext = true, closePrev = false) + } + if (closePrev) { + prevChunk?.close(closeNext = false, closePrev = true) + } + nextChunk = null + nextChunkLatch?.cancel() + prevChunk = null + prevChunkLatch?.cancel() + chunkEntity.removeChangeListener(chunkObjectListener) + timelineEventEntities.removeChangeListener(timelineEventsChangeListener) + } + + /** + * This method tries to read events from the current chunk. + */ + private suspend fun loadFromStorage(count: Int, direction: Timeline.Direction): Int { + val displayIndex = getNextDisplayIndex(direction) ?: return 0 + val baseQuery = timelineEventEntities.where() + val timelineEvents = baseQuery.offsets(direction, count, displayIndex).findAll().orEmpty() + if (timelineEvents.isEmpty()) return 0 + fetchRootThreadEventsIfNeeded(timelineEvents) + if (direction == Timeline.Direction.FORWARDS) { + builtEventsIndexes.entries.forEach { it.setValue(it.value + timelineEvents.size) } + } + timelineEvents + .mapIndexed { index, timelineEventEntity -> + val timelineEvent = timelineEventEntity.buildAndDecryptIfNeeded() + if (timelineEvent.root.type == EventType.STATE_ROOM_CREATE) { + isLastBackward.set(true) + } + if (direction == Timeline.Direction.FORWARDS) { + builtEventsIndexes[timelineEvent.eventId] = index + builtEvents.add(index, timelineEvent) + } else { + builtEventsIndexes[timelineEvent.eventId] = builtEvents.size + builtEvents.add(timelineEvent) + } + } + return timelineEvents.size + } + + /** + * This function is responsible to fetch and store the root event of a thread event + * in order to be able to display the event to the user appropriately + */ + private suspend fun fetchRootThreadEventsIfNeeded(offsetResults: List) { + val eventEntityList = offsetResults + .mapNotNull { + it.root + }.map { + EventMapper.map(it) + } + threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(eventEntityList) + } + + private fun TimelineEventEntity.buildAndDecryptIfNeeded(): TimelineEvent { + val timelineEvent = buildTimelineEvent(this) + val transactionId = timelineEvent.root.unsignedData?.transactionId + uiEchoManager?.onSyncedEvent(transactionId) + if (timelineEvent.isEncrypted() && + timelineEvent.root.mxDecryptionResult == null) { + timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineId)) } + } + return timelineEvent + } + + private fun buildTimelineEvent(eventEntity: TimelineEventEntity) = timelineEventMapper.map( + timelineEventEntity = eventEntity, + buildReadReceipts = timelineSettings.buildReadReceipts + ).let { + // eventually enhance with ui echo? + (uiEchoManager?.decorateEventWithReactionUiEcho(it) ?: it) + } + + /** + * Will try to fetch a new chunk on the home server. + * It will take care to update the database by inserting new events and linking new chunk + * with this one. + */ + private suspend fun fetchFromServer(count: Int, token: String?, direction: Timeline.Direction): LoadMoreResult { + val latch = if (direction == Timeline.Direction.FORWARDS) { + nextChunkLatch = CompletableDeferred() + nextChunkLatch + } else { + prevChunkLatch = CompletableDeferred() + prevChunkLatch + } + val loadMoreResult = try { + if (token == null) { + if (direction == Timeline.Direction.BACKWARDS || !chunkEntity.hasBeenALastForwardChunk()) return LoadMoreResult.REACHED_END + val lastKnownEventId = chunkEntity.sortedTimelineEvents().firstOrNull()?.eventId ?: return LoadMoreResult.FAILURE + val taskParams = FetchTokenAndPaginateTask.Params(roomId, lastKnownEventId, direction.toPaginationDirection(), count) + fetchTokenAndPaginateTask.execute(taskParams).toLoadMoreResult() + } else { + Timber.v("Fetch $count more events on server") + val taskParams = PaginationTask.Params(roomId, token, direction.toPaginationDirection(), count) + paginationTask.execute(taskParams).toLoadMoreResult() + } + } catch (failure: Throwable) { + Timber.e("Failed to fetch from server: $failure", failure) + LoadMoreResult.FAILURE + } + return if (loadMoreResult == LoadMoreResult.SUCCESS) { + latch?.await() + loadMore(count, direction, fetchOnServerIfNeeded = false) + } else { + loadMoreResult + } + } + + private fun TokenChunkEventPersistor.Result.toLoadMoreResult(): LoadMoreResult { + return when (this) { + TokenChunkEventPersistor.Result.REACHED_END -> LoadMoreResult.REACHED_END + TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE, + TokenChunkEventPersistor.Result.SUCCESS -> LoadMoreResult.SUCCESS + } + } + + private fun getOffsetIndex(): Int { + var offset = 0 + var currentNextChunk = nextChunk + while (currentNextChunk != null) { + offset += currentNextChunk.builtEvents.size + currentNextChunk = currentNextChunk.nextChunk + } + return offset + } + + /** + * This method is responsible for managing insertions and updates of events on this chunk. + * + */ + private fun handleDatabaseChangeSet(frozenResults: RealmResults, changeSet: OrderedCollectionChangeSet) { + val insertions = changeSet.insertionRanges + for (range in insertions) { + val newItems = frozenResults + .subList(range.startIndex, range.startIndex + range.length) + .map { it.buildAndDecryptIfNeeded() } + builtEventsIndexes.entries.filter { it.value >= range.startIndex }.forEach { it.setValue(it.value + range.length) } + newItems.mapIndexed { index, timelineEvent -> + if (timelineEvent.root.type == EventType.STATE_ROOM_CREATE) { + isLastBackward.set(true) + } + val correctedIndex = range.startIndex + index + builtEvents.add(correctedIndex, timelineEvent) + builtEventsIndexes[timelineEvent.eventId] = correctedIndex + } + } + val modifications = changeSet.changeRanges + for (range in modifications) { + for (modificationIndex in (range.startIndex until range.startIndex + range.length)) { + val updatedEntity = frozenResults[modificationIndex] ?: continue + try { + builtEvents[modificationIndex] = updatedEntity.buildAndDecryptIfNeeded() + } catch (failure: Throwable) { + Timber.v("Fail to update items at index: $modificationIndex") + } + } + } + if (insertions.isNotEmpty() || modifications.isNotEmpty()) { + onBuiltEvents(true) + } + } + + private fun getNextDisplayIndex(direction: Timeline.Direction): Int? { + val frozenTimelineEvents = timelineEventEntities.freeze() + if (frozenTimelineEvents.isEmpty()) { + return null + } + return if (builtEvents.isEmpty()) { + if (initialEventId != null) { + frozenTimelineEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, initialEventId).findFirst()?.displayIndex + } else if (direction == Timeline.Direction.BACKWARDS) { + frozenTimelineEvents.first()?.displayIndex + } else { + frozenTimelineEvents.last()?.displayIndex + } + } else if (direction == Timeline.Direction.FORWARDS) { + builtEvents.first().displayIndex + 1 + } else { + builtEvents.last().displayIndex - 1 + } + } + + private fun createTimelineChunk(chunkEntity: ChunkEntity?): TimelineChunk? { + if (chunkEntity == null) return null + return TimelineChunk( + chunkEntity = chunkEntity, + timelineSettings = timelineSettings, + roomId = roomId, + timelineId = timelineId, + eventDecryptor = eventDecryptor, + paginationTask = paginationTask, + fetchTokenAndPaginateTask = fetchTokenAndPaginateTask, + timelineEventMapper = timelineEventMapper, + uiEchoManager = uiEchoManager, + threadsAwarenessHandler = threadsAwarenessHandler, + initialEventId = null, + onBuiltEvents = this.onBuiltEvents + ) + } +} + +private fun RealmQuery.offsets( + direction: Timeline.Direction, + count: Int, + startDisplayIndex: Int +): RealmQuery { + sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) + if (direction == Timeline.Direction.BACKWARDS) { + lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex) + } else { + greaterThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex) + } + return limit(count.toLong()) +} + +private fun Timeline.Direction.toPaginationDirection(): PaginationDirection { + return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS +} + +private fun ChunkEntity.sortedTimelineEvents(): RealmResults { + return timelineEvents.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineInput.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineInput.kt index cdc85ea7227..a953db07044 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineInput.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineInput.kt @@ -23,6 +23,9 @@ import javax.inject.Inject @SessionScope internal class TimelineInput @Inject constructor() { + + val listeners = mutableSetOf() + fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) { listeners.toSet().forEach { it.onLocalEchoCreated(roomId, timelineEvent) } } @@ -35,8 +38,6 @@ internal class TimelineInput @Inject constructor() { listeners.toSet().forEach { it.onNewTimelineEvents(roomId, eventIds) } } - val listeners = mutableSetOf() - internal interface Listener { fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) = Unit fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState) = Unit diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt index dbcc37a918a..a85f0dbdc93 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.room.timeline import com.zhuinden.monarchy.Monarchy +import dagger.Lazy import io.realm.Realm import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel @@ -25,93 +26,27 @@ import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.database.helper.addIfNecessary import org.matrix.android.sdk.internal.database.helper.addStateEvent import org.matrix.android.sdk.internal.database.helper.addTimelineEvent -import org.matrix.android.sdk.internal.database.helper.merge import org.matrix.android.sdk.internal.database.mapper.toEntity import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.EventInsertType import org.matrix.android.sdk.internal.database.model.RoomEntity -import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity -import org.matrix.android.sdk.internal.database.model.deleteOnCascade +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore import org.matrix.android.sdk.internal.database.query.create import org.matrix.android.sdk.internal.database.query.find -import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents -import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom -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.SessionDatabase -import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryEventsHelper +import org.matrix.android.sdk.internal.session.StreamEventsManager import org.matrix.android.sdk.internal.util.awaitTransaction import timber.log.Timber import javax.inject.Inject /** - * Insert Chunk in DB, and eventually merge with existing chunk event + * Insert Chunk in DB, and eventually link next and previous chunk in db. */ -internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase private val monarchy: Monarchy) { - - /** - *
-     * ========================================================================================================
-     * | Backward case                                                                                        |
-     * ========================================================================================================
-     *
-     *                               *--------------------------*        *--------------------------*
-     *                               | startToken1              |        | startToken1              |
-     *                               *--------------------------*        *--------------------------*
-     *                               |                          |        |                          |
-     *                               |                          |        |                          |
-     *                               |  receivedChunk backward  |        |                          |
-     *                               |         Events           |        |                          |
-     *                               |                          |        |                          |
-     *                               |                          |        |                          |
-     *                               |                          |        |                          |
-     * *--------------------------*  *--------------------------*        |                          |
-     * | startToken0              |  | endToken1                |   =>   |       Merged chunk       |
-     * *--------------------------*  *--------------------------*        |          Events          |
-     * |                          |                                      |                          |
-     * |                          |                                      |                          |
-     * |      Current Chunk       |                                      |                          |
-     * |         Events           |                                      |                          |
-     * |                          |                                      |                          |
-     * |                          |                                      |                          |
-     * |                          |                                      |                          |
-     * *--------------------------*                                      *--------------------------*
-     * | endToken0                |                                      | endToken0                |
-     * *--------------------------*                                      *--------------------------*
-     *
-     *
-     * ========================================================================================================
-     * | Forward case                                                                                         |
-     * ========================================================================================================
-     *
-     * *--------------------------*                                      *--------------------------*
-     * | startToken0              |                                      | startToken0              |
-     * *--------------------------*                                      *--------------------------*
-     * |                          |                                      |                          |
-     * |                          |                                      |                          |
-     * |      Current Chunk       |                                      |                          |
-     * |         Events           |                                      |                          |
-     * |                          |                                      |                          |
-     * |                          |                                      |                          |
-     * |                          |                                      |                          |
-     * *--------------------------*  *--------------------------*        |                          |
-     * | endToken0                |  | startToken1              |   =>   |       Merged chunk       |
-     * *--------------------------*  *--------------------------*        |          Events          |
-     *                               |                          |        |                          |
-     *                               |                          |        |                          |
-     *                               |  receivedChunk forward   |        |                          |
-     *                               |         Events           |        |                          |
-     *                               |                          |        |                          |
-     *                               |                          |        |                          |
-     *                               |                          |        |                          |
-     *                               *--------------------------*        *--------------------------*
-     *                               | endToken1                |        | endToken1                |
-     *                               *--------------------------*        *--------------------------*
-     *
-     * ========================================================================================================
-     * 
- */ +internal class TokenChunkEventPersistor @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, + private val liveEventManager: Lazy) { enum class Result { SHOULD_FETCH_MORE, @@ -136,21 +71,21 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri prevToken = receivedChunk.end } + val existingChunk = ChunkEntity.find(realm, roomId, prevToken = prevToken, nextToken = nextToken) + if (existingChunk != null) { + Timber.v("This chunk is already in the db, returns") + return@awaitTransaction + } val prevChunk = ChunkEntity.find(realm, roomId, nextToken = prevToken) val nextChunk = ChunkEntity.find(realm, roomId, prevToken = nextToken) - - // The current chunk is the one we will keep all along the merge processChanges. - // We try to look for a chunk next to the token, - // otherwise we create a whole new one which is unlinked (not live) - val currentChunk = if (direction == PaginationDirection.FORWARDS) { - prevChunk?.apply { this.nextToken = nextToken } - } else { - nextChunk?.apply { this.prevToken = prevToken } + val currentChunk = ChunkEntity.create(realm, prevToken = prevToken, nextToken = nextToken).apply { + this.nextChunk = nextChunk + this.prevChunk = prevChunk } - ?: ChunkEntity.create(realm, prevToken, nextToken) - - if (receivedChunk.events.isNullOrEmpty() && !receivedChunk.hasMore()) { - handleReachEnd(realm, roomId, direction, currentChunk) + nextChunk?.prevChunk = currentChunk + prevChunk?.nextChunk = currentChunk + if (receivedChunk.events.isEmpty() && !receivedChunk.hasMore()) { + handleReachEnd(roomId, direction, currentChunk) } else { handlePagination(realm, roomId, direction, receivedChunk, currentChunk) } @@ -166,17 +101,10 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri } } - private fun handleReachEnd(realm: Realm, roomId: String, direction: PaginationDirection, currentChunk: ChunkEntity) { + private fun handleReachEnd(roomId: String, direction: PaginationDirection, currentChunk: ChunkEntity) { Timber.v("Reach end of $roomId") if (direction == PaginationDirection.FORWARDS) { - val currentLastForwardChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) - if (currentChunk != currentLastForwardChunk) { - currentChunk.isLastForward = true - currentLastForwardChunk?.deleteOnCascade(deleteStateEvents = false, canDeleteRoot = false) - RoomSummaryEntity.where(realm, roomId).findFirst()?.apply { - latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) - } - } + Timber.v("We should keep the lastForward chunk unique, the one from sync") } else { currentChunk.isLastBackward = true } @@ -204,45 +132,52 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri roomMemberContentsByUser[stateEvent.stateKey] = stateEvent.content.toModel() } } - val eventIds = ArrayList(eventList.size) - eventList.forEach { event -> - if (event.eventId == null || event.senderId == null) { - return@forEach - } - val ageLocalTs = event.unsignedData?.age?.let { now - it } - eventIds.add(event.eventId) - val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) - if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null) { - val contentToUse = if (direction == PaginationDirection.BACKWARDS) { - event.prevContent - } else { - event.content + run processTimelineEvents@{ + eventList.forEach { event -> + if (event.eventId == null || event.senderId == null) { + return@forEach } - roomMemberContentsByUser[event.stateKey] = contentToUse.toModel() - } - - currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser) - } - // Find all the chunks which contain at least one event from the list of eventIds - val chunks = ChunkEntity.findAllIncludingEvents(realm, eventIds) - Timber.d("Found ${chunks.size} chunks containing at least one of the eventIds") - val chunksToDelete = ArrayList() - chunks.forEach { - if (it != currentChunk) { - Timber.d("Merge $it") - currentChunk.merge(roomId, it, direction) - chunksToDelete.add(it) + // We check for the timeline event with this id + val eventId = event.eventId + val existingTimelineEvent = TimelineEventEntity.where(realm, roomId, eventId).findFirst() + // If it exists, we want to stop here, just link the prevChunk + val existingChunk = existingTimelineEvent?.chunk?.firstOrNull() + if (existingChunk != null) { + when (direction) { + PaginationDirection.BACKWARDS -> { + if (currentChunk.nextChunk == existingChunk) { + Timber.w("Avoid double link, shouldn't happen in an ideal world") + } else { + currentChunk.prevChunk = existingChunk + existingChunk.nextChunk = currentChunk + } + } + PaginationDirection.FORWARDS -> { + if (currentChunk.prevChunk == existingChunk) { + Timber.w("Avoid double link, shouldn't happen in an ideal world") + } else { + currentChunk.nextChunk = existingChunk + existingChunk.prevChunk = currentChunk + } + } + } + // Stop processing here + return@processTimelineEvents + } + val ageLocalTs = event.unsignedData?.age?.let { now - it } + val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) + if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null) { + val contentToUse = if (direction == PaginationDirection.BACKWARDS) { + event.prevContent + } else { + event.content + } + roomMemberContentsByUser[event.stateKey] = contentToUse.toModel() + } + liveEventManager.get().dispatchPaginatedEventReceived(event, roomId) + currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser) } } - chunksToDelete.forEach { - it.deleteOnCascade(deleteStateEvents = false, canDeleteRoot = false) - } - val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId) - val shouldUpdateSummary = roomSummaryEntity.latestPreviewableEvent == null || - (chunksToDelete.isNotEmpty() && currentChunk.isLastForward && direction == PaginationDirection.FORWARDS) - if (shouldUpdateSummary) { - roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) - } if (currentChunk.isValid) { RoomEntity.where(realm, roomId).findFirst()?.addIfNecessary(currentChunk) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt index 4804fbd7314..16d36c0cd92 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt @@ -24,14 +24,10 @@ import org.matrix.android.sdk.api.session.room.model.ReactionAggregatedSummary import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent 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.session.room.timeline.TimelineSettings import timber.log.Timber import java.util.Collections -internal class UIEchoManager( - private val settings: TimelineSettings, - private val listener: Listener -) { +internal class UIEchoManager(private val listener: Listener) { interface Listener { fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?): Boolean @@ -70,13 +66,12 @@ internal class UIEchoManager( return existingState != sendState } - fun onLocalEchoCreated(timelineEvent: TimelineEvent) { - // Manage some ui echos (do it before filter because actual event could be filtered out) + fun onLocalEchoCreated(timelineEvent: TimelineEvent): Boolean { when (timelineEvent.root.getClearType()) { EventType.REDACTION -> { } EventType.REACTION -> { - val content = timelineEvent.root.content?.toModel() + val content: ReactionContent? = timelineEvent.root.content?.toModel() if (RelationType.ANNOTATION == content?.relatesTo?.type) { val reaction = content.relatesTo.key val relatedEventID = content.relatesTo.eventId @@ -96,11 +91,12 @@ internal class UIEchoManager( } Timber.v("On local echo created: ${timelineEvent.eventId}") inMemorySendingEvents.add(0, timelineEvent) + return true } - fun decorateEventWithReactionUiEcho(timelineEvent: TimelineEvent): TimelineEvent? { + fun decorateEventWithReactionUiEcho(timelineEvent: TimelineEvent): TimelineEvent { val relatedEventID = timelineEvent.eventId - val contents = inMemoryReactions[relatedEventID] ?: return null + val contents = inMemoryReactions[relatedEventID] ?: return timelineEvent var existingAnnotationSummary = timelineEvent.annotations ?: EventAnnotationsSummary( relatedEventID diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt index 1a7e15e14c4..24722445be4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.session.sync.handler.room +import dagger.Lazy import io.realm.Realm import io.realm.kotlin.createObject import org.matrix.android.sdk.api.session.crypto.MXCryptoError @@ -52,6 +53,7 @@ import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.extensions.clearWith +import org.matrix.android.sdk.internal.session.StreamEventsManager import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent import org.matrix.android.sdk.internal.session.initsync.ProgressReporter import org.matrix.android.sdk.internal.session.initsync.mapWithProgress @@ -79,7 +81,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle private val threadsAwarenessHandler: ThreadsAwarenessHandler, private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, @UserId private val userId: String, - private val timelineInput: TimelineInput) { + private val timelineInput: TimelineInput, + private val liveEventService: Lazy) { sealed class HandlingStrategy { data class JOINED(val data: Map) : HandlingStrategy() @@ -218,6 +221,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle } val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType) + Timber.v("## received state event ${event.type} and key ${event.stateKey}") CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { // Timber.v("## Space state event: $eventEntity") eventId = event.eventId @@ -345,15 +349,17 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle syncLocalTimestampMillis: Long, aggregator: SyncResponsePostTreatmentAggregator): ChunkEntity { val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId) + if (isLimited && lastChunk != null) { + lastChunk.deleteOnCascade(deleteStateEvents = true, canDeleteRoot = true) + } val chunkEntity = if (!isLimited && lastChunk != null) { lastChunk } else { - realm.createObject().apply { this.prevToken = prevToken } + realm.createObject().apply { + this.prevToken = prevToken + this.isLastForward = true + } } - // Only one chunk has isLastForward set to true - lastChunk?.isLastForward = false - chunkEntity.isLastForward = true - val eventIds = ArrayList(eventList.size) val roomMemberContentsByUser = HashMap() @@ -362,6 +368,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle continue } eventIds.add(event.eventId) + liveEventService.get().dispatchLiveEventReceived(event, roomId, insertType == EventInsertType.INITIAL_SYNC) val isInitialSync = insertType == EventInsertType.INITIAL_SYNC @@ -387,6 +394,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle roomMemberEventHandler.handle(realm, roomEntity.roomId, event.stateKey, fixedContent, aggregator) } } + roomMemberContentsByUser.getOrPut(event.senderId) { // If we don't have any new state on this user, get it from db val rootStateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt index 3faa0c9488a..b6ea7a68f76 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt @@ -30,6 +30,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.isTokenError import org.matrix.android.sdk.api.logger.LoggerTag @@ -71,6 +72,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, private var isStarted = false private var isTokenValid = true private var retryNoNetworkTask: TimerTask? = null + private var previousSyncResponseHasToDevice = false private val activeCallListObserver = Observer> { activeCalls -> if (activeCalls.isEmpty() && backgroundDetectionObserver.isInBackground) { @@ -171,12 +173,15 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, if (state !is SyncState.Running) { updateStateTo(SyncState.Running(afterPause = true)) } - // No timeout after a pause - val timeout = state.let { if (it is SyncState.Running && it.afterPause) 0 else DEFAULT_LONG_POOL_TIMEOUT } + val timeout = when { + previousSyncResponseHasToDevice -> 0L /* Force timeout to 0 */ + state.let { it is SyncState.Running && it.afterPause } -> 0L /* No timeout after a pause */ + else -> DEFAULT_LONG_POOL_TIMEOUT + } Timber.tag(loggerTag.value).d("Execute sync request with timeout $timeout") val params = SyncTask.Params(timeout, SyncPresence.Online) val sync = syncScope.launch { - doSync(params) + previousSyncResponseHasToDevice = doSync(params) } runBlocking { sync.join() @@ -203,10 +208,14 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, } } - private suspend fun doSync(params: SyncTask.Params) { - try { + /** + * Will return true if the sync response contains some toDevice events. + */ + private suspend fun doSync(params: SyncTask.Params): Boolean { + return try { val syncResponse = syncTask.execute(params) _syncFlow.emit(syncResponse) + syncResponse.toDevice?.events?.isNotEmpty().orFalse() } catch (failure: Throwable) { if (failure is Failure.NetworkConnection) { canReachServer = false @@ -229,6 +238,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, delay(RETRY_WAIT_TIME_MS) } } + false } finally { state.let { if (it is SyncState.Running && it.afterPause) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt index 763cd55714b..2f1241f4d8f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt @@ -20,6 +20,7 @@ import androidx.work.BackoffPolicy import androidx.work.ExistingWorkPolicy import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.failure.isTokenError import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.di.WorkManagerProvider @@ -34,8 +35,8 @@ import timber.log.Timber import java.util.concurrent.TimeUnit import javax.inject.Inject -private const val DEFAULT_LONG_POOL_TIMEOUT = 6L -private const val DEFAULT_DELAY_TIMEOUT = 30_000L +private const val DEFAULT_LONG_POOL_TIMEOUT_SECONDS = 6L +private const val DEFAULT_DELAY_MILLIS = 30_000L /** * Possible previous worker: None @@ -47,9 +48,12 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, @JsonClass(generateAdapter = true) internal data class Params( override val sessionId: String, - val timeout: Long = DEFAULT_LONG_POOL_TIMEOUT, - val delay: Long = DEFAULT_DELAY_TIMEOUT, + // In seconds + val timeout: Long = DEFAULT_LONG_POOL_TIMEOUT_SECONDS, + // In milliseconds + val delay: Long = DEFAULT_DELAY_MILLIS, val periodic: Boolean = false, + val forceImmediate: Boolean = false, override val lastFailureMessage: String? = null ) : SessionWorkerParams @@ -65,13 +69,26 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, Timber.i("Sync work starting") return runCatching { - doSync(params.timeout) + doSync(if (params.forceImmediate) 0 else params.timeout) }.fold( - { + { hasToDeviceEvents -> Result.success().also { if (params.periodic) { - // we want to schedule another one after delay - automaticallyBackgroundSync(workManagerProvider, params.sessionId, params.timeout, params.delay) + // we want to schedule another one after a delay, or immediately if hasToDeviceEvents + automaticallyBackgroundSync( + workManagerProvider = workManagerProvider, + sessionId = params.sessionId, + serverTimeoutInSeconds = params.timeout, + delayInSeconds = params.delay, + forceImmediate = hasToDeviceEvents + ) + } else if (hasToDeviceEvents) { + // Previous response has toDevice events, request an immediate sync request + requireBackgroundSync( + workManagerProvider = workManagerProvider, + sessionId = params.sessionId, + serverTimeoutInSeconds = 0 + ) } } }, @@ -92,16 +109,29 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) } - private suspend fun doSync(timeout: Long) { + /** + * Will return true if the sync response contains some toDevice events. + */ + private suspend fun doSync(timeout: Long): Boolean { val taskParams = SyncTask.Params(timeout * 1000, SyncPresence.Offline) - syncTask.execute(taskParams) + val syncResponse = syncTask.execute(taskParams) + return syncResponse.toDevice?.events?.isNotEmpty().orFalse() } companion object { private const val BG_SYNC_WORK_NAME = "BG_SYNCP" - fun requireBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0) { - val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, 0L, false)) + fun requireBackgroundSync(workManagerProvider: WorkManagerProvider, + sessionId: String, + serverTimeoutInSeconds: Long = 0) { + val data = WorkerParamsFactory.toData( + Params( + sessionId = sessionId, + timeout = serverTimeoutInSeconds, + delay = 0L, + periodic = false + ) + ) val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() .setConstraints(WorkManagerProvider.workConstraints) .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) @@ -111,13 +141,24 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, .enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest) } - fun automaticallyBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0, delayInSeconds: Long = 30) { - val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, delayInSeconds, true)) + fun automaticallyBackgroundSync(workManagerProvider: WorkManagerProvider, + sessionId: String, + serverTimeoutInSeconds: Long = 0, + delayInSeconds: Long = 30, + forceImmediate: Boolean = false) { + val data = WorkerParamsFactory.toData( + Params( + sessionId = sessionId, + timeout = serverTimeoutInSeconds, + delay = delayInSeconds, + forceImmediate = forceImmediate + ) + ) val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() .setConstraints(WorkManagerProvider.workConstraints) .setInputData(data) .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) - .setInitialDelay(delayInSeconds, TimeUnit.SECONDS) + .setInitialDelay(if (forceImmediate) 0 else delayInSeconds, TimeUnit.SECONDS) .build() // Avoid risking multiple chains of syncs by replacing the existing chain workManagerProvider.workManager diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt index c52c6a404ed..313fb6319d5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt @@ -36,6 +36,7 @@ import org.matrix.android.sdk.internal.session.sync.model.accountdata.AcceptedTe import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask import org.matrix.android.sdk.internal.session.user.accountdata.UserAccountDataDataSource import org.matrix.android.sdk.internal.util.ensureTrailingSlash +import timber.log.Timber import javax.inject.Inject internal class DefaultTermsService @Inject constructor( @@ -63,19 +64,28 @@ internal class DefaultTermsService @Inject constructor( */ override suspend fun getHomeserverTerms(baseUrl: String): TermsResponse { return try { + val request = baseUrl + NetworkConstants.URI_API_PREFIX_PATH_R0 + "register" executeRequest(null) { - termsAPI.register(baseUrl + NetworkConstants.URI_API_PREFIX_PATH_R0 + "register") + termsAPI.register(request) } // Return empty result if it succeed, but it should never happen + Timber.w("Request $request succeeded, it should never happen") TermsResponse() } catch (throwable: Throwable) { - @Suppress("UNCHECKED_CAST") - TermsResponse( - policies = (throwable.toRegistrationFlowResponse() - ?.params - ?.get(LoginFlowTypes.TERMS) as? JsonDict) - ?.get("policies") as? JsonDict - ) + val registrationFlowResponse = throwable.toRegistrationFlowResponse() + if (registrationFlowResponse != null) { + @Suppress("UNCHECKED_CAST") + TermsResponse( + policies = (registrationFlowResponse + .params + ?.get(LoginFlowTypes.TERMS) as? JsonDict) + ?.get("policies") as? JsonDict + ) + } else { + // Other error + Timber.e(throwable, "Error while getting homeserver terms") + throw throwable + } } } diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index 6ca86be0950..293e0b2a583 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -160,7 +160,7 @@ Formatter\.formatShortFileSize===1 # android\.text\.TextUtils ### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt -enum class===114 +enum class===119 ### Do not import temporary legacy classes import org.matrix.android.sdk.internal.legacy.riot===3 diff --git a/tools/release/sign_apk.sh b/tools/release/sign_apk.sh index aae9e1a378a..de5a22dd34e 100755 --- a/tools/release/sign_apk.sh +++ b/tools/release/sign_apk.sh @@ -17,7 +17,7 @@ PARAM_KEYSTORE_PATH=$1 PARAM_APK=$2 # Other params -BUILD_TOOLS_VERSION="31.0.0-rc5" +BUILD_TOOLS_VERSION="31.0.0" MIN_SDK_VERSION=21 echo "Signing APK with build-tools version ${BUILD_TOOLS_VERSION} for min SDK version ${MIN_SDK_VERSION}..." diff --git a/tools/release/sign_apk_unsafe.sh b/tools/release/sign_apk_unsafe.sh index 5d209a4a2b4..a7536616e98 100755 --- a/tools/release/sign_apk_unsafe.sh +++ b/tools/release/sign_apk_unsafe.sh @@ -23,7 +23,7 @@ PARAM_KS_PASS=$3 PARAM_KEY_PASS=$4 # Other params -BUILD_TOOLS_VERSION="31.0.0-rc5" +BUILD_TOOLS_VERSION="31.0.0" MIN_SDK_VERSION=21 echo "Signing APK with build-tools version ${BUILD_TOOLS_VERSION} for min SDK version ${MIN_SDK_VERSION}..." diff --git a/vector/build.gradle b/vector/build.gradle index a578fdb52f3..2be5fa984dd 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -15,7 +15,7 @@ kapt { // Note: 2 digits max for each value ext.versionMajor = 1 ext.versionMinor = 3 -ext.versionPatch = 10 +ext.versionPatch = 13 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' @@ -140,7 +140,7 @@ android { buildConfigField "String", "BUILD_NUMBER", "\"${buildNumber}\"" resValue "string", "build_number", "\"${buildNumber}\"" - buildConfigField "im.vector.app.features.VectorFeatures.LoginVersion", "LOGIN_VERSION", "im.vector.app.features.VectorFeatures.LoginVersion.V1" + buildConfigField "im.vector.app.features.VectorFeatures.OnboardingVariant", "ONBOARDING_VARIANT", "im.vector.app.features.VectorFeatures.OnboardingVariant.FTUE_AUTH" buildConfigField "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy", "outboundSessionKeySharingStrategy", "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy.WhenTyping" @@ -359,7 +359,7 @@ dependencies { implementation 'com.facebook.stetho:stetho:1.6.0' // Phone number https://github.com/google/libphonenumber - implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.39' + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.40' // FlowBinding implementation libs.github.flowBinding @@ -389,6 +389,8 @@ dependencies { implementation libs.google.material implementation 'me.gujun.android:span:1.7' implementation libs.markwon.core + implementation libs.markwon.extLatex + implementation libs.markwon.inlineParser implementation libs.markwon.html implementation 'com.googlecode.htmlcompressor:htmlcompressor:1.5.2' implementation 'me.saket:better-link-movement-method:2.2.0' @@ -452,7 +454,7 @@ dependencies { // OSS License, gplay flavor only gplayImplementation 'com.google.android.gms:play-services-oss-licenses:17.0.0' - implementation "androidx.emoji2:emoji2:1.0.0" + implementation "androidx.emoji2:emoji2:1.0.1" implementation('com.github.BillCarsonFr:JsonViewer:0.7') // WebRTC diff --git a/vector/lint.xml b/vector/lint.xml index 9d9b208df7b..818349da240 100644 --- a/vector/lint.xml +++ b/vector/lint.xml @@ -72,6 +72,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +