Merge branch 'release/1.4.0' into main
This commit is contained in:
commit
d1e49624c7
36
.github/ISSUE_TEMPLATE/release.yml
vendored
36
.github/ISSUE_TEMPLATE/release.yml
vendored
@ -1,6 +1,6 @@
|
|||||||
name: Release checklist
|
name: Release checklist
|
||||||
description: Checklist for each release. This template is only for the core team.
|
description: Checklist for each release. This template is only for the core team.
|
||||||
title: "[Release] Element Android v"
|
title: "[Release] Element Android v"
|
||||||
labels: [🚀 Release]
|
labels: [🚀 Release]
|
||||||
assignees:
|
assignees:
|
||||||
- bmarty
|
- bmarty
|
||||||
@ -10,7 +10,7 @@ body:
|
|||||||
id: checklist
|
id: checklist
|
||||||
attributes:
|
attributes:
|
||||||
label: Release checklist
|
label: Release checklist
|
||||||
description: For the template example, we are releasing the version 1.1.10. Replace 1.1.10 with the version in the issue body.
|
description: For the template example, we are releasing the version 1.2.3. Replace 1.2.3 with the version in the issue body.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
If you are reading this, you have deleted the content of the release template: undo the deletion or start again.
|
If you are reading this, you have deleted the content of the release template: undo the deletion or start again.
|
||||||
value: |
|
value: |
|
||||||
@ -22,35 +22,41 @@ body:
|
|||||||
|
|
||||||
### Do the release
|
### Do the release
|
||||||
|
|
||||||
- [ ] Create release with gitflow, branch name `release/1.1.10`
|
- [ ] Create release with gitflow, branch name `release/1.2.3`
|
||||||
- [ ] Check the crashes from the PlayStore
|
- [ ] Check the crashes from the PlayStore
|
||||||
- [ ] Check the rageshake with the current dev version: https://github.com/matrix-org/element-android-rageshakes/labels/1.1.10-dev
|
- [ ] Check the rageshake with the current dev version: https://github.com/matrix-org/element-android-rageshakes/labels/1.2.3-dev
|
||||||
- [ ] Run the integration test, and especially `UiAllScreensSanityTest.allScreensTest()`
|
- [ ] Run the integration test, and especially `UiAllScreensSanityTest.allScreensTest()`
|
||||||
- [ ] Create an account on matrix.org
|
- [ ] Create an account on matrix.org and do some smoke tests that the sanity test does not cover like: 1-1 call, 1-1 video call, Jitsi call for instance
|
||||||
- [ ] Run towncrier: `towncrier --version v1.1.10 --draft` (remove `--draft` do write the file CHANGES.md)
|
- [ ] Run towncrier: `towncrier --version v1.2.3 --draft` (remove `--draft` do write the file CHANGES.md)
|
||||||
|
- [ ] Check that the folder `changelog.d` is empty. It can happen that some remaining files stay here
|
||||||
|
- [ ] Check the file CHANGES.md consistency. It's possible to reorder items (most important changes first) or change their section if relevant. Also an opportunity to fix some typo, or rewrite things
|
||||||
- [ ] Add file for fastlane under ./fastlane/metadata/android/en-US/changelogs
|
- [ ] Add file for fastlane under ./fastlane/metadata/android/en-US/changelogs
|
||||||
- [ ] Push the branch and start a draft PR (will not be merged), to check that the CI is happy with all the changes.
|
- [ ] (optional) Push the branch and start a draft PR (will not be merged), to check that the CI is happy with all the changes.
|
||||||
- [ ] Finish release with gitflow, delete the draft PR
|
- [ ] Finish release with gitflow, delete the draft PR (if created)
|
||||||
- [ ] Push `main` and the new tag `v1.1.10` to origin
|
- [ ] Push `main` and the new tag `v1.2.3` to origin
|
||||||
- [ ] Checkout `develop`
|
- [ ] Checkout `develop`
|
||||||
- [ ] Increase version in `./vector/build.gradle`
|
- [ ] Increase version (versionPatch + 2) in `./vector/build.gradle`
|
||||||
- [ ] Change the value of SDK_VERSION in the file `./matrix-sdk-android/build.gradle`
|
- [ ] Change the value of SDK_VERSION in the file `./matrix-sdk-android/build.gradle`
|
||||||
- [ ] Commit and push `develop`
|
- [ ] Commit and push `develop`
|
||||||
- [ ] Wait for [Buildkite](https://buildkite.com/matrix-dot-org/element-android/builds?branch=main) to build the `main` branch.
|
- [ ] Wait for [Buildkite](https://buildkite.com/matrix-dot-org/element-android/builds?branch=main) to build the `main` branch.
|
||||||
- [ ] Run the script `~/scripts/releaseElement.sh`. It will download the APKs from Buildkite check them and sign them.
|
- [ ] Run the script `~/scripts/releaseElement.sh`. It will download the APKs from Buildkite check them and sign them.
|
||||||
- [ ] Install the APK on your phone to check that the upgrade went well (no init sync, etc.)
|
- [ ] Install the APK on your phone to check that the upgrade went well (no init sync, etc.)
|
||||||
|
- [ ] Create the release on gitHub [from the tag](https://github.com/vector-im/element-android/tags), copy paste the block from the file CHANGES.md
|
||||||
|
- [ ] Add the 4 signed APKs to the GitHub release
|
||||||
|
- [ ] Ping the Android Internal room
|
||||||
|
|
||||||
|
### Once tested and validated internally
|
||||||
|
|
||||||
- [ ] Create a new beta release on the GooglePlay console and upload the 4 signed Apks.
|
- [ ] Create a new beta release on the GooglePlay console and upload the 4 signed Apks.
|
||||||
- [ ] Check that the version codes are correct
|
- [ ] Check that the version codes are correct
|
||||||
- [ ] Copy the fastlane change to the GooglePlay console in the section en-GB.
|
- [ ] Copy the fastlane change to the GooglePlay console in the section en-GB.
|
||||||
- [ ] Push to beta release to 100% of the users
|
- [ ] Push to beta release to 100% of the users
|
||||||
- [ ] Create the release on gitHub [from the tag](https://github.com/vector-im/element-android/tags), copy paste the block from the file CHANGES.md
|
- [ ] Notify the F-Droid team so that they can schedule the publication on F-Droid
|
||||||
- [ ] Add the 4 signed APKs to the GitHub release
|
|
||||||
- [ ] Ping the Android Internal room
|
|
||||||
- [ ] Add an entry in the internal diary
|
|
||||||
|
|
||||||
### Once Live on PlayStore
|
### Once Live on PlayStore
|
||||||
|
|
||||||
- [ ] Ping the Android public room and update its topic
|
- [ ] Ping the Android public room and update its topic
|
||||||
|
- [ ] Add an entry in the internal diary
|
||||||
|
|
||||||
### After at least 2 days
|
### After at least 2 days
|
||||||
|
|
||||||
@ -62,6 +68,8 @@ body:
|
|||||||
|
|
||||||
### Android SDK2
|
### Android SDK2
|
||||||
|
|
||||||
|
The SDK2 and the sample app are released only when Element has been pushed to production.
|
||||||
|
|
||||||
- [ ] Checkout the `main` branch on Element Android project
|
- [ ] Checkout the `main` branch on Element Android project
|
||||||
|
|
||||||
#### On the SDK2 project
|
#### On the SDK2 project
|
||||||
|
2
.github/workflows/integration_tests.yml
vendored
2
.github/workflows/integration_tests.yml
vendored
@ -180,6 +180,7 @@ jobs:
|
|||||||
body="$(cat ./matrix-sdk-android/build/outputs/androidTest-results/connected/*.xml | grep "<testsuite" | sed "s@.*tests=\(.*\)time=.*@\1@")"
|
body="$(cat ./matrix-sdk-android/build/outputs/androidTest-results/connected/*.xml | grep "<testsuite" | sed "s@.*tests=\(.*\)time=.*@\1@")"
|
||||||
echo "::set-output name=permalink::passed=$body"
|
echo "::set-output name=permalink::passed=$body"
|
||||||
- name: Find Comment
|
- name: Find Comment
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
uses: peter-evans/find-comment@v1
|
uses: peter-evans/find-comment@v1
|
||||||
id: fc
|
id: fc
|
||||||
with:
|
with:
|
||||||
@ -187,6 +188,7 @@ jobs:
|
|||||||
comment-author: 'github-actions[bot]'
|
comment-author: 'github-actions[bot]'
|
||||||
body-includes: Integration Tests Results
|
body-includes: Integration Tests Results
|
||||||
- name: Publish results to PR
|
- name: Publish results to PR
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
uses: peter-evans/create-or-update-comment@v1
|
uses: peter-evans/create-or-update-comment@v1
|
||||||
with:
|
with:
|
||||||
comment-id: ${{ steps.fc.outputs.comment-id }}
|
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||||
|
6
.github/workflows/quality.yml
vendored
6
.github/workflows/quality.yml
vendored
@ -54,7 +54,7 @@ jobs:
|
|||||||
echo "::set-output name=body::$body"
|
echo "::set-output name=body::$body"
|
||||||
fi
|
fi
|
||||||
- name: Find Comment
|
- name: Find Comment
|
||||||
if: always()
|
if: always() && github.event_name == 'pull_request'
|
||||||
uses: peter-evans/find-comment@v1
|
uses: peter-evans/find-comment@v1
|
||||||
id: fc
|
id: fc
|
||||||
with:
|
with:
|
||||||
@ -62,7 +62,7 @@ jobs:
|
|||||||
comment-author: 'github-actions[bot]'
|
comment-author: 'github-actions[bot]'
|
||||||
body-includes: Ktlint Results
|
body-includes: Ktlint Results
|
||||||
- name: Add comment if needed
|
- name: Add comment if needed
|
||||||
if: always() && steps.ktlint-results.outputs.add_comment == 'true'
|
if: always() && github.event_name == 'pull_request' && steps.ktlint-results.outputs.add_comment == 'true'
|
||||||
uses: peter-evans/create-or-update-comment@v1
|
uses: peter-evans/create-or-update-comment@v1
|
||||||
with:
|
with:
|
||||||
comment-id: ${{ steps.fc.outputs.comment-id }}
|
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||||
@ -73,7 +73,7 @@ jobs:
|
|||||||
${{ steps.ktlint-results.outputs.body }}
|
${{ steps.ktlint-results.outputs.body }}
|
||||||
edit-mode: replace
|
edit-mode: replace
|
||||||
- name: Delete comment if needed
|
- name: Delete comment if needed
|
||||||
if: always() && steps.fc.outputs.comment-id != '' && steps.ktlint-results.outputs.add_comment == 'false'
|
if: always() && github.event_name == 'pull_request' && steps.fc.outputs.comment-id != '' && steps.ktlint-results.outputs.add_comment == 'false'
|
||||||
uses: actions/github-script@v3
|
uses: actions/github-script@v3
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
|
50
.github/workflows/triage-labelled.yml
vendored
50
.github/workflows/triage-labelled.yml
vendored
@ -202,3 +202,53 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
PROJECT_ID: "PN_kwDOAM0swc3m-g"
|
PROJECT_ID: "PN_kwDOAM0swc3m-g"
|
||||||
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
|
||||||
|
move_ftue_issues:
|
||||||
|
name: Z-FTUE to Mobile FTUE board
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Skip in forks
|
||||||
|
if: >
|
||||||
|
github.repository == 'vector-im/element-android' &&
|
||||||
|
contains(github.event.issue.labels.*.name, 'Z-FTUE')
|
||||||
|
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_kwDOAM0swc4AAqVx"
|
||||||
|
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
|
||||||
|
move_WTF_issues:
|
||||||
|
name: Z-WTF to WTF board
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Skip in forks
|
||||||
|
if: >
|
||||||
|
github.repository == 'vector-im/element-android' &&
|
||||||
|
contains(github.event.issue.labels.*.name, 'Z-WTF')
|
||||||
|
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_kwDOAM0swc4AArk0"
|
||||||
|
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
32
CHANGES.md
32
CHANGES.md
@ -1,3 +1,35 @@
|
|||||||
|
Changes in Element v1.4.0 (2022-02-09)
|
||||||
|
======================================
|
||||||
|
|
||||||
|
Features ✨
|
||||||
|
----------
|
||||||
|
- Initial implementation of thread messages ([#4746](https://github.com/vector-im/element-android/issues/4746))
|
||||||
|
- Support message bubbles in timeline. ([#4937](https://github.com/vector-im/element-android/issues/4937))
|
||||||
|
- Support generic location pin ([#5146](https://github.com/vector-im/element-android/issues/5146))
|
||||||
|
- Retrieve map style url from .well-known ([#5175](https://github.com/vector-im/element-android/issues/5175))
|
||||||
|
|
||||||
|
Bugfixes 🐛
|
||||||
|
----------
|
||||||
|
- Fixes non sans-serif font weights being ignored ([#3907](https://github.com/vector-im/element-android/issues/3907))
|
||||||
|
- Fixing missing/intermittent notifications on the google play variant when wifi is enabled ([#5038](https://github.com/vector-im/element-android/issues/5038))
|
||||||
|
- Fixes call statuses in the timeline for missed/rejected calls and connected calls. ([#5088](https://github.com/vector-im/element-android/issues/5088))
|
||||||
|
- Fix fallback permalink when threads are disabled ([#5128](https://github.com/vector-im/element-android/issues/5128))
|
||||||
|
- Analytics: aligns use case identifying with iOS implementation ([#5142](https://github.com/vector-im/element-android/issues/5142))
|
||||||
|
- Fix location rendering in timeline if map cannot be loaded ([#5143](https://github.com/vector-im/element-android/issues/5143))
|
||||||
|
|
||||||
|
Other changes
|
||||||
|
-------------
|
||||||
|
- "Invite users to space" dialog now closed when user choose invite method ([#4295](https://github.com/vector-im/element-android/issues/4295))
|
||||||
|
- Changed layout for space card and room card used at "explore room" screen and space/room invite dialogs ([#4304](https://github.com/vector-im/element-android/issues/4304))
|
||||||
|
- Removed spaces restricted search hint dialogs ([#4315](https://github.com/vector-im/element-android/issues/4315))
|
||||||
|
- Remove Search from room options if not available ([#4641](https://github.com/vector-im/element-android/issues/4641))
|
||||||
|
- Qr code scanning fragments merged into one ([#4873](https://github.com/vector-im/element-android/issues/4873))
|
||||||
|
- Fix CI/CD errors after merges for quality and integration tests ([#5118](https://github.com/vector-im/element-android/issues/5118))
|
||||||
|
- Added automation for the Z-FTUE label to add issues to the FTUE Project Board ([#5120](https://github.com/vector-im/element-android/issues/5120))
|
||||||
|
- Added automation for WTF labels to move to WTF project board ([#5148](https://github.com/vector-im/element-android/issues/5148))
|
||||||
|
- Update WTF automation to fix it ([#5173](https://github.com/vector-im/element-android/issues/5173))
|
||||||
|
|
||||||
|
|
||||||
Changes in Element v1.3.18 (2022-02-03)
|
Changes in Element v1.3.18 (2022-02-03)
|
||||||
=======================================
|
=======================================
|
||||||
|
|
||||||
|
@ -14,7 +14,8 @@ It is a total rewrite of [Riot-Android](https://github.com/vector-im/riot-androi
|
|||||||
[<img src="resources/img/google-play-badge.png" alt="Get it on Google Play" height="60">](https://play.google.com/store/apps/details?id=im.vector.app)
|
[<img src="resources/img/google-play-badge.png" alt="Get it on Google Play" height="60">](https://play.google.com/store/apps/details?id=im.vector.app)
|
||||||
[<img src="resources/img/f-droid-badge.png" alt="Get it on F-Droid" height="60">](https://f-droid.org/app/im.vector.app)
|
[<img src="resources/img/f-droid-badge.png" alt="Get it on F-Droid" height="60">](https://f-droid.org/app/im.vector.app)
|
||||||
|
|
||||||
Nightly build: [](https://buildkite.com/matrix-dot-org/element-android/builds?branch=develop)
|
Nightly build: [](https://buildkite.com/matrix-dot-org/element-android/builds?branch=develop) Nighly sanity test status: [](https://github.com/vector-im/element-android/actions/workflows/sanity_test.yml)
|
||||||
|
|
||||||
|
|
||||||
# New Android SDK
|
# New Android SDK
|
||||||
|
|
||||||
|
17
build.gradle
17
build.gradle
@ -36,6 +36,12 @@ allprojects {
|
|||||||
apply plugin: "org.jlleitschuh.gradle.ktlint"
|
apply plugin: "org.jlleitschuh.gradle.ktlint"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
mavenCentral {
|
||||||
|
content {
|
||||||
|
groups.mavenCentral.regex.each { includeGroupByRegex it }
|
||||||
|
groups.mavenCentral.group.each { includeGroup it }
|
||||||
|
}
|
||||||
|
}
|
||||||
maven {
|
maven {
|
||||||
url 'https://jitpack.io'
|
url 'https://jitpack.io'
|
||||||
content {
|
content {
|
||||||
@ -59,12 +65,6 @@ allprojects {
|
|||||||
groups.google.group.each { includeGroup it }
|
groups.google.group.each { includeGroup it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mavenCentral {
|
|
||||||
content {
|
|
||||||
groups.mavenCentral.regex.each { includeGroupByRegex it }
|
|
||||||
groups.mavenCentral.group.each { includeGroup it }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//noinspection JcenterRepositoryObsolete
|
//noinspection JcenterRepositoryObsolete
|
||||||
jcenter {
|
jcenter {
|
||||||
content {
|
content {
|
||||||
@ -144,6 +144,11 @@ project(":diff-match-patch") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Global configurations across all modules
|
||||||
|
ext {
|
||||||
|
isThreadingEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
//project(":matrix-sdk-android") {
|
//project(":matrix-sdk-android") {
|
||||||
// sonarqube {
|
// sonarqube {
|
||||||
// properties {
|
// properties {
|
||||||
|
2
fastlane/metadata/android/cs-CZ/changelogs/40103160.txt
Normal file
2
fastlane/metadata/android/cs-CZ/changelogs/40103160.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Hlavní změny v této verzi: Odeslání vlastní polohy do libovolné místnosti. Možnost úpravy hlasování.
|
||||||
|
Úplný seznam změn: https://github.com/vector-im/element-android/releases/tag/v1.3.16
|
2
fastlane/metadata/android/de-DE/changelogs/40103160.txt
Normal file
2
fastlane/metadata/android/de-DE/changelogs/40103160.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Hauptänderungen: Du kannst ab sofort deinen Standort an deine Räume senden und Abstimmungen bearbeiten.
|
||||||
|
Alle Änderungen: https://github.com/vector-im/element-android/releases/tag/v1.3.16
|
2
fastlane/metadata/android/en-US/changelogs/40104000.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40104000.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Main changes in this version: Initial implementation of thread messages. Message bubbles.
|
||||||
|
Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.4.0
|
2
fastlane/metadata/android/et/changelogs/40103160.txt
Normal file
2
fastlane/metadata/android/et/changelogs/40103160.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Põhilised muutused selles versioonis: oma asukoha saatmine jututuppa ja küsitluste muutmise võimalus.
|
||||||
|
Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases/tag/v1.3.16
|
2
fastlane/metadata/android/fa/changelogs/40103130.txt
Normal file
2
fastlane/metadata/android/fa/changelogs/40103130.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
تغییرات عمده در این نگارشنخستین تغییر در صفحههای راهاندازی شامل وارد شدن به تجزیهها. پشتیبانی از رویدادهایی با ریاضیات افزوده در آزمایشگاهها.
|
||||||
|
گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.3.13
|
2
fastlane/metadata/android/fa/changelogs/40103140.txt
Normal file
2
fastlane/metadata/android/fa/changelogs/40103140.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
تغییرات عمده در این نگارشنخستین تغییر در صفحههای راهاندازی شامل وارد شدن به تجزیهها. پشتیبانی از رویدادهایی با ریاضیات افزوده در آزمایشگاهها.
|
||||||
|
گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.3.14
|
2
fastlane/metadata/android/fa/changelogs/40103150.txt
Normal file
2
fastlane/metadata/android/fa/changelogs/40103150.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
تغییرات عمده در این نگارشنخستین تغییر در صفحههای راهاندازی شامل وارد شدن به تجزیهها. پشتیبانی از رویدادهایی با ریاضیات افزوده در آزمایشگاهها.
|
||||||
|
گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.3.15
|
2
fastlane/metadata/android/fa/changelogs/40103160.txt
Normal file
2
fastlane/metadata/android/fa/changelogs/40103160.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
تغییرات عمده در این نگارش: فرستادن مکانتان به هر اتاقی. ویرایش نظرسنجی.
|
||||||
|
گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.3.16
|
2
fastlane/metadata/android/fr-FR/changelogs/40103130.txt
Normal file
2
fastlane/metadata/android/fr-FR/changelogs/40103130.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Principaux changements pour cette version : Premier changement dans l’écran de bienvenue, y compris l’adhésion aux données d’analyses. Support des événements avec opération mathématiques ajoutées dans les labs.
|
||||||
|
Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.3.13
|
2
fastlane/metadata/android/fr-FR/changelogs/40103140.txt
Normal file
2
fastlane/metadata/android/fr-FR/changelogs/40103140.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Principaux changements pour cette version : Premier changement dans l’écran de bienvenue, y compris l’adhésion aux données d’analyses. Support des événements avec opération mathématiques ajoutées dans les labs.
|
||||||
|
Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.3.14
|
2
fastlane/metadata/android/fr-FR/changelogs/40103150.txt
Normal file
2
fastlane/metadata/android/fr-FR/changelogs/40103150.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Principaux changements pour cette version : Premier changement dans l’écran de bienvenue, y compris l’adhésion aux données d’analyses. Support des événements avec opération mathématiques ajoutées dans les labs.
|
||||||
|
Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.3.15
|
2
fastlane/metadata/android/fr-FR/changelogs/40103160.txt
Normal file
2
fastlane/metadata/android/fr-FR/changelogs/40103160.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Principaux changements pour cette version : envoi de votre position dans n’importe quelle salon. Édition des sondage.
|
||||||
|
Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.3.16
|
2
fastlane/metadata/android/hu-HU/changelogs/40103160.txt
Normal file
2
fastlane/metadata/android/hu-HU/changelogs/40103160.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Fő változás ebben a verzióban: földrajzi helyzet küldése bármely szobába. Szavazás szerkesztése
|
||||||
|
Teljes változásnapló: https://github.com/vector-im/element-android/releases/tag/v1.3.16
|
@ -1,2 +1,2 @@
|
|||||||
Perubahan utama dalam versi ini: Perubahan pertama di layar permulaan, termasuk analitik opt-in. Dukungan untuk Peristiwa dengan Matematika ditambahkan di Uji Coba.
|
Perubahan utama dalam versi ini: Perubahan pertama di layar permulaan, termasuk analitik opt-in. Dukungan untuk Peristiwa dengan Matematika ditambahkan di Uji Coba.
|
||||||
Changelog lanjutan:
|
Changelog lanjutan: https://github.com/vector-im/element-android/releases/tag/v1.3.13
|
||||||
|
2
fastlane/metadata/android/id/changelogs/40103160.txt
Normal file
2
fastlane/metadata/android/id/changelogs/40103160.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Perubahan utama dalam versi ini: Kirim lokasi Anda ke ruangan apa saja. Edit poll.
|
||||||
|
Changelog lanjutan: https://github.com/vector-im/element-android/releases/tag/v1.3.16
|
2
fastlane/metadata/android/it-IT/changelogs/40103160.txt
Normal file
2
fastlane/metadata/android/it-IT/changelogs/40103160.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Modifiche principali in questa versione: invia la tua posizione in qualsiasi stanza. Modifica sondaggi.
|
||||||
|
Cronologia completa: https://github.com/vector-im/element-android/releases/tag/v1.3.16
|
2
fastlane/metadata/android/pt-BR/changelogs/40103160.txt
Normal file
2
fastlane/metadata/android/pt-BR/changelogs/40103160.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Principais mudanças nesta versão: envie sua localização para qualquer sala. Editar sondagem.
|
||||||
|
Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.3.16
|
2
fastlane/metadata/android/ru-RU/changelogs/40103160.txt
Normal file
2
fastlane/metadata/android/ru-RU/changelogs/40103160.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Основные изменения в этой версии: отправьте свое местоположение в любую комнату. Редактирование опроса.
|
||||||
|
Полный список изменений: https://github.com/vector-im/element-android/releases/tag/v1.3.16
|
2
fastlane/metadata/android/sk/changelogs/40103160.txt
Normal file
2
fastlane/metadata/android/sk/changelogs/40103160.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Hlavné zmeny v tejto verzii: odoslanie polohy do ľubovoľnej miestnosti. Úprava ankety.
|
||||||
|
Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.3.16
|
2
fastlane/metadata/android/sq/changelogs/40103140.txt
Normal file
2
fastlane/metadata/android/sq/changelogs/40103140.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Ndryshimet kryesore në këtë version: Ndryshimi i parë në skenat e mirëseardhjes, përfshi zgjedhje për pjesëmarrje në Analiza. Në laboratorë u shtua mbulim për Akte me Formula Matematikore.
|
||||||
|
Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.3.14
|
2
fastlane/metadata/android/sq/changelogs/40103150.txt
Normal file
2
fastlane/metadata/android/sq/changelogs/40103150.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Ndryshimet kryesore në këtë version: Ndryshimi i parë në skenat e mirëseardhjes, përfshi zgjedhje për pjesëmarrje në Analiza. Në laboratorë u shtua mbulim për Akte me Formula Matematikore.
|
||||||
|
Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.3.15
|
2
fastlane/metadata/android/sq/changelogs/40103160.txt
Normal file
2
fastlane/metadata/android/sq/changelogs/40103160.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Ndryshimet kryesore në këtë version: dërgojeni vendndodhjen tuaj te cilado dhomë. Përpunoni pyetësor.
|
||||||
|
Regjistër i plotës ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.3.16
|
2
fastlane/metadata/android/sv-SE/changelogs/40103130.txt
Normal file
2
fastlane/metadata/android/sv-SE/changelogs/40103130.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Huvudsakliga ändringar i den här versionen: Första ändringen på introduktionsskärmar, inklusive opt-in för statistik. Stöd för händelser med matte tillagd i experiment.
|
||||||
|
Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.3.13
|
2
fastlane/metadata/android/sv-SE/changelogs/40103140.txt
Normal file
2
fastlane/metadata/android/sv-SE/changelogs/40103140.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Huvudsakliga ändringar i den här versionen: Första ändringen på introduktionsskärmar, inklusive opt-in för statistik. Stöd för händelser med matte tillagd i experiment.
|
||||||
|
Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.3.14
|
2
fastlane/metadata/android/sv-SE/changelogs/40103150.txt
Normal file
2
fastlane/metadata/android/sv-SE/changelogs/40103150.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Huvudsakliga ändringar i den här versionen: Första ändringen på introduktionsskärmar, inklusive opt-in för statistik. Stöd för händelser med matte tillagd i experiment.
|
||||||
|
Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.3.15
|
2
fastlane/metadata/android/sv-SE/changelogs/40103160.txt
Normal file
2
fastlane/metadata/android/sv-SE/changelogs/40103160.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Huvudsakliga ändringar i den här versionen: skicka din plats till vilket rum som helst. Redigera omröstningar.
|
||||||
|
Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.3.16
|
2
fastlane/metadata/android/uk/changelogs/40103160.txt
Normal file
2
fastlane/metadata/android/uk/changelogs/40103160.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Основні зміни у цій версії: надсилання свого місцеперебування у будь-яку кімнату. Редагування опитувань.
|
||||||
|
Вичерпний перелік змін: https://github.com/vector-im/element-android/releases/tag/v1.3.16
|
2
fastlane/metadata/android/zh-TW/changelogs/40103160.txt
Normal file
2
fastlane/metadata/android/zh-TW/changelogs/40103160.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
此版本中的主要變動:將您的位置傳送給任何聊天室。編輯投票。
|
||||||
|
完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.16
|
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<translate
|
||||||
|
android:fromXDelta="-100%p" android:toXDelta="0"
|
||||||
|
android:duration="@android:integer/config_mediumAnimTime"/>
|
||||||
|
</set>
|
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<translate
|
||||||
|
android:startOffset="250"
|
||||||
|
android:fromXDelta="100%p" android:toXDelta="0"
|
||||||
|
android:duration="@android:integer/config_mediumAnimTime"/>
|
||||||
|
</set>
|
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<translate
|
||||||
|
android:startOffset="250"
|
||||||
|
android:fromXDelta="0" android:toXDelta="-100%p"
|
||||||
|
android:duration="@android:integer/config_mediumAnimTime"/>
|
||||||
|
</set>
|
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<translate
|
||||||
|
android:fromXDelta="0" android:toXDelta="100%p"
|
||||||
|
android:duration="@android:integer/config_mediumAnimTime"/>
|
||||||
|
</set>
|
@ -2,9 +2,6 @@
|
|||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<!-- Tint color is provided by the theme -->
|
<!-- Tint color is provided by the theme -->
|
||||||
<solid android:color="@android:color/black" />
|
<solid android:color="@android:color/black" />
|
||||||
<size
|
|
||||||
android:width="240dp"
|
|
||||||
android:height="44dp" />
|
|
||||||
<corners
|
<corners
|
||||||
android:bottomLeftRadius="12dp"
|
android:bottomLeftRadius="12dp"
|
||||||
android:bottomRightRadius="12dp"
|
android:bottomRightRadius="12dp"
|
@ -2,16 +2,14 @@
|
|||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<item android:id="@android:id/background">
|
<item android:id="@android:id/background">
|
||||||
<shape>
|
<shape android:shape="oval">
|
||||||
<corners android:radius="8dp" />
|
<solid android:color="?vctr_system" />
|
||||||
<solid android:color="?vctr_room_active_widgets_banner_bg" />
|
|
||||||
</shape>
|
</shape>
|
||||||
</item>
|
</item>
|
||||||
|
|
||||||
<item android:id="@android:id/progress">
|
<item android:id="@android:id/progress">
|
||||||
<clip>
|
<clip>
|
||||||
<shape>
|
<shape android:shape="oval">
|
||||||
<corners android:radius="8dp" />
|
|
||||||
<solid android:color="@color/vctr_notice_secondary_alpha12" />
|
<solid android:color="@color/vctr_notice_secondary_alpha12" />
|
||||||
</shape>
|
</shape>
|
||||||
</clip>
|
</clip>
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
<dimen name="menu_item_ripple_size">28dp</dimen>
|
||||||
<bool name="is_rtl">true</bool>
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
@ -4,6 +4,4 @@
|
|||||||
<!-- Created to detect what has to be implemented (especially in the settings) -->
|
<!-- Created to detect what has to be implemented (especially in the settings) -->
|
||||||
<bool name="false_not_implemented">false</bool>
|
<bool name="false_not_implemented">false</bool>
|
||||||
|
|
||||||
<bool name="is_rtl">false</bool>
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
@ -137,4 +137,5 @@
|
|||||||
<attr name="vctr_presence_indicator_offline" format="color" />
|
<attr name="vctr_presence_indicator_offline" format="color" />
|
||||||
<color name="vctr_presence_indicator_offline_light">@color/palette_gray_100</color>
|
<color name="vctr_presence_indicator_offline_light">@color/palette_gray_100</color>
|
||||||
<color name="vctr_presence_indicator_offline_dark">@color/palette_gray_450</color>
|
<color name="vctr_presence_indicator_offline_dark">@color/palette_gray_450</color>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<!-- Timeline bubble background colors -->
|
||||||
|
<attr name="vctr_message_bubble_inbound" format="color" />
|
||||||
|
<color name="vctr_message_bubble_inbound_light">#E8EDF4</color>
|
||||||
|
<color name="vctr_message_bubble_inbound_dark">#21262C</color>
|
||||||
|
<attr name="vctr_message_bubble_outbound" format="color" />
|
||||||
|
<color name="vctr_message_bubble_outbound_light">#E7F8F3</color>
|
||||||
|
<color name="vctr_message_bubble_outbound_dark">#133A34</color>
|
||||||
|
</resources>
|
@ -15,6 +15,8 @@
|
|||||||
<dimen name="item_decoration_left_margin">72dp</dimen>
|
<dimen name="item_decoration_left_margin">72dp</dimen>
|
||||||
<dimen name="item_event_message_state_size">16dp</dimen>
|
<dimen name="item_event_message_state_size">16dp</dimen>
|
||||||
|
|
||||||
|
<dimen name="item_event_message_media_button_size">32dp</dimen>
|
||||||
|
|
||||||
<dimen name="chat_avatar_size">40dp</dimen>
|
<dimen name="chat_avatar_size">40dp</dimen>
|
||||||
<dimen name="member_list_avatar_size">60dp</dimen>
|
<dimen name="member_list_avatar_size">60dp</dimen>
|
||||||
|
|
||||||
@ -42,12 +44,23 @@
|
|||||||
|
|
||||||
<!-- Preview Url -->
|
<!-- Preview Url -->
|
||||||
<dimen name="preview_url_view_corner_radius">8dp</dimen>
|
<dimen name="preview_url_view_corner_radius">8dp</dimen>
|
||||||
|
<dimen name="preview_url_view_image_max_height">160dp</dimen>
|
||||||
|
|
||||||
|
<dimen name="menu_item_icon_size">24dp</dimen>
|
||||||
|
<dimen name="menu_item_size">48dp</dimen>
|
||||||
|
<dimen name="menu_item_ripple_size">48dp</dimen>
|
||||||
|
|
||||||
<!-- Composer -->
|
<!-- Composer -->
|
||||||
<dimen name="composer_min_height">56dp</dimen>
|
<dimen name="composer_min_height">56dp</dimen>
|
||||||
<dimen name="composer_attachment_size">52dp</dimen>
|
<dimen name="composer_attachment_size">52dp</dimen>
|
||||||
<dimen name="composer_attachment_margin">1dp</dimen>
|
<dimen name="composer_attachment_margin">1dp</dimen>
|
||||||
|
|
||||||
|
|
||||||
|
<dimen name="chat_bubble_margin_start">28dp</dimen>
|
||||||
|
<dimen name="chat_bubble_margin_end">62dp</dimen>
|
||||||
|
<dimen name="chat_bubble_fixed_size">300dp</dimen>
|
||||||
|
<dimen name="chat_bubble_corner_radius">12dp</dimen>
|
||||||
|
|
||||||
<!-- Onboarding -->
|
<!-- Onboarding -->
|
||||||
<item name="ftue_auth_gutter_start_percent" format="float" type="dimen">0.05</item>
|
<item name="ftue_auth_gutter_start_percent" format="float" type="dimen">0.05</item>
|
||||||
<item name="ftue_auth_gutter_end_percent" format="float" type="dimen">0.95</item>
|
<item name="ftue_auth_gutter_end_percent" format="float" type="dimen">0.95</item>
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<declare-styleable name="MessageBubble">
|
||||||
|
<attr name="incoming_style" format="boolean" />
|
||||||
|
<attr name="show_time_overlay" format="boolean" />
|
||||||
|
<attr name="is_first" format="boolean" />
|
||||||
|
<attr name="is_last" format="boolean" />
|
||||||
|
</declare-styleable>
|
||||||
|
|
||||||
|
</resources>
|
@ -6,6 +6,7 @@
|
|||||||
<style name="Widget.Vector.ProgressBar.Horizontal.File">
|
<style name="Widget.Vector.ProgressBar.Horizontal.File">
|
||||||
<item name="android:indeterminateOnly">false</item>
|
<item name="android:indeterminateOnly">false</item>
|
||||||
<item name="android:progressDrawable">@drawable/file_progress_bar</item>
|
<item name="android:progressDrawable">@drawable/file_progress_bar</item>
|
||||||
|
<item name="android:progressBackgroundTint">?android:colorBackground</item>
|
||||||
<item name="android:minHeight">10dp</item>
|
<item name="android:minHeight">10dp</item>
|
||||||
<item name="android:maxHeight">40dp</item>
|
<item name="android:maxHeight">40dp</item>
|
||||||
</style>
|
</style>
|
||||||
|
@ -4,12 +4,23 @@
|
|||||||
<style name="TimelineContentStubBaseParams">
|
<style name="TimelineContentStubBaseParams">
|
||||||
<item name="android:layout_width">match_parent</item>
|
<item name="android:layout_width">match_parent</item>
|
||||||
<item name="android:layout_height">wrap_content</item>
|
<item name="android:layout_height">wrap_content</item>
|
||||||
<item name="android:layout_marginStart">8dp</item>
|
</style>
|
||||||
<item name="android:layout_marginLeft">8dp</item>
|
|
||||||
<item name="android:layout_marginEnd">8dp</item>
|
<style name="TimelineContentStubContainerParams">
|
||||||
<item name="android:layout_marginRight">8dp</item>
|
<item name="android:paddingStart">8dp</item>
|
||||||
<item name="android:layout_marginBottom">4dp</item>
|
<item name="android:paddingEnd">8dp</item>
|
||||||
<item name="android:layout_marginTop">4dp</item>
|
<item name="android:paddingTop">4dp</item>
|
||||||
|
<item name="android:paddingBottom">4dp</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="TimelineContentMediaPillStyle">
|
||||||
|
<item name="android:paddingStart">8dp</item>
|
||||||
|
<item name="android:paddingEnd">8dp</item>
|
||||||
|
<item name="android:paddingTop">6dp</item>
|
||||||
|
<item name="android:paddingBottom">6dp</item>
|
||||||
|
<item name="minHeight">48dp</item>
|
||||||
|
<item name="android:background">@drawable/bg_media_pill</item>
|
||||||
|
<item name="android:backgroundTint">?vctr_content_quinary</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
@ -31,6 +31,8 @@
|
|||||||
<item name="vctr_waiting_background_color">@color/vctr_waiting_background_color_dark</item>
|
<item name="vctr_waiting_background_color">@color/vctr_waiting_background_color_dark</item>
|
||||||
<item name="vctr_chat_effect_snow_background">@color/vctr_chat_effect_snow_background_dark</item>
|
<item name="vctr_chat_effect_snow_background">@color/vctr_chat_effect_snow_background_dark</item>
|
||||||
<item name="vctr_toolbar_background">@color/element_system_dark</item>
|
<item name="vctr_toolbar_background">@color/element_system_dark</item>
|
||||||
|
<item name="vctr_message_bubble_inbound">@color/vctr_message_bubble_inbound_dark</item>
|
||||||
|
<item name="vctr_message_bubble_outbound">@color/vctr_message_bubble_outbound_dark</item>
|
||||||
|
|
||||||
<!-- room message colors -->
|
<!-- room message colors -->
|
||||||
<item name="vctr_notice_secondary">#61708B</item>
|
<item name="vctr_notice_secondary">#61708B</item>
|
||||||
@ -105,9 +107,6 @@
|
|||||||
<!-- disable the overscroll because setOverscrollHeader/Footer don't always work -->
|
<!-- disable the overscroll because setOverscrollHeader/Footer don't always work -->
|
||||||
<item name="android:overScrollMode">never</item>
|
<item name="android:overScrollMode">never</item>
|
||||||
|
|
||||||
<!-- fonts -->
|
|
||||||
<item name="android:typeface">sans</item>
|
|
||||||
|
|
||||||
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
|
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
|
||||||
|
|
||||||
<item name="pf_lock_screen">@style/PinCodeScreenStyle</item>
|
<item name="pf_lock_screen">@style/PinCodeScreenStyle</item>
|
||||||
|
@ -31,6 +31,8 @@
|
|||||||
<item name="vctr_waiting_background_color">@color/vctr_waiting_background_color_light</item>
|
<item name="vctr_waiting_background_color">@color/vctr_waiting_background_color_light</item>
|
||||||
<item name="vctr_chat_effect_snow_background">@color/vctr_chat_effect_snow_background_light</item>
|
<item name="vctr_chat_effect_snow_background">@color/vctr_chat_effect_snow_background_light</item>
|
||||||
<item name="vctr_toolbar_background">@color/element_background_light</item>
|
<item name="vctr_toolbar_background">@color/element_background_light</item>
|
||||||
|
<item name="vctr_message_bubble_inbound">@color/vctr_message_bubble_inbound_light</item>
|
||||||
|
<item name="vctr_message_bubble_outbound">@color/vctr_message_bubble_outbound_light</item>
|
||||||
|
|
||||||
<!-- room message colors -->
|
<!-- room message colors -->
|
||||||
<item name="vctr_notice_secondary">#61708B</item>
|
<item name="vctr_notice_secondary">#61708B</item>
|
||||||
@ -105,9 +107,6 @@
|
|||||||
<!-- disable the overscroll because setOverscrollHeader/Footer don't always work -->
|
<!-- disable the overscroll because setOverscrollHeader/Footer don't always work -->
|
||||||
<item name="android:overScrollMode">never</item>
|
<item name="android:overScrollMode">never</item>
|
||||||
|
|
||||||
<!-- fonts -->
|
|
||||||
<item name="android:typeface">sans</item>
|
|
||||||
|
|
||||||
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
|
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
|
||||||
|
|
||||||
<item name="pf_lock_screen">@style/PinCodeScreenStyle</item>
|
<item name="pf_lock_screen">@style/PinCodeScreenStyle</item>
|
||||||
|
@ -32,6 +32,8 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
|||||||
import org.matrix.android.sdk.api.util.Optional
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
import org.matrix.android.sdk.api.util.toOptional
|
import org.matrix.android.sdk.api.util.toOptional
|
||||||
|
|
||||||
|
typealias ThreadRootEvent = TimelineEvent
|
||||||
|
|
||||||
class FlowRoom(private val room: Room) {
|
class FlowRoom(private val room: Room) {
|
||||||
|
|
||||||
fun liveRoomSummary(): Flow<Optional<RoomSummary>> {
|
fun liveRoomSummary(): Flow<Optional<RoomSummary>> {
|
||||||
@ -98,6 +100,20 @@ class FlowRoom(private val room: Room) {
|
|||||||
fun liveNotificationState(): Flow<RoomNotificationState> {
|
fun liveNotificationState(): Flow<RoomNotificationState> {
|
||||||
return room.getLiveRoomNotificationState().asFlow()
|
return room.getLiveRoomNotificationState().asFlow()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun liveThreadList(): Flow<List<ThreadRootEvent>> {
|
||||||
|
return room.getAllThreadsLive().asFlow()
|
||||||
|
.startWith(room.coroutineDispatchers.io) {
|
||||||
|
room.getAllThreads()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun liveLocalUnreadThreadList(): Flow<List<ThreadRootEvent>> {
|
||||||
|
return room.getMarkedThreadNotificationsLive().asFlow()
|
||||||
|
.startWith(room.coroutineDispatchers.io) {
|
||||||
|
room.getMarkedThreadNotifications()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Room.flow(): FlowRoom {
|
fun Room.flow(): FlowRoom {
|
||||||
|
@ -31,13 +31,15 @@ android {
|
|||||||
// that the app's state is completely cleared between tests.
|
// that the app's state is completely cleared between tests.
|
||||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||||
|
|
||||||
buildConfigField "String", "SDK_VERSION", "\"1.3.18\""
|
buildConfigField "String", "SDK_VERSION", "\"1.3.19\""
|
||||||
|
|
||||||
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
|
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
|
||||||
resValue "string", "git_sdk_revision", "\"${gitRevision()}\""
|
resValue "string", "git_sdk_revision", "\"${gitRevision()}\""
|
||||||
resValue "string", "git_sdk_revision_unix_date", "\"${gitRevisionUnixDate()}\""
|
resValue "string", "git_sdk_revision_unix_date", "\"${gitRevisionUnixDate()}\""
|
||||||
resValue "string", "git_sdk_revision_date", "\"${gitRevisionDate()}\""
|
resValue "string", "git_sdk_revision_date", "\"${gitRevisionDate()}\""
|
||||||
|
|
||||||
|
// Indicates whether or not threading support is enabled
|
||||||
|
buildConfigField "Boolean", "THREADING_ENABLED", "${isThreadingEnabled}"
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
consumerProguardFiles 'proguard-rules.pro'
|
consumerProguardFiles 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
@ -139,6 +141,9 @@ dependencies {
|
|||||||
|
|
||||||
kapt 'dk.ilios:realmfieldnameshelper:2.0.0'
|
kapt 'dk.ilios:realmfieldnameshelper:2.0.0'
|
||||||
|
|
||||||
|
// Shared Preferences
|
||||||
|
implementation libs.androidx.preferenceKtx
|
||||||
|
|
||||||
// Work
|
// Work
|
||||||
implementation libs.androidx.work
|
implementation libs.androidx.work
|
||||||
|
|
||||||
|
@ -157,14 +157,20 @@ class CommonTestHelper(context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Will send nb of messages provided by count parameter but waits 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(timeline: Timeline, room: Room, message: String, count: Int, timeout: Long): List<TimelineEvent> {
|
private fun sendTextMessagesBatched(timeline: Timeline, room: Room, message: String, count: Int, timeout: Long, rootThreadEventId: String? = null): List<TimelineEvent> {
|
||||||
val sentEvents = ArrayList<TimelineEvent>(count)
|
val sentEvents = ArrayList<TimelineEvent>(count)
|
||||||
(1 until count + 1)
|
(1 until count + 1)
|
||||||
.map { "$message #$it" }
|
.map { "$message #$it" }
|
||||||
.chunked(10)
|
.chunked(10)
|
||||||
.forEach { batchedMessages ->
|
.forEach { batchedMessages ->
|
||||||
batchedMessages.forEach { formattedMessage ->
|
batchedMessages.forEach { formattedMessage ->
|
||||||
room.sendTextMessage(formattedMessage)
|
if (rootThreadEventId != null) {
|
||||||
|
room.replyInThread(
|
||||||
|
rootThreadEventId = rootThreadEventId,
|
||||||
|
replyInThreadText = formattedMessage)
|
||||||
|
} else {
|
||||||
|
room.sendTextMessage(formattedMessage)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
waitWithLatch(timeout) { latch ->
|
waitWithLatch(timeout) { latch ->
|
||||||
val timelineListener = object : Timeline.Listener {
|
val timelineListener = object : Timeline.Listener {
|
||||||
@ -196,6 +202,27 @@ class CommonTestHelper(context: Context) {
|
|||||||
return sentEvents
|
return sentEvents
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reply in a thread
|
||||||
|
* @param room the room where to send the messages
|
||||||
|
* @param message the message to send
|
||||||
|
* @param numberOfMessages the number of time the message will be sent
|
||||||
|
*/
|
||||||
|
fun replyInThreadMessage(
|
||||||
|
room: Room,
|
||||||
|
message: String,
|
||||||
|
numberOfMessages: Int,
|
||||||
|
rootThreadEventId: String,
|
||||||
|
timeout: Long = TestConstants.timeOutMillis): List<TimelineEvent> {
|
||||||
|
val timeline = room.createTimeline(null, TimelineSettings(10))
|
||||||
|
timeline.start()
|
||||||
|
val sentEvents = sendTextMessagesBatched(timeline, room, message, numberOfMessages, timeout, rootThreadEventId)
|
||||||
|
timeline.dispose()
|
||||||
|
// Check that all events has been created
|
||||||
|
assertEquals("Message number do not match $sentEvents", numberOfMessages.toLong(), sentEvents.size.toLong())
|
||||||
|
return sentEvents
|
||||||
|
}
|
||||||
|
|
||||||
// PRIVATE METHODS *****************************************************************************
|
// PRIVATE METHODS *****************************************************************************
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -0,0 +1,339 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.session.room.threads
|
||||||
|
|
||||||
|
import org.amshove.kluent.shouldBe
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.amshove.kluent.shouldBeFalse
|
||||||
|
import org.amshove.kluent.shouldBeNull
|
||||||
|
import org.amshove.kluent.shouldBeTrue
|
||||||
|
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.session.events.model.getRootThreadEventId
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.isTextMessage
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.isThread
|
||||||
|
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 java.util.concurrent.CountDownLatch
|
||||||
|
|
||||||
|
@RunWith(JUnit4::class)
|
||||||
|
@FixMethodOrder(MethodSorters.JVM)
|
||||||
|
class ThreadMessagingTest : InstrumentedTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun reply_in_thread_should_create_a_thread() {
|
||||||
|
val commonTestHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||||
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
|
||||||
|
|
||||||
|
val aliceSession = cryptoTestData.firstSession
|
||||||
|
val aliceRoomId = cryptoTestData.roomId
|
||||||
|
|
||||||
|
val aliceRoom = aliceSession.getRoom(aliceRoomId)!!
|
||||||
|
|
||||||
|
// Let's send a message in the normal timeline
|
||||||
|
val textMessage = "This is a normal timeline message"
|
||||||
|
val sentMessages = commonTestHelper.sendTextMessage(
|
||||||
|
room = aliceRoom,
|
||||||
|
message = textMessage,
|
||||||
|
nbOfMessages = 1)
|
||||||
|
|
||||||
|
val initMessage = sentMessages.first()
|
||||||
|
|
||||||
|
initMessage.root.isThread().shouldBeFalse()
|
||||||
|
initMessage.root.isTextMessage().shouldBeTrue()
|
||||||
|
initMessage.root.getRootThreadEventId().shouldBeNull()
|
||||||
|
initMessage.root.threadDetails?.isRootThread?.shouldBeFalse()
|
||||||
|
|
||||||
|
// Let's reply in timeline to that message
|
||||||
|
val repliesInThread = commonTestHelper.replyInThreadMessage(
|
||||||
|
room = aliceRoom,
|
||||||
|
message = "Reply In the above thread",
|
||||||
|
numberOfMessages = 1,
|
||||||
|
rootThreadEventId = initMessage.root.eventId.orEmpty())
|
||||||
|
|
||||||
|
val replyInThread = repliesInThread.first()
|
||||||
|
replyInThread.root.isThread().shouldBeTrue()
|
||||||
|
replyInThread.root.isTextMessage().shouldBeTrue()
|
||||||
|
replyInThread.root.getRootThreadEventId().shouldBeEqualTo(initMessage.root.eventId)
|
||||||
|
|
||||||
|
// The init normal message should now be a root thread event
|
||||||
|
val timeline = aliceRoom.createTimeline(null, TimelineSettings(30))
|
||||||
|
timeline.start()
|
||||||
|
|
||||||
|
aliceSession.startSync(true)
|
||||||
|
run {
|
||||||
|
val lock = CountDownLatch(1)
|
||||||
|
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||||
|
val initMessageThreadDetails = snapshot.firstOrNull {
|
||||||
|
it.root.eventId == initMessage.root.eventId
|
||||||
|
}?.root?.threadDetails
|
||||||
|
initMessageThreadDetails?.isRootThread?.shouldBeTrue()
|
||||||
|
initMessageThreadDetails?.numberOfThreads?.shouldBe(1)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
timeline.addListener(eventsListener)
|
||||||
|
commonTestHelper.await(lock, 600_000)
|
||||||
|
}
|
||||||
|
aliceSession.stopSync()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun reply_in_thread_should_create_a_thread_from_other_user() {
|
||||||
|
val commonTestHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||||
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
|
||||||
|
|
||||||
|
val aliceSession = cryptoTestData.firstSession
|
||||||
|
val aliceRoomId = cryptoTestData.roomId
|
||||||
|
val aliceRoom = aliceSession.getRoom(aliceRoomId)!!
|
||||||
|
|
||||||
|
// Let's send a message in the normal timeline
|
||||||
|
val textMessage = "This is a normal timeline message"
|
||||||
|
val sentMessages = commonTestHelper.sendTextMessage(
|
||||||
|
room = aliceRoom,
|
||||||
|
message = textMessage,
|
||||||
|
nbOfMessages = 1)
|
||||||
|
|
||||||
|
val initMessage = sentMessages.first()
|
||||||
|
|
||||||
|
initMessage.root.isThread().shouldBeFalse()
|
||||||
|
initMessage.root.isTextMessage().shouldBeTrue()
|
||||||
|
initMessage.root.getRootThreadEventId().shouldBeNull()
|
||||||
|
initMessage.root.threadDetails?.isRootThread?.shouldBeFalse()
|
||||||
|
|
||||||
|
// Let's reply in timeline to that message from another user
|
||||||
|
val bobSession = cryptoTestData.secondSession!!
|
||||||
|
val bobRoomId = cryptoTestData.roomId
|
||||||
|
val bobRoom = bobSession.getRoom(bobRoomId)!!
|
||||||
|
|
||||||
|
val repliesInThread = commonTestHelper.replyInThreadMessage(
|
||||||
|
room = bobRoom,
|
||||||
|
message = "Reply In the above thread",
|
||||||
|
numberOfMessages = 1,
|
||||||
|
rootThreadEventId = initMessage.root.eventId.orEmpty())
|
||||||
|
|
||||||
|
val replyInThread = repliesInThread.first()
|
||||||
|
replyInThread.root.isThread().shouldBeTrue()
|
||||||
|
replyInThread.root.isTextMessage().shouldBeTrue()
|
||||||
|
replyInThread.root.getRootThreadEventId().shouldBeEqualTo(initMessage.root.eventId)
|
||||||
|
|
||||||
|
// The init normal message should now be a root thread event
|
||||||
|
val timeline = aliceRoom.createTimeline(null, TimelineSettings(30))
|
||||||
|
timeline.start()
|
||||||
|
|
||||||
|
aliceSession.startSync(true)
|
||||||
|
run {
|
||||||
|
val lock = CountDownLatch(1)
|
||||||
|
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||||
|
val initMessageThreadDetails = snapshot.firstOrNull { it.root.eventId == initMessage.root.eventId }?.root?.threadDetails
|
||||||
|
initMessageThreadDetails?.isRootThread?.shouldBeTrue()
|
||||||
|
initMessageThreadDetails?.numberOfThreads?.shouldBe(1)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
timeline.addListener(eventsListener)
|
||||||
|
commonTestHelper.await(lock, 600_000)
|
||||||
|
}
|
||||||
|
aliceSession.stopSync()
|
||||||
|
|
||||||
|
bobSession.startSync(true)
|
||||||
|
run {
|
||||||
|
val lock = CountDownLatch(1)
|
||||||
|
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||||
|
val initMessageThreadDetails = snapshot.firstOrNull { it.root.eventId == initMessage.root.eventId }?.root?.threadDetails
|
||||||
|
initMessageThreadDetails?.isRootThread?.shouldBeTrue()
|
||||||
|
initMessageThreadDetails?.numberOfThreads?.shouldBe(1)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
timeline.addListener(eventsListener)
|
||||||
|
commonTestHelper.await(lock, 600_000)
|
||||||
|
}
|
||||||
|
bobSession.stopSync()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun reply_in_thread_to_timeline_message_multiple_times() {
|
||||||
|
val commonTestHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||||
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
|
||||||
|
|
||||||
|
val aliceSession = cryptoTestData.firstSession
|
||||||
|
val aliceRoomId = cryptoTestData.roomId
|
||||||
|
|
||||||
|
val aliceRoom = aliceSession.getRoom(aliceRoomId)!!
|
||||||
|
|
||||||
|
// Let's send 5 messages in the normal timeline
|
||||||
|
val textMessage = "This is a normal timeline message"
|
||||||
|
val sentMessages = commonTestHelper.sendTextMessage(
|
||||||
|
room = aliceRoom,
|
||||||
|
message = textMessage,
|
||||||
|
nbOfMessages = 5)
|
||||||
|
|
||||||
|
sentMessages.forEach {
|
||||||
|
it.root.isThread().shouldBeFalse()
|
||||||
|
it.root.isTextMessage().shouldBeTrue()
|
||||||
|
it.root.getRootThreadEventId().shouldBeNull()
|
||||||
|
it.root.threadDetails?.isRootThread?.shouldBeFalse()
|
||||||
|
}
|
||||||
|
// let's start the thread from the second message
|
||||||
|
val selectedInitMessage = sentMessages[1]
|
||||||
|
|
||||||
|
// Let's reply 40 times in the timeline to the second message
|
||||||
|
val repliesInThread = commonTestHelper.replyInThreadMessage(
|
||||||
|
room = aliceRoom,
|
||||||
|
message = "Reply In the above thread",
|
||||||
|
numberOfMessages = 40,
|
||||||
|
rootThreadEventId = selectedInitMessage.root.eventId.orEmpty())
|
||||||
|
|
||||||
|
repliesInThread.forEach {
|
||||||
|
it.root.isThread().shouldBeTrue()
|
||||||
|
it.root.isTextMessage().shouldBeTrue()
|
||||||
|
it.root.getRootThreadEventId()?.shouldBeEqualTo(selectedInitMessage.root.eventId.orEmpty()) ?: assert(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The init normal message should now be a root thread event
|
||||||
|
val timeline = aliceRoom.createTimeline(null, TimelineSettings(30))
|
||||||
|
timeline.start()
|
||||||
|
|
||||||
|
aliceSession.startSync(true)
|
||||||
|
run {
|
||||||
|
val lock = CountDownLatch(1)
|
||||||
|
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||||
|
val initMessageThreadDetails = snapshot.firstOrNull { it.root.eventId == selectedInitMessage.root.eventId }?.root?.threadDetails
|
||||||
|
// Selected init message should be the thread root
|
||||||
|
initMessageThreadDetails?.isRootThread?.shouldBeTrue()
|
||||||
|
// All threads should be 40
|
||||||
|
initMessageThreadDetails?.numberOfThreads?.shouldBeEqualTo(40)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
// Because we sent more than 30 messages we should paginate a bit more
|
||||||
|
timeline.paginate(Timeline.Direction.BACKWARDS, 50)
|
||||||
|
timeline.addListener(eventsListener)
|
||||||
|
commonTestHelper.await(lock, 600_000)
|
||||||
|
}
|
||||||
|
aliceSession.stopSync()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun thread_summary_advanced_validation_after_multiple_messages_in_multiple_threads() {
|
||||||
|
val commonTestHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||||
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
|
||||||
|
|
||||||
|
val aliceSession = cryptoTestData.firstSession
|
||||||
|
val aliceRoomId = cryptoTestData.roomId
|
||||||
|
|
||||||
|
val aliceRoom = aliceSession.getRoom(aliceRoomId)!!
|
||||||
|
|
||||||
|
// Let's send 5 messages in the normal timeline
|
||||||
|
val textMessage = "This is a normal timeline message"
|
||||||
|
val sentMessages = commonTestHelper.sendTextMessage(
|
||||||
|
room = aliceRoom,
|
||||||
|
message = textMessage,
|
||||||
|
nbOfMessages = 5)
|
||||||
|
|
||||||
|
sentMessages.forEach {
|
||||||
|
it.root.isThread().shouldBeFalse()
|
||||||
|
it.root.isTextMessage().shouldBeTrue()
|
||||||
|
it.root.getRootThreadEventId().shouldBeNull()
|
||||||
|
it.root.threadDetails?.isRootThread?.shouldBeFalse()
|
||||||
|
}
|
||||||
|
// let's start the thread from the second message
|
||||||
|
val firstMessage = sentMessages[0]
|
||||||
|
val secondMessage = sentMessages[1]
|
||||||
|
|
||||||
|
// Alice will reply in thread to the second message 35 times
|
||||||
|
val aliceThreadRepliesInSecondMessage = commonTestHelper.replyInThreadMessage(
|
||||||
|
room = aliceRoom,
|
||||||
|
message = "Alice reply In the above second thread message",
|
||||||
|
numberOfMessages = 35,
|
||||||
|
rootThreadEventId = secondMessage.root.eventId.orEmpty())
|
||||||
|
|
||||||
|
// Let's reply in timeline to that message from another user
|
||||||
|
val bobSession = cryptoTestData.secondSession!!
|
||||||
|
val bobRoomId = cryptoTestData.roomId
|
||||||
|
val bobRoom = bobSession.getRoom(bobRoomId)!!
|
||||||
|
|
||||||
|
// Bob will reply in thread to the first message 35 times
|
||||||
|
val bobThreadRepliesInFirstMessage = commonTestHelper.replyInThreadMessage(
|
||||||
|
room = bobRoom,
|
||||||
|
message = "Bob reply In the above first thread message",
|
||||||
|
numberOfMessages = 42,
|
||||||
|
rootThreadEventId = firstMessage.root.eventId.orEmpty())
|
||||||
|
|
||||||
|
// Bob will also reply in second thread 5 times
|
||||||
|
val bobThreadRepliesInSecondMessage = commonTestHelper.replyInThreadMessage(
|
||||||
|
room = bobRoom,
|
||||||
|
message = "Another Bob reply In the above second thread message",
|
||||||
|
numberOfMessages = 20,
|
||||||
|
rootThreadEventId = secondMessage.root.eventId.orEmpty())
|
||||||
|
|
||||||
|
aliceThreadRepliesInSecondMessage.forEach {
|
||||||
|
it.root.isThread().shouldBeTrue()
|
||||||
|
it.root.isTextMessage().shouldBeTrue()
|
||||||
|
it.root.getRootThreadEventId()?.shouldBeEqualTo(secondMessage.root.eventId.orEmpty()) ?: assert(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
bobThreadRepliesInFirstMessage.forEach {
|
||||||
|
it.root.isThread().shouldBeTrue()
|
||||||
|
it.root.isTextMessage().shouldBeTrue()
|
||||||
|
it.root.getRootThreadEventId()?.shouldBeEqualTo(firstMessage.root.eventId.orEmpty()) ?: assert(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
bobThreadRepliesInSecondMessage.forEach {
|
||||||
|
it.root.isThread().shouldBeTrue()
|
||||||
|
it.root.isTextMessage().shouldBeTrue()
|
||||||
|
it.root.getRootThreadEventId()?.shouldBeEqualTo(secondMessage.root.eventId.orEmpty()) ?: assert(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The init normal message should now be a root thread event
|
||||||
|
val timeline = aliceRoom.createTimeline(null, TimelineSettings(30))
|
||||||
|
timeline.start()
|
||||||
|
|
||||||
|
aliceSession.startSync(true)
|
||||||
|
run {
|
||||||
|
val lock = CountDownLatch(1)
|
||||||
|
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||||
|
val firstMessageThreadDetails = snapshot.firstOrNull { it.root.eventId == firstMessage.root.eventId }?.root?.threadDetails
|
||||||
|
val secondMessageThreadDetails = snapshot.firstOrNull { it.root.eventId == secondMessage.root.eventId }?.root?.threadDetails
|
||||||
|
|
||||||
|
// first & second message should be the thread root
|
||||||
|
firstMessageThreadDetails?.isRootThread?.shouldBeTrue()
|
||||||
|
secondMessageThreadDetails?.isRootThread?.shouldBeTrue()
|
||||||
|
|
||||||
|
// First thread message should contain 42
|
||||||
|
firstMessageThreadDetails?.numberOfThreads shouldBeEqualTo 42
|
||||||
|
// Second thread message should contain 35+20
|
||||||
|
secondMessageThreadDetails?.numberOfThreads shouldBeEqualTo 55
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
// Because we sent more than 30 messages we should paginate a bit more
|
||||||
|
timeline.paginate(Timeline.Direction.BACKWARDS, 50)
|
||||||
|
timeline.paginate(Timeline.Direction.BACKWARDS, 50)
|
||||||
|
timeline.addListener(eventsListener)
|
||||||
|
commonTestHelper.await(lock, 600_000)
|
||||||
|
}
|
||||||
|
aliceSession.stopSync()
|
||||||
|
}
|
||||||
|
}
|
@ -25,9 +25,14 @@ import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
|||||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.relation.shouldRenderInThread
|
||||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||||
|
import org.matrix.android.sdk.api.session.threads.ThreadDetails
|
||||||
|
import org.matrix.android.sdk.api.util.ContentUtils
|
||||||
import org.matrix.android.sdk.api.util.JsonDict
|
import org.matrix.android.sdk.api.util.JsonDict
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
|
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||||
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
||||||
@ -98,6 +103,9 @@ data class Event(
|
|||||||
@Transient
|
@Transient
|
||||||
var sendStateDetails: String? = null
|
var sendStateDetails: String? = null
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
var threadDetails: ThreadDetails? = null
|
||||||
|
|
||||||
fun sendStateError(): MatrixError? {
|
fun sendStateError(): MatrixError? {
|
||||||
return sendStateDetails?.let {
|
return sendStateDetails?.let {
|
||||||
val matrixErrorAdapter = MoshiProvider.providesMoshi().adapter(MatrixError::class.java)
|
val matrixErrorAdapter = MoshiProvider.providesMoshi().adapter(MatrixError::class.java)
|
||||||
@ -123,6 +131,7 @@ data class Event(
|
|||||||
it.mCryptoErrorReason = mCryptoErrorReason
|
it.mCryptoErrorReason = mCryptoErrorReason
|
||||||
it.sendState = sendState
|
it.sendState = sendState
|
||||||
it.ageLocalTs = ageLocalTs
|
it.ageLocalTs = ageLocalTs
|
||||||
|
it.threadDetails = threadDetails
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,6 +194,51 @@ data class Event(
|
|||||||
return contentMap?.let { JSONObject(adapter.toJson(it)).toString(4) }
|
return contentMap?.let { JSONObject(adapter.toJson(it)).toString(4) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a user friendly content depending on the message type.
|
||||||
|
* It can be used especially for message summaries.
|
||||||
|
* It will return a decrypted text message or an empty string otherwise.
|
||||||
|
*/
|
||||||
|
fun getDecryptedTextSummary(): String? {
|
||||||
|
if (isRedacted()) return "Message Deleted"
|
||||||
|
val text = getDecryptedValue() ?: return null
|
||||||
|
return when {
|
||||||
|
isReplyRenderedInThread() || isQuote() -> ContentUtils.extractUsefulTextFromReply(text)
|
||||||
|
isFileMessage() -> "sent a file."
|
||||||
|
isAudioMessage() -> "sent an audio file."
|
||||||
|
isImageMessage() -> "sent an image."
|
||||||
|
isVideoMessage() -> "sent a video."
|
||||||
|
isSticker() -> "sent a sticker"
|
||||||
|
isPoll() -> getPollQuestion() ?: "created a poll."
|
||||||
|
else -> text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Event.isQuote(): Boolean {
|
||||||
|
if (isReplyRenderedInThread()) return false
|
||||||
|
return getDecryptedValue("formatted_body")?.contains("<blockquote>") ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether or not current event has mentioned the user
|
||||||
|
*/
|
||||||
|
fun isUserMentioned(userId: String): Boolean {
|
||||||
|
return getDecryptedValue("formatted_body")?.contains(userId) ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt the message, or return the pure payload value if there is no encryption
|
||||||
|
*/
|
||||||
|
private fun getDecryptedValue(key: String = "body"): String? {
|
||||||
|
return if (isEncrypted()) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val decryptedContent = mxDecryptionResult?.payload?.get("content") as? JsonDict
|
||||||
|
decryptedContent?.get(key) as? String
|
||||||
|
} else {
|
||||||
|
content?.get(key) as? String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tells if the event is redacted
|
* Tells if the event is redacted
|
||||||
*/
|
*/
|
||||||
@ -217,7 +271,7 @@ data class Event(
|
|||||||
if (mCryptoError != other.mCryptoError) return false
|
if (mCryptoError != other.mCryptoError) return false
|
||||||
if (mCryptoErrorReason != other.mCryptoErrorReason) return false
|
if (mCryptoErrorReason != other.mCryptoErrorReason) return false
|
||||||
if (sendState != other.sendState) return false
|
if (sendState != other.sendState) return false
|
||||||
|
if (threadDetails != other.threadDetails) return false
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,6 +290,8 @@ data class Event(
|
|||||||
result = 31 * result + (mCryptoError?.hashCode() ?: 0)
|
result = 31 * result + (mCryptoError?.hashCode() ?: 0)
|
||||||
result = 31 * result + (mCryptoErrorReason?.hashCode() ?: 0)
|
result = 31 * result + (mCryptoErrorReason?.hashCode() ?: 0)
|
||||||
result = 31 * result + sendState.hashCode()
|
result = 31 * result + sendState.hashCode()
|
||||||
|
result = 31 * result + threadDetails.hashCode()
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -243,70 +299,101 @@ data class Event(
|
|||||||
fun Event.isTextMessage(): Boolean {
|
fun Event.isTextMessage(): Boolean {
|
||||||
return getClearType() == EventType.MESSAGE &&
|
return getClearType() == EventType.MESSAGE &&
|
||||||
when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
|
when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
|
||||||
MessageType.MSGTYPE_TEXT,
|
MessageType.MSGTYPE_TEXT,
|
||||||
MessageType.MSGTYPE_EMOTE,
|
MessageType.MSGTYPE_EMOTE,
|
||||||
MessageType.MSGTYPE_NOTICE -> true
|
MessageType.MSGTYPE_NOTICE -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Event.isImageMessage(): Boolean {
|
fun Event.isImageMessage(): Boolean {
|
||||||
return getClearType() == EventType.MESSAGE &&
|
return getClearType() == EventType.MESSAGE &&
|
||||||
when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
|
when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
|
||||||
MessageType.MSGTYPE_IMAGE -> true
|
MessageType.MSGTYPE_IMAGE -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Event.isVideoMessage(): Boolean {
|
fun Event.isVideoMessage(): Boolean {
|
||||||
return getClearType() == EventType.MESSAGE &&
|
return getClearType() == EventType.MESSAGE &&
|
||||||
when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
|
when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
|
||||||
MessageType.MSGTYPE_VIDEO -> true
|
MessageType.MSGTYPE_VIDEO -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Event.isAudioMessage(): Boolean {
|
fun Event.isAudioMessage(): Boolean {
|
||||||
return getClearType() == EventType.MESSAGE &&
|
return getClearType() == EventType.MESSAGE &&
|
||||||
when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
|
when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
|
||||||
MessageType.MSGTYPE_AUDIO -> true
|
MessageType.MSGTYPE_AUDIO -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Event.isFileMessage(): Boolean {
|
fun Event.isFileMessage(): Boolean {
|
||||||
return getClearType() == EventType.MESSAGE &&
|
return getClearType() == EventType.MESSAGE &&
|
||||||
when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
|
when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
|
||||||
MessageType.MSGTYPE_FILE -> true
|
MessageType.MSGTYPE_FILE -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Event.isAttachmentMessage(): Boolean {
|
fun Event.isAttachmentMessage(): Boolean {
|
||||||
return getClearType() == EventType.MESSAGE &&
|
return getClearType() == EventType.MESSAGE &&
|
||||||
when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
|
when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
|
||||||
MessageType.MSGTYPE_IMAGE,
|
MessageType.MSGTYPE_IMAGE,
|
||||||
MessageType.MSGTYPE_AUDIO,
|
MessageType.MSGTYPE_AUDIO,
|
||||||
MessageType.MSGTYPE_VIDEO,
|
MessageType.MSGTYPE_VIDEO,
|
||||||
MessageType.MSGTYPE_FILE -> true
|
MessageType.MSGTYPE_FILE -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Event.isPoll(): Boolean = getClearType() == EventType.POLL_START || getClearType() == EventType.POLL_END
|
||||||
|
|
||||||
|
fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER
|
||||||
|
|
||||||
fun Event.getRelationContent(): RelationDefaultContent? {
|
fun Event.getRelationContent(): RelationDefaultContent? {
|
||||||
return if (isEncrypted()) {
|
return if (isEncrypted()) {
|
||||||
content.toModel<EncryptedEventContent>()?.relatesTo
|
content.toModel<EncryptedEventContent>()?.relatesTo
|
||||||
} else {
|
} else {
|
||||||
content.toModel<MessageContent>()?.relatesTo
|
content.toModel<MessageContent>()?.relatesTo ?: run {
|
||||||
|
// Special case to handle stickers, while there is only a local msgtype for stickers
|
||||||
|
if (getClearType() == EventType.STICKER) {
|
||||||
|
getClearContent().toModel<MessageStickerContent>()?.relatesTo
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the poll question or null otherwise
|
||||||
|
*/
|
||||||
|
fun Event.getPollQuestion(): String? =
|
||||||
|
getPollContent()?.pollCreationInfo?.question?.question
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the relation content for a specific type or null otherwise
|
||||||
|
*/
|
||||||
|
fun Event.getRelationContentForType(type: String): RelationDefaultContent? =
|
||||||
|
getRelationContent()?.takeIf { it.type == type }
|
||||||
|
|
||||||
fun Event.isReply(): Boolean {
|
fun Event.isReply(): Boolean {
|
||||||
return getRelationContent()?.inReplyTo?.eventId != null
|
return getRelationContent()?.inReplyTo?.eventId != null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Event.isReplyRenderedInThread(): Boolean {
|
||||||
|
return isReply() && getRelationContent()?.inReplyTo?.shouldRenderInThread() == true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Event.isThread(): Boolean = getRelationContentForType(RelationType.IO_THREAD)?.eventId != null
|
||||||
|
|
||||||
|
fun Event.getRootThreadEventId(): String? = getRelationContentForType(RelationType.IO_THREAD)?.eventId
|
||||||
|
|
||||||
fun Event.isEdition(): Boolean {
|
fun Event.isEdition(): Boolean {
|
||||||
return getRelationContent()?.takeIf { it.type == RelationType.REPLACE }?.eventId != null
|
return getRelationContentForType(RelationType.REPLACE)?.eventId != null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Event.getPresenceContent(): PresenceContent? {
|
fun Event.getPresenceContent(): PresenceContent? {
|
||||||
@ -315,3 +402,7 @@ fun Event.getPresenceContent(): PresenceContent? {
|
|||||||
|
|
||||||
fun Event.isInvitation(): Boolean = type == EventType.STATE_ROOM_MEMBER &&
|
fun Event.isInvitation(): Boolean = type == EventType.STATE_ROOM_MEMBER &&
|
||||||
content?.toModel<RoomMemberContent>()?.membership == Membership.INVITE
|
content?.toModel<RoomMemberContent>()?.membership == Membership.INVITE
|
||||||
|
|
||||||
|
fun Event.getPollContent(): MessagePollContent? {
|
||||||
|
return content.toModel<MessagePollContent>()
|
||||||
|
}
|
||||||
|
@ -28,9 +28,9 @@ object RelationType {
|
|||||||
/** Lets you define an event which references an existing event.*/
|
/** Lets you define an event which references an existing event.*/
|
||||||
const val REFERENCE = "m.reference"
|
const val REFERENCE = "m.reference"
|
||||||
|
|
||||||
/** Lets you define an thread event that belongs to another existing event.*/
|
/** Lets you define an event which is a thread reply to an existing event.*/
|
||||||
// const val THREAD = "m.thread" // m.thread is not yet released in the backend
|
const val THREAD = "m.thread"
|
||||||
const val THREAD = "io.element.thread" // io.element.thread will be replaced by m.thread when it is released
|
const val IO_THREAD = "io.element.thread"
|
||||||
|
|
||||||
/** Lets you define an event which adds a response to an existing event.*/
|
/** Lets you define an event which adds a response to an existing event.*/
|
||||||
const val RESPONSE = "org.matrix.response"
|
const val RESPONSE = "org.matrix.response"
|
||||||
|
@ -47,5 +47,9 @@ data class PreviewUrlData(
|
|||||||
// Value of field "og:description"
|
// Value of field "og:description"
|
||||||
val description: String?,
|
val description: String?,
|
||||||
// Value of field "og:image"
|
// Value of field "og:image"
|
||||||
val mxcUrl: String?
|
val mxcUrl: String?,
|
||||||
|
// Value of field "og:image:width"
|
||||||
|
val imageWidth: Int?,
|
||||||
|
// Value of field "og:image:height"
|
||||||
|
val imageHeight: Int?
|
||||||
)
|
)
|
||||||
|
@ -32,6 +32,7 @@ import org.matrix.android.sdk.api.session.room.send.DraftService
|
|||||||
import org.matrix.android.sdk.api.session.room.send.SendService
|
import org.matrix.android.sdk.api.session.room.send.SendService
|
||||||
import org.matrix.android.sdk.api.session.room.state.StateService
|
import org.matrix.android.sdk.api.session.room.state.StateService
|
||||||
import org.matrix.android.sdk.api.session.room.tags.TagsService
|
import org.matrix.android.sdk.api.session.room.tags.TagsService
|
||||||
|
import org.matrix.android.sdk.api.session.room.threads.ThreadsService
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineService
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineService
|
||||||
import org.matrix.android.sdk.api.session.room.typing.TypingService
|
import org.matrix.android.sdk.api.session.room.typing.TypingService
|
||||||
import org.matrix.android.sdk.api.session.room.uploads.UploadsService
|
import org.matrix.android.sdk.api.session.room.uploads.UploadsService
|
||||||
@ -45,6 +46,7 @@ import org.matrix.android.sdk.api.util.Optional
|
|||||||
*/
|
*/
|
||||||
interface Room :
|
interface Room :
|
||||||
TimelineService,
|
TimelineService,
|
||||||
|
ThreadsService,
|
||||||
SendService,
|
SendService,
|
||||||
DraftService,
|
DraftService,
|
||||||
ReadService,
|
ReadService,
|
||||||
|
@ -64,4 +64,12 @@ data class MessageLocationContent(
|
|||||||
) : MessageContent {
|
) : MessageContent {
|
||||||
|
|
||||||
fun getBestGeoUri() = locationInfo?.geoUri ?: geoUri
|
fun getBestGeoUri() = locationInfo?.geoUri ?: geoUri
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if the location asset is a user location, not a generic one.
|
||||||
|
*/
|
||||||
|
fun isSelfLocation(): Boolean {
|
||||||
|
// Should behave like m.self if locationAsset is null
|
||||||
|
return locationAsset?.type == null || locationAsset.type == LocationAssetType.SELF
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.room.model.relation
|
|||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
|
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.PollType
|
import org.matrix.android.sdk.api.session.room.model.message.PollType
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
import org.matrix.android.sdk.api.util.Cancelable
|
import org.matrix.android.sdk.api.util.Cancelable
|
||||||
@ -45,6 +46,9 @@ import org.matrix.android.sdk.api.util.Optional
|
|||||||
* m.reference - lets you define an event which references an existing event.
|
* m.reference - lets you define an event which references an existing event.
|
||||||
* When aggregated, currently doesn't do anything special, but in future could bundle chains of references (i.e. threads).
|
* When aggregated, currently doesn't do anything special, but in future could bundle chains of references (i.e. threads).
|
||||||
* These are primarily intended for handling replies (and in future threads).
|
* These are primarily intended for handling replies (and in future threads).
|
||||||
|
*
|
||||||
|
* m.thread - lets you define an event which is a thread reply to an existing event.
|
||||||
|
* When aggregated, returns the most thread event
|
||||||
*/
|
*/
|
||||||
interface RelationService {
|
interface RelationService {
|
||||||
|
|
||||||
@ -62,8 +66,8 @@ interface RelationService {
|
|||||||
* @param targetEventId the id of the event being reacted
|
* @param targetEventId the id of the event being reacted
|
||||||
* @param reaction the reaction (preferably emoji)
|
* @param reaction the reaction (preferably emoji)
|
||||||
*/
|
*/
|
||||||
fun undoReaction(targetEventId: String,
|
suspend fun undoReaction(targetEventId: String,
|
||||||
reaction: String): Cancelable
|
reaction: String): Cancelable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Edit a poll.
|
* Edit a poll.
|
||||||
@ -118,10 +122,15 @@ interface RelationService {
|
|||||||
* @param eventReplied the event referenced by the reply
|
* @param eventReplied the event referenced by the reply
|
||||||
* @param replyText the reply text
|
* @param replyText the reply text
|
||||||
* @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
|
* @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
|
||||||
|
* @param showInThread If true, relation will be added to the reply in order to be visible from within threads
|
||||||
|
* @param rootThreadEventId If show in thread is true then we need the rootThreadEventId to generate the relation
|
||||||
*/
|
*/
|
||||||
fun replyToMessage(eventReplied: TimelineEvent,
|
fun replyToMessage(eventReplied: TimelineEvent,
|
||||||
replyText: CharSequence,
|
replyText: CharSequence,
|
||||||
autoMarkdown: Boolean = false): Cancelable?
|
autoMarkdown: Boolean = false,
|
||||||
|
showInThread: Boolean = false,
|
||||||
|
rootThreadEventId: String? = null
|
||||||
|
): Cancelable?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current EventAnnotationsSummary
|
* Get the current EventAnnotationsSummary
|
||||||
@ -136,4 +145,31 @@ interface RelationService {
|
|||||||
* @return the LiveData of EventAnnotationsSummary
|
* @return the LiveData of EventAnnotationsSummary
|
||||||
*/
|
*/
|
||||||
fun getEventAnnotationsSummaryLive(eventId: String): LiveData<Optional<EventAnnotationsSummary>>
|
fun getEventAnnotationsSummaryLive(eventId: String): LiveData<Optional<EventAnnotationsSummary>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a thread reply for an existing timeline event
|
||||||
|
* The replyInThreadText can be a Spannable and contains special spans (MatrixItemSpan) that will be translated
|
||||||
|
* by the sdk into pills.
|
||||||
|
* @param rootThreadEventId the root thread eventId
|
||||||
|
* @param replyInThreadText the reply text
|
||||||
|
* @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE
|
||||||
|
* @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML
|
||||||
|
* @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
|
||||||
|
* @param eventReplied the event referenced by the reply within a thread
|
||||||
|
*/
|
||||||
|
fun replyInThread(rootThreadEventId: String,
|
||||||
|
replyInThreadText: CharSequence,
|
||||||
|
msgType: String = MessageType.MSGTYPE_TEXT,
|
||||||
|
autoMarkdown: Boolean = false,
|
||||||
|
formattedText: String? = null,
|
||||||
|
eventReplied: TimelineEvent? = null): Cancelable?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all the thread replies for the specified rootThreadEventId
|
||||||
|
* The return list will contain the original root thread event and all the thread replies to that event
|
||||||
|
* Note: We will use a large limit value in order to avoid using pagination until it would be 100% ready
|
||||||
|
* from the backend
|
||||||
|
* @param rootThreadEventId the root thread eventId
|
||||||
|
*/
|
||||||
|
suspend fun fetchThreadTimeline(rootThreadEventId: String): Boolean
|
||||||
}
|
}
|
||||||
|
@ -21,5 +21,8 @@ import com.squareup.moshi.JsonClass
|
|||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class ReplyToContent(
|
data class ReplyToContent(
|
||||||
@Json(name = "event_id") val eventId: String? = null
|
@Json(name = "event_id") val eventId: String? = null,
|
||||||
|
@Json(name = "render_in") val renderIn: List<String>? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun ReplyToContent.shouldRenderInThread(): Boolean = renderIn?.contains("m.thread") == true
|
||||||
|
@ -64,7 +64,7 @@ interface SendService {
|
|||||||
* @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
|
* @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
|
||||||
* @return a [Cancelable]
|
* @return a [Cancelable]
|
||||||
*/
|
*/
|
||||||
fun sendQuotedTextMessage(quotedEvent: TimelineEvent, text: String, autoMarkdown: Boolean): Cancelable
|
fun sendQuotedTextMessage(quotedEvent: TimelineEvent, text: String, autoMarkdown: Boolean, rootThreadEventId: String? = null): Cancelable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to send a media asynchronously.
|
* Method to send a media asynchronously.
|
||||||
@ -72,11 +72,13 @@ interface SendService {
|
|||||||
* @param compressBeforeSending set to true to compress images before sending them
|
* @param compressBeforeSending set to true to compress images before sending them
|
||||||
* @param roomIds set of roomIds to where the media will be sent. The current roomId will be add to this set if not present.
|
* @param roomIds set of roomIds to where the media will be sent. The current roomId will be add to this set if not present.
|
||||||
* It can be useful to send media to multiple room. It's safe to include the current roomId in this set
|
* It can be useful to send media to multiple room. It's safe to include the current roomId in this set
|
||||||
|
* @param rootThreadEventId when this param is not null, the Media will be sent in this specific thread
|
||||||
* @return a [Cancelable]
|
* @return a [Cancelable]
|
||||||
*/
|
*/
|
||||||
fun sendMedia(attachment: ContentAttachmentData,
|
fun sendMedia(attachment: ContentAttachmentData,
|
||||||
compressBeforeSending: Boolean,
|
compressBeforeSending: Boolean,
|
||||||
roomIds: Set<String>): Cancelable
|
roomIds: Set<String>,
|
||||||
|
rootThreadEventId: String? = null): Cancelable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to send a list of media asynchronously.
|
* Method to send a list of media asynchronously.
|
||||||
@ -84,11 +86,13 @@ interface SendService {
|
|||||||
* @param compressBeforeSending set to true to compress images before sending them
|
* @param compressBeforeSending set to true to compress images before sending them
|
||||||
* @param roomIds set of roomIds to where the media will be sent. The current roomId will be add to this set if not present.
|
* @param roomIds set of roomIds to where the media will be sent. The current roomId will be add to this set if not present.
|
||||||
* It can be useful to send media to multiple room. It's safe to include the current roomId in this set
|
* It can be useful to send media to multiple room. It's safe to include the current roomId in this set
|
||||||
|
* @param rootThreadEventId when this param is not null, all the Media will be sent in this specific thread
|
||||||
* @return a [Cancelable]
|
* @return a [Cancelable]
|
||||||
*/
|
*/
|
||||||
fun sendMedias(attachments: List<ContentAttachmentData>,
|
fun sendMedias(attachments: List<ContentAttachmentData>,
|
||||||
compressBeforeSending: Boolean,
|
compressBeforeSending: Boolean,
|
||||||
roomIds: Set<String>): Cancelable
|
roomIds: Set<String>,
|
||||||
|
rootThreadEventId: String? = null): Cancelable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a poll to the room.
|
* Send a poll to the room.
|
||||||
|
@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.api.session.room.threads
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This interface defines methods to interact with threads related features.
|
||||||
|
* It's implemented at the room level within the main timeline.
|
||||||
|
*/
|
||||||
|
interface ThreadsService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a [LiveData] list of all the thread root TimelineEvents that exists at the room level
|
||||||
|
*/
|
||||||
|
fun getAllThreadsLive(): LiveData<List<TimelineEvent>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of all the thread root TimelineEvents that exists at the room level
|
||||||
|
*/
|
||||||
|
fun getAllThreads(): List<TimelineEvent>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a [LiveData] list of all the marked unread threads that exists at the room level
|
||||||
|
*/
|
||||||
|
fun getMarkedThreadNotificationsLive(): LiveData<List<TimelineEvent>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of all the marked unread threads that exists at the room level
|
||||||
|
*/
|
||||||
|
fun getMarkedThreadNotifications(): List<TimelineEvent>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether or not the current user is participating in the thread
|
||||||
|
* @param rootThreadEventId the eventId of the current thread
|
||||||
|
*/
|
||||||
|
fun isUserParticipatingInThread(rootThreadEventId: String): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enhance the provided root thread TimelineEvent [List] by adding the latest
|
||||||
|
* message edition for that thread
|
||||||
|
* @return the enhanced [List] with edited updates
|
||||||
|
*/
|
||||||
|
fun mapEventsWithEdition(threads: List<TimelineEvent>): List<TimelineEvent>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the current thread as read in local DB.
|
||||||
|
* note: read receipts within threads are not yet supported with the API
|
||||||
|
* @param rootThreadEventId the root eventId of the current thread
|
||||||
|
*/
|
||||||
|
suspend fun markThreadAsRead(rootThreadEventId: String)
|
||||||
|
}
|
@ -43,7 +43,7 @@ interface Timeline {
|
|||||||
/**
|
/**
|
||||||
* This must be called before any other method after creating the timeline. It ensures the underlying database is open
|
* This must be called before any other method after creating the timeline. It ensures the underlying database is open
|
||||||
*/
|
*/
|
||||||
fun start()
|
fun start(rootThreadEventId: String? = null)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This must be called when you don't need the timeline. It ensures the underlying database get closed.
|
* This must be called when you don't need the timeline. It ensures the underlying database get closed.
|
||||||
|
@ -22,7 +22,9 @@ import org.matrix.android.sdk.api.session.events.model.EventType
|
|||||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||||
import org.matrix.android.sdk.api.session.events.model.getRelationContent
|
import org.matrix.android.sdk.api.session.events.model.getRelationContent
|
||||||
import org.matrix.android.sdk.api.session.events.model.isEdition
|
import org.matrix.android.sdk.api.session.events.model.isEdition
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.isPoll
|
||||||
import org.matrix.android.sdk.api.session.events.model.isReply
|
import org.matrix.android.sdk.api.session.events.model.isReply
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.isSticker
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
|
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
|
||||||
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
|
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
|
||||||
@ -149,6 +151,13 @@ fun TimelineEvent.isEdition(): Boolean {
|
|||||||
return root.isEdition()
|
return root.isEdition()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun TimelineEvent.isPoll(): Boolean =
|
||||||
|
root.isPoll()
|
||||||
|
|
||||||
|
fun TimelineEvent.isSticker(): Boolean {
|
||||||
|
return root.isSticker()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the latest message body, after a possible edition, stripping the reply prefix if necessary
|
* Get the latest message body, after a possible edition, stripping the reply prefix if necessary
|
||||||
*/
|
*/
|
||||||
|
@ -27,5 +27,14 @@ data class TimelineSettings(
|
|||||||
/**
|
/**
|
||||||
* If true, will build read receipts for each event.
|
* If true, will build read receipts for each event.
|
||||||
*/
|
*/
|
||||||
val buildReadReceipts: Boolean = true
|
val buildReadReceipts: Boolean = true,
|
||||||
)
|
/**
|
||||||
|
* The root thread eventId if this is a thread timeline, or null if this is NOT a thread timeline
|
||||||
|
*/
|
||||||
|
val rootThreadEventId: String? = null) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if this is a thread timeline or false otherwise
|
||||||
|
*/
|
||||||
|
fun isThreadTimeline() = rootThreadEventId != null
|
||||||
|
}
|
||||||
|
@ -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.api.session.threads
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class contains all the details needed for threads.
|
||||||
|
* Is is mainly used from within an Event.
|
||||||
|
*/
|
||||||
|
data class ThreadDetails(
|
||||||
|
val isRootThread: Boolean = false,
|
||||||
|
val numberOfThreads: Int = 0,
|
||||||
|
val threadSummarySenderInfo: SenderInfo? = null,
|
||||||
|
val threadSummaryLatestTextMessage: String? = null,
|
||||||
|
val lastMessageTimestamp: Long? = null,
|
||||||
|
var threadNotificationState: ThreadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE,
|
||||||
|
val isThread: Boolean = false,
|
||||||
|
val lastRootThreadEdition: String? = null
|
||||||
|
)
|
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* 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.threads
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class defines the state of a thread notification badge
|
||||||
|
*/
|
||||||
|
data class ThreadNotificationBadgeState(
|
||||||
|
val numberOfLocalUnreadThreads: Int = 0,
|
||||||
|
val isUserMentioned: Boolean = false
|
||||||
|
)
|
@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* 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.threads
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class defines the state of a thread notification
|
||||||
|
*/
|
||||||
|
enum class ThreadNotificationState {
|
||||||
|
|
||||||
|
// There are no new message
|
||||||
|
NO_NEW_MESSAGE,
|
||||||
|
|
||||||
|
// There is at least one new message
|
||||||
|
NEW_MESSAGE,
|
||||||
|
|
||||||
|
// The is at least one new message that should be highlighted
|
||||||
|
// ex. "Hello @aris.kotsomitopoulos"
|
||||||
|
NEW_HIGHLIGHTED_MESSAGE;
|
||||||
|
}
|
@ -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.threads
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class contains a thread TimelineEvent along with a boolean that
|
||||||
|
* determines if the current user has participated in that event
|
||||||
|
*/
|
||||||
|
data class ThreadTimelineEvent(
|
||||||
|
val timelineEvent: TimelineEvent,
|
||||||
|
val isParticipating: Boolean
|
||||||
|
)
|
@ -35,7 +35,7 @@ internal class MXOutboundSessionInfo(
|
|||||||
val sessionLifetime = System.currentTimeMillis() - creationTime
|
val sessionLifetime = System.currentTimeMillis() - creationTime
|
||||||
|
|
||||||
if (useCount >= rotationPeriodMsgs || sessionLifetime >= rotationPeriodMs) {
|
if (useCount >= rotationPeriodMsgs || sessionLifetime >= rotationPeriodMs) {
|
||||||
Timber.v("## needsRotation() : Rotating megolm session after " + useCount + ", " + sessionLifetime + "ms")
|
Timber.v("## needsRotation() : Rotating megolm session after $useCount, ${sessionLifetime}ms")
|
||||||
needsRotation = true
|
needsRotation = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
|
|||||||
import org.matrix.android.sdk.api.session.room.model.VersioningState
|
import org.matrix.android.sdk.api.session.room.model.VersioningState
|
||||||
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
|
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.api.session.room.model.tag.RoomTag
|
||||||
|
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
|
||||||
import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent
|
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.ChunkEntityFields
|
||||||
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields
|
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields
|
||||||
@ -56,7 +57,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
|||||||
) : RealmMigration {
|
) : RealmMigration {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val SESSION_STORE_SCHEMA_VERSION = 22L
|
const val SESSION_STORE_SCHEMA_VERSION = 24L
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -91,6 +92,8 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
|||||||
if (oldVersion <= 19) migrateTo20(realm)
|
if (oldVersion <= 19) migrateTo20(realm)
|
||||||
if (oldVersion <= 20) migrateTo21(realm)
|
if (oldVersion <= 20) migrateTo21(realm)
|
||||||
if (oldVersion <= 21) migrateTo22(realm)
|
if (oldVersion <= 21) migrateTo22(realm)
|
||||||
|
if (oldVersion <= 22) migrateTo23(realm)
|
||||||
|
if (oldVersion <= 23) migrateTo24(realm)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun migrateTo1(realm: DynamicRealm) {
|
private fun migrateTo1(realm: DynamicRealm) {
|
||||||
@ -462,4 +465,28 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
|||||||
realm.deleteAll()
|
realm.deleteAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun migrateTo23(realm: DynamicRealm) {
|
||||||
|
Timber.d("Step 22 -> 23")
|
||||||
|
val eventEntity = realm.schema.get("TimelineEventEntity") ?: return
|
||||||
|
|
||||||
|
realm.schema.get("EventEntity")
|
||||||
|
?.addField(EventEntityFields.IS_ROOT_THREAD, Boolean::class.java, FieldAttribute.INDEXED)
|
||||||
|
?.addField(EventEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED)
|
||||||
|
?.addField(EventEntityFields.NUMBER_OF_THREADS, Int::class.java)
|
||||||
|
?.addField(EventEntityFields.THREAD_NOTIFICATION_STATE_STR, String::class.java)
|
||||||
|
?.transform {
|
||||||
|
it.setString(EventEntityFields.THREAD_NOTIFICATION_STATE_STR, ThreadNotificationState.NO_NEW_MESSAGE.name)
|
||||||
|
}
|
||||||
|
?.addRealmObjectField(EventEntityFields.THREAD_SUMMARY_LATEST_MESSAGE.`$`, eventEntity)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun migrateTo24(realm: DynamicRealm) {
|
||||||
|
Timber.d("Step 23 -> 24")
|
||||||
|
realm.schema.get("PreviewUrlCacheEntity")
|
||||||
|
?.addField(PreviewUrlCacheEntityFields.IMAGE_WIDTH, Int::class.java)
|
||||||
|
?.setNullable(PreviewUrlCacheEntityFields.IMAGE_WIDTH, true)
|
||||||
|
?.addField(PreviewUrlCacheEntityFields.IMAGE_HEIGHT, Int::class.java)
|
||||||
|
?.setNullable(PreviewUrlCacheEntityFields.IMAGE_HEIGHT, true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,7 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
|
|||||||
import org.matrix.android.sdk.internal.database.query.find
|
import org.matrix.android.sdk.internal.database.query.find
|
||||||
import org.matrix.android.sdk.internal.database.query.getOrCreate
|
import org.matrix.android.sdk.internal.database.query.getOrCreate
|
||||||
import org.matrix.android.sdk.internal.database.query.where
|
import org.matrix.android.sdk.internal.database.query.where
|
||||||
|
import org.matrix.android.sdk.internal.database.query.whereRoomId
|
||||||
import org.matrix.android.sdk.internal.extensions.assertIsManaged
|
import org.matrix.android.sdk.internal.extensions.assertIsManaged
|
||||||
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
|
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@ -81,7 +82,7 @@ internal fun ChunkEntity.addStateEvent(roomId: String, stateEvent: EventEntity,
|
|||||||
internal fun ChunkEntity.addTimelineEvent(roomId: String,
|
internal fun ChunkEntity.addTimelineEvent(roomId: String,
|
||||||
eventEntity: EventEntity,
|
eventEntity: EventEntity,
|
||||||
direction: PaginationDirection,
|
direction: PaginationDirection,
|
||||||
roomMemberContentsByUser: Map<String, RoomMemberContent?>) {
|
roomMemberContentsByUser: Map<String, RoomMemberContent?>? = null) {
|
||||||
val eventId = eventEntity.eventId
|
val eventId = eventEntity.eventId
|
||||||
if (timelineEvents.find(eventId) != null) {
|
if (timelineEvents.find(eventId) != null) {
|
||||||
return
|
return
|
||||||
@ -101,7 +102,7 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
|
|||||||
?.also { it.cleanUp(eventEntity.sender) }
|
?.also { it.cleanUp(eventEntity.sender) }
|
||||||
this.readReceipts = readReceiptsSummaryEntity
|
this.readReceipts = readReceiptsSummaryEntity
|
||||||
this.displayIndex = displayIndex
|
this.displayIndex = displayIndex
|
||||||
val roomMemberContent = roomMemberContentsByUser[senderId]
|
val roomMemberContent = roomMemberContentsByUser?.get(senderId)
|
||||||
this.senderAvatar = roomMemberContent?.avatarUrl
|
this.senderAvatar = roomMemberContent?.avatarUrl
|
||||||
this.senderName = roomMemberContent?.displayName
|
this.senderName = roomMemberContent?.displayName
|
||||||
isUniqueDisplayName = if (roomMemberContent?.displayName != null) {
|
isUniqueDisplayName = if (roomMemberContent?.displayName != null) {
|
||||||
@ -157,9 +158,21 @@ private fun ChunkEntity.addTimelineEventFromMerge(realm: Realm, timelineEventEnt
|
|||||||
this.senderName = timelineEventEntity.senderName
|
this.senderName = timelineEventEntity.senderName
|
||||||
this.isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName
|
this.isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName
|
||||||
}
|
}
|
||||||
|
handleThreadSummary(realm, eventId, copied)
|
||||||
timelineEvents.add(copied)
|
timelineEvents.add(copied)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upon copy of the timeline events we should update the latestMessage TimelineEventEntity with the new one
|
||||||
|
*/
|
||||||
|
private fun handleThreadSummary(realm: Realm, oldEventId: String, newTimelineEventEntity: TimelineEventEntity) {
|
||||||
|
EventEntity
|
||||||
|
.whereRoomId(realm, newTimelineEventEntity.roomId)
|
||||||
|
.equalTo(EventEntityFields.IS_ROOT_THREAD, true)
|
||||||
|
.equalTo(EventEntityFields.THREAD_SUMMARY_LATEST_MESSAGE.EVENT_ID, oldEventId)
|
||||||
|
.findFirst()?.threadSummaryLatestMessage = newTimelineEventEntity
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventEntity, senderId: String): ReadReceiptsSummaryEntity {
|
private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventEntity, senderId: String): ReadReceiptsSummaryEntity {
|
||||||
val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventEntity.eventId).findFirst()
|
val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventEntity.eventId).findFirst()
|
||||||
?: realm.createObject<ReadReceiptsSummaryEntity>(eventEntity.eventId).apply {
|
?: realm.createObject<ReadReceiptsSummaryEntity>(eventEntity.eventId).apply {
|
||||||
|
@ -0,0 +1,321 @@
|
|||||||
|
/*
|
||||||
|
* 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.database.helper
|
||||||
|
|
||||||
|
import io.realm.Realm
|
||||||
|
import io.realm.RealmQuery
|
||||||
|
import io.realm.Sort
|
||||||
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
|
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
|
||||||
|
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
||||||
|
import org.matrix.android.sdk.internal.database.model.ChunkEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity
|
||||||
|
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.find
|
||||||
|
import org.matrix.android.sdk.internal.database.query.findIncludingEvent
|
||||||
|
import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
|
||||||
|
import org.matrix.android.sdk.internal.database.query.where
|
||||||
|
import org.matrix.android.sdk.internal.database.query.whereRoomId
|
||||||
|
|
||||||
|
private typealias ThreadSummary = Pair<Int, TimelineEventEntity>?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the root thread event and update it with the latest message summary along with the number
|
||||||
|
* of threads included. If there is no root thread event no action is done
|
||||||
|
*/
|
||||||
|
internal fun Map<String, EventEntity>.updateThreadSummaryIfNeeded(
|
||||||
|
roomId: String,
|
||||||
|
realm: Realm, currentUserId: String,
|
||||||
|
chunkEntity: ChunkEntity? = null,
|
||||||
|
shouldUpdateNotifications: Boolean = true) {
|
||||||
|
for ((rootThreadEventId, eventEntity) in this) {
|
||||||
|
eventEntity.threadSummaryInThread(eventEntity.realm, rootThreadEventId, chunkEntity)?.let { threadSummary ->
|
||||||
|
|
||||||
|
val numberOfMessages = threadSummary.first
|
||||||
|
val latestEventInThread = threadSummary.second
|
||||||
|
|
||||||
|
// If this is a thread message, find its root event if exists
|
||||||
|
val rootThreadEvent = if (eventEntity.isThread()) eventEntity.findRootThreadEvent() else eventEntity
|
||||||
|
|
||||||
|
rootThreadEvent?.markEventAsRoot(
|
||||||
|
threadsCounted = numberOfMessages,
|
||||||
|
latestMessageTimelineEventEntity = latestEventInThread
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldUpdateNotifications) {
|
||||||
|
updateNotificationsNew(roomId, realm, currentUserId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the root event of the the current thread event message.
|
||||||
|
* Returns the EventEntity or null if the root event do not exist
|
||||||
|
*/
|
||||||
|
internal fun EventEntity.findRootThreadEvent(): EventEntity? =
|
||||||
|
rootThreadEventId?.let {
|
||||||
|
EventEntity
|
||||||
|
.where(realm, it)
|
||||||
|
.findFirst()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark or update the current event a root thread event
|
||||||
|
*/
|
||||||
|
internal fun EventEntity.markEventAsRoot(
|
||||||
|
threadsCounted: Int,
|
||||||
|
latestMessageTimelineEventEntity: TimelineEventEntity?) {
|
||||||
|
isRootThread = true
|
||||||
|
numberOfThreads = threadsCounted
|
||||||
|
threadSummaryLatestMessage = latestMessageTimelineEventEntity
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count the number of threads for the provided root thread eventId, and finds the latest event message
|
||||||
|
* @param rootThreadEventId The root eventId that will find the number of threads
|
||||||
|
* @return A ThreadSummary containing the counted threads and the latest event message
|
||||||
|
*/
|
||||||
|
internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: String, chunkEntity: ChunkEntity?): ThreadSummary {
|
||||||
|
// Number of messages
|
||||||
|
val messages = TimelineEventEntity
|
||||||
|
.whereRoomId(realm, roomId = roomId)
|
||||||
|
.equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId)
|
||||||
|
.count()
|
||||||
|
.toInt()
|
||||||
|
|
||||||
|
if (messages <= 0) return null
|
||||||
|
|
||||||
|
// Find latest thread event, we know it exists
|
||||||
|
var chunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) ?: chunkEntity ?: return null
|
||||||
|
var result: TimelineEventEntity? = null
|
||||||
|
|
||||||
|
// Iterate the chunk until we find our latest event
|
||||||
|
while (result == null) {
|
||||||
|
result = findLatestSortedChunkEvent(chunk, rootThreadEventId)
|
||||||
|
chunk = ChunkEntity.find(realm, roomId, nextToken = chunk.prevToken) ?: break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == null && chunkEntity != null) {
|
||||||
|
// Find latest event from our current chunk
|
||||||
|
result = findLatestSortedChunkEvent(chunkEntity, rootThreadEventId)
|
||||||
|
} else if (result != null && chunkEntity != null) {
|
||||||
|
val currentChunkLatestEvent = findLatestSortedChunkEvent(chunkEntity, rootThreadEventId)
|
||||||
|
result = findMostRecentEvent(result, currentChunkLatestEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
result ?: return null
|
||||||
|
|
||||||
|
return ThreadSummary(messages, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lets compare them in case user is moving forward in the timeline and we cannot know the
|
||||||
|
* exact chunk sequence while currentChunk is not yet committed in the DB
|
||||||
|
*/
|
||||||
|
private fun findMostRecentEvent(result: TimelineEventEntity, currentChunkLatestEvent: TimelineEventEntity?): TimelineEventEntity {
|
||||||
|
currentChunkLatestEvent ?: return result
|
||||||
|
val currentChunkEventTimestamp = currentChunkLatestEvent.root?.originServerTs ?: return result
|
||||||
|
val resultTimestamp = result.root?.originServerTs ?: return result
|
||||||
|
if (currentChunkEventTimestamp > resultTimestamp) {
|
||||||
|
return currentChunkLatestEvent
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the latest event of the current chunk
|
||||||
|
*/
|
||||||
|
private fun findLatestSortedChunkEvent(chunk: ChunkEntity, rootThreadEventId: String): TimelineEventEntity? =
|
||||||
|
chunk.timelineEvents.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)?.firstOrNull {
|
||||||
|
it.root?.rootThreadEventId == rootThreadEventId
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all TimelineEventEntity that are root threads for the specified room
|
||||||
|
* @param roomId The room that all stored root threads will be returned
|
||||||
|
*/
|
||||||
|
internal fun TimelineEventEntity.Companion.findAllThreadsForRoomId(realm: Realm, roomId: String): RealmQuery<TimelineEventEntity> =
|
||||||
|
TimelineEventEntity
|
||||||
|
.whereRoomId(realm, roomId = roomId)
|
||||||
|
.equalTo(TimelineEventEntityFields.ROOT.IS_ROOT_THREAD, true)
|
||||||
|
.sort("${TimelineEventEntityFields.ROOT.THREAD_SUMMARY_LATEST_MESSAGE}.${TimelineEventEntityFields.ROOT.ORIGIN_SERVER_TS}", Sort.DESCENDING)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map each root thread TimelineEvent with the equivalent decrypted text edition/replacement
|
||||||
|
*/
|
||||||
|
internal fun List<TimelineEvent>.mapEventsWithEdition(realm: Realm, roomId: String): List<TimelineEvent> =
|
||||||
|
this.map {
|
||||||
|
EventAnnotationsSummaryEntity
|
||||||
|
.where(realm, roomId, eventId = it.eventId)
|
||||||
|
.findFirst()
|
||||||
|
?.editSummary
|
||||||
|
?.editions
|
||||||
|
?.lastOrNull()
|
||||||
|
?.eventId
|
||||||
|
?.let { editedEventId ->
|
||||||
|
TimelineEventEntity.where(realm, roomId, eventId = editedEventId).findFirst()?.let { editedEvent ->
|
||||||
|
it.root.threadDetails = it.root.threadDetails?.copy(lastRootThreadEdition = editedEvent.root?.asDomain()?.getDecryptedTextSummary()
|
||||||
|
?: "(edited)")
|
||||||
|
it
|
||||||
|
} ?: it
|
||||||
|
} ?: it
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of all the marked unread threads that exists for the specified room
|
||||||
|
* @param roomId The roomId that the user is currently in
|
||||||
|
*/
|
||||||
|
internal fun TimelineEventEntity.Companion.findAllLocalThreadNotificationsForRoomId(realm: Realm, roomId: String): RealmQuery<TimelineEventEntity> =
|
||||||
|
TimelineEventEntity
|
||||||
|
.whereRoomId(realm, roomId = roomId)
|
||||||
|
.equalTo(TimelineEventEntityFields.ROOT.IS_ROOT_THREAD, true)
|
||||||
|
.beginGroup()
|
||||||
|
.equalTo(TimelineEventEntityFields.ROOT.THREAD_NOTIFICATION_STATE_STR, ThreadNotificationState.NEW_MESSAGE.name)
|
||||||
|
.or()
|
||||||
|
.equalTo(TimelineEventEntityFields.ROOT.THREAD_NOTIFICATION_STATE_STR, ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE.name)
|
||||||
|
.endGroup()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether or not the given user is participating in a current thread
|
||||||
|
* @param roomId the room that the thread exists
|
||||||
|
* @param rootThreadEventId the thread that the search will be done
|
||||||
|
* @param senderId the user that will try to find participation
|
||||||
|
*/
|
||||||
|
internal fun TimelineEventEntity.Companion.isUserParticipatingInThread(realm: Realm, roomId: String, rootThreadEventId: String, senderId: String): Boolean =
|
||||||
|
TimelineEventEntity
|
||||||
|
.whereRoomId(realm, roomId = roomId)
|
||||||
|
.equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId)
|
||||||
|
.equalTo(TimelineEventEntityFields.ROOT.SENDER, senderId)
|
||||||
|
.findFirst()
|
||||||
|
?.let { true }
|
||||||
|
?: false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether or not the given user is mentioned in a current thread
|
||||||
|
* @param roomId the room that the thread exists
|
||||||
|
* @param rootThreadEventId the thread that the search will be done
|
||||||
|
* @param userId the user that will try to find if there is a mention
|
||||||
|
*/
|
||||||
|
internal fun TimelineEventEntity.Companion.isUserMentionedInThread(realm: Realm, roomId: String, rootThreadEventId: String, userId: String): Boolean =
|
||||||
|
TimelineEventEntity
|
||||||
|
.whereRoomId(realm, roomId = roomId)
|
||||||
|
.equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId)
|
||||||
|
.equalTo(TimelineEventEntityFields.ROOT.SENDER, userId)
|
||||||
|
.findAll()
|
||||||
|
.firstOrNull { isUserMentioned(userId, it) }
|
||||||
|
?.let { true }
|
||||||
|
?: false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the read receipt for the current user
|
||||||
|
*/
|
||||||
|
internal fun findMyReadReceipt(realm: Realm, roomId: String, userId: String): String? =
|
||||||
|
ReadReceiptEntity.where(realm, roomId = roomId, userId = userId)
|
||||||
|
.findFirst()
|
||||||
|
?.eventId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether or not the user is mentioned in the event
|
||||||
|
*/
|
||||||
|
internal fun isUserMentioned(currentUserId: String, timelineEventEntity: TimelineEventEntity?): Boolean {
|
||||||
|
return timelineEventEntity?.root?.asDomain()?.isUserMentioned(currentUserId) == true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update badge notifications. Count the number of new thread events after the latest
|
||||||
|
* read receipt and aggregate. This function will find and notify new thread events
|
||||||
|
* that the user is either mentioned, or the user had participated in.
|
||||||
|
* Important: If the root thread event is not fetched notification will not work
|
||||||
|
* Important: It will work only with the latest chunk, while read marker will be changed
|
||||||
|
* immediately so we should not display wrong notifications
|
||||||
|
*/
|
||||||
|
internal fun updateNotificationsNew(roomId: String, realm: Realm, currentUserId: String) {
|
||||||
|
val readReceipt = findMyReadReceipt(realm, roomId, currentUserId) ?: return
|
||||||
|
|
||||||
|
val readReceiptChunk = ChunkEntity
|
||||||
|
.findIncludingEvent(realm, readReceipt) ?: return
|
||||||
|
|
||||||
|
val readReceiptChunkTimelineEvents = readReceiptChunk
|
||||||
|
.timelineEvents
|
||||||
|
.where()
|
||||||
|
.equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
|
||||||
|
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
|
||||||
|
.findAll() ?: return
|
||||||
|
|
||||||
|
val readReceiptChunkPosition = readReceiptChunkTimelineEvents.indexOfFirst { it.eventId == readReceipt }
|
||||||
|
|
||||||
|
if (readReceiptChunkPosition == -1) return
|
||||||
|
|
||||||
|
if (readReceiptChunkPosition < readReceiptChunkTimelineEvents.lastIndex) {
|
||||||
|
// If the read receipt is found inside the chunk
|
||||||
|
|
||||||
|
val threadEventsAfterReadReceipt = readReceiptChunkTimelineEvents
|
||||||
|
.slice(readReceiptChunkPosition..readReceiptChunkTimelineEvents.lastIndex)
|
||||||
|
.filter { it.root?.isThread() == true }
|
||||||
|
|
||||||
|
// In order for the below code to work for old events, we should save the previous read receipt
|
||||||
|
// and then continue with the chunk search for that read receipt
|
||||||
|
/*
|
||||||
|
val newThreadEventsList = arrayListOf<TimelineEventEntity>()
|
||||||
|
newThreadEventsList.addAll(threadEventsAfterReadReceipt)
|
||||||
|
|
||||||
|
// got from latest chunk all new threads, lets move to the others
|
||||||
|
var nextChunk = ChunkEntity
|
||||||
|
.find(realm = realm, roomId = roomId, nextToken = readReceiptChunk.nextToken)
|
||||||
|
.takeIf { readReceiptChunk.nextToken != null }
|
||||||
|
while (nextChunk != null) {
|
||||||
|
newThreadEventsList.addAll(nextChunk.timelineEvents
|
||||||
|
.filter { it.root?.isThread() == true })
|
||||||
|
nextChunk = ChunkEntity
|
||||||
|
.find(realm = realm, roomId = roomId, nextToken = nextChunk.nextToken)
|
||||||
|
.takeIf { readReceiptChunk.nextToken != null }
|
||||||
|
}*/
|
||||||
|
|
||||||
|
// Find if the user is mentioned in those events
|
||||||
|
val userMentionsList = threadEventsAfterReadReceipt
|
||||||
|
.filter {
|
||||||
|
isUserMentioned(currentUserId = currentUserId, it)
|
||||||
|
}.map {
|
||||||
|
it.root?.rootThreadEventId
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the root events in the new thread events
|
||||||
|
val rootThreads = threadEventsAfterReadReceipt.distinctBy { it.root?.rootThreadEventId }.mapNotNull { it.root?.rootThreadEventId }
|
||||||
|
|
||||||
|
// Update root thread events only if the user have participated in
|
||||||
|
rootThreads.forEach { eventId ->
|
||||||
|
val isUserParticipating = TimelineEventEntity.isUserParticipatingInThread(
|
||||||
|
realm = realm,
|
||||||
|
roomId = roomId,
|
||||||
|
rootThreadEventId = eventId,
|
||||||
|
senderId = currentUserId)
|
||||||
|
val rootThreadEventEntity = EventEntity.where(realm, eventId).findFirst()
|
||||||
|
|
||||||
|
if (isUserParticipating) {
|
||||||
|
rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_MESSAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userMentionsList.contains(eventId)) {
|
||||||
|
rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.database.lightweight
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The purpose of this class is to provide an alternative and lightweight way to store settings/data
|
||||||
|
* on the sdi without using the database. This should be used just for sdk/user preferences and
|
||||||
|
* not for large data sets
|
||||||
|
*/
|
||||||
|
|
||||||
|
class LightweightSettingsStorage @Inject constructor(context: Context) {
|
||||||
|
|
||||||
|
private val sdkDefaultPrefs = PreferenceManager.getDefaultSharedPreferences(context.applicationContext)
|
||||||
|
|
||||||
|
fun setThreadMessagesEnabled(enabled: Boolean) {
|
||||||
|
sdkDefaultPrefs.edit {
|
||||||
|
putBoolean(MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED, enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun areThreadMessagesEnabled(): Boolean {
|
||||||
|
return sdkDefaultPrefs.getBoolean(MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED = "MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED"
|
||||||
|
}
|
||||||
|
}
|
@ -21,7 +21,11 @@ import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
|||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.api.session.events.model.UnsignedData
|
import org.matrix.android.sdk.api.session.events.model.UnsignedData
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId
|
||||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||||
|
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
|
||||||
|
import org.matrix.android.sdk.api.session.threads.ThreadDetails
|
||||||
|
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
|
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||||
@ -51,6 +55,10 @@ internal object EventMapper {
|
|||||||
}
|
}
|
||||||
eventEntity.decryptionErrorReason = event.mCryptoErrorReason
|
eventEntity.decryptionErrorReason = event.mCryptoErrorReason
|
||||||
eventEntity.decryptionErrorCode = event.mCryptoError?.name
|
eventEntity.decryptionErrorCode = event.mCryptoError?.name
|
||||||
|
eventEntity.isRootThread = event.threadDetails?.isRootThread ?: false
|
||||||
|
eventEntity.rootThreadEventId = event.getRootThreadEventId()
|
||||||
|
eventEntity.numberOfThreads = event.threadDetails?.numberOfThreads ?: 0
|
||||||
|
eventEntity.threadNotificationState = event.threadDetails?.threadNotificationState ?: ThreadNotificationState.NO_NEW_MESSAGE
|
||||||
return eventEntity
|
return eventEntity
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,6 +101,23 @@ internal object EventMapper {
|
|||||||
MXCryptoError.ErrorType.valueOf(errorCode)
|
MXCryptoError.ErrorType.valueOf(errorCode)
|
||||||
}
|
}
|
||||||
it.mCryptoErrorReason = eventEntity.decryptionErrorReason
|
it.mCryptoErrorReason = eventEntity.decryptionErrorReason
|
||||||
|
it.threadDetails = ThreadDetails(
|
||||||
|
isRootThread = eventEntity.isRootThread,
|
||||||
|
isThread = if (it.threadDetails?.isThread == true) true else eventEntity.isThread(),
|
||||||
|
numberOfThreads = eventEntity.numberOfThreads,
|
||||||
|
threadSummarySenderInfo = eventEntity.threadSummaryLatestMessage?.let { timelineEventEntity ->
|
||||||
|
SenderInfo(
|
||||||
|
userId = timelineEventEntity.root?.sender ?: "",
|
||||||
|
displayName = timelineEventEntity.senderName,
|
||||||
|
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
|
||||||
|
avatarUrl = timelineEventEntity.senderAvatar
|
||||||
|
)
|
||||||
|
},
|
||||||
|
threadNotificationState = eventEntity.threadNotificationState,
|
||||||
|
threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedTextSummary(),
|
||||||
|
lastMessageTimestamp = eventEntity.threadSummaryLatestMessage?.root?.originServerTs
|
||||||
|
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -101,9 +126,15 @@ internal fun EventEntity.asDomain(castJsonNumbers: Boolean = false): Event {
|
|||||||
return EventMapper.map(this, castJsonNumbers)
|
return EventMapper.map(this, castJsonNumbers)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun Event.toEntity(roomId: String, sendState: SendState, ageLocalTs: Long?): EventEntity {
|
internal fun Event.toEntity(roomId: String, sendState: SendState, ageLocalTs: Long?, contentToInject: String? = null): EventEntity {
|
||||||
return EventMapper.map(this, roomId).apply {
|
return EventMapper.map(this, roomId).apply {
|
||||||
this.sendState = sendState
|
this.sendState = sendState
|
||||||
this.ageLocalTs = ageLocalTs
|
this.ageLocalTs = ageLocalTs
|
||||||
|
contentToInject?.let {
|
||||||
|
this.content = it
|
||||||
|
if (this.type == EventType.STICKER) {
|
||||||
|
this.type = EventType.MESSAGE
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.database.model
|
|||||||
import io.realm.RealmObject
|
import io.realm.RealmObject
|
||||||
import io.realm.annotations.Index
|
import io.realm.annotations.Index
|
||||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||||
import org.matrix.android.sdk.api.util.JsonDict
|
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
|
||||||
import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
|
import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
|
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||||
@ -40,7 +40,12 @@ internal open class EventEntity(@Index var eventId: String = "",
|
|||||||
var unsignedData: String? = null,
|
var unsignedData: String? = null,
|
||||||
var redacts: String? = null,
|
var redacts: String? = null,
|
||||||
var decryptionResultJson: String? = null,
|
var decryptionResultJson: String? = null,
|
||||||
var ageLocalTs: Long? = null
|
var ageLocalTs: Long? = null,
|
||||||
|
// Thread related, no need to create a new Entity for performance
|
||||||
|
@Index var isRootThread: Boolean = false,
|
||||||
|
@Index var rootThreadEventId: String? = null,
|
||||||
|
var numberOfThreads: Int = 0,
|
||||||
|
var threadSummaryLatestMessage: TimelineEventEntity? = null
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
||||||
private var sendStateStr: String = SendState.UNKNOWN.name
|
private var sendStateStr: String = SendState.UNKNOWN.name
|
||||||
@ -53,6 +58,15 @@ internal open class EventEntity(@Index var eventId: String = "",
|
|||||||
sendStateStr = value.name
|
sendStateStr = value.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var threadNotificationStateStr: String = ThreadNotificationState.NO_NEW_MESSAGE.name
|
||||||
|
var threadNotificationState: ThreadNotificationState
|
||||||
|
get() {
|
||||||
|
return ThreadNotificationState.valueOf(threadNotificationStateStr)
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
threadNotificationStateStr = value.name
|
||||||
|
}
|
||||||
|
|
||||||
var decryptionErrorCode: String? = null
|
var decryptionErrorCode: String? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
if (value != field) field = value
|
if (value != field) field = value
|
||||||
@ -65,10 +79,10 @@ internal open class EventEntity(@Index var eventId: String = "",
|
|||||||
|
|
||||||
companion object
|
companion object
|
||||||
|
|
||||||
fun setDecryptionResult(result: MXEventDecryptionResult, clearEvent: JsonDict? = null) {
|
fun setDecryptionResult(result: MXEventDecryptionResult) {
|
||||||
assertIsManaged()
|
assertIsManaged()
|
||||||
val decryptionResult = OlmDecryptionResult(
|
val decryptionResult = OlmDecryptionResult(
|
||||||
payload = clearEvent ?: result.clearEvent,
|
payload = result.clearEvent,
|
||||||
senderKey = result.senderCurve25519Key,
|
senderKey = result.senderCurve25519Key,
|
||||||
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
||||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||||
@ -84,4 +98,6 @@ internal open class EventEntity(@Index var eventId: String = "",
|
|||||||
.findFirst()
|
.findFirst()
|
||||||
?.canBeProcessed = true
|
?.canBeProcessed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isThread(): Boolean = rootThreadEventId != null
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,8 @@ internal open class PreviewUrlCacheEntity(
|
|||||||
var title: String? = null,
|
var title: String? = null,
|
||||||
var description: String? = null,
|
var description: String? = null,
|
||||||
var mxcUrl: String? = null,
|
var mxcUrl: String? = null,
|
||||||
|
var imageWidth: Int? = null,
|
||||||
|
var imageHeight: Int? = null,
|
||||||
var lastUpdatedTimestamp: Long = 0L
|
var lastUpdatedTimestamp: Long = 0L
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
||||||
|
@ -49,6 +49,11 @@ internal fun EventEntity.Companion.where(realm: Realm, eventId: String): RealmQu
|
|||||||
.equalTo(EventEntityFields.EVENT_ID, eventId)
|
.equalTo(EventEntityFields.EVENT_ID, eventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun EventEntity.Companion.whereRoomId(realm: Realm, roomId: String): RealmQuery<EventEntity> {
|
||||||
|
return realm.where<EventEntity>()
|
||||||
|
.equalTo(EventEntityFields.ROOM_ID, roomId)
|
||||||
|
}
|
||||||
|
|
||||||
internal fun EventEntity.Companion.where(realm: Realm, eventIds: List<String>): RealmQuery<EventEntity> {
|
internal fun EventEntity.Companion.where(realm: Realm, eventIds: List<String>): RealmQuery<EventEntity> {
|
||||||
return realm.where<EventEntity>()
|
return realm.where<EventEntity>()
|
||||||
.`in`(EventEntityFields.EVENT_ID, eventIds.toTypedArray())
|
.`in`(EventEntityFields.EVENT_ID, eventIds.toTypedArray())
|
||||||
@ -85,3 +90,8 @@ internal fun RealmList<EventEntity>.find(eventId: String): EventEntity? {
|
|||||||
internal fun RealmList<EventEntity>.fastContains(eventId: String): Boolean {
|
internal fun RealmList<EventEntity>.fastContains(eventId: String): Boolean {
|
||||||
return this.find(eventId) != null
|
return this.find(eventId) != null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun EventEntity.Companion.whereRootThreadEventId(realm: Realm, rootThreadEventId: String): RealmQuery<EventEntity> {
|
||||||
|
return realm.where<EventEntity>()
|
||||||
|
.equalTo(EventEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId)
|
||||||
|
}
|
||||||
|
@ -34,27 +34,29 @@ internal fun isEventRead(realmConfiguration: RealmConfiguration,
|
|||||||
if (LocalEcho.isLocalEchoId(eventId)) {
|
if (LocalEcho.isLocalEchoId(eventId)) {
|
||||||
return true
|
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 ->
|
return Realm.getInstance(realmConfiguration).use { realm ->
|
||||||
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()
|
val eventToCheck = TimelineEventEntity.where(realm, roomId, eventId).findFirst()
|
||||||
isEventRead = when {
|
when {
|
||||||
eventToCheck == null -> false
|
// The event doesn't exist locally, let's assume it hasn't been read
|
||||||
eventToCheck.root?.sender == userId -> true
|
eventToCheck == null -> false
|
||||||
else -> {
|
eventToCheck.root?.sender == userId -> true
|
||||||
val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst() ?: return@use
|
// If new event exists and the latest event is from ourselves we can infer the event is read
|
||||||
val readReceiptEvent = TimelineEventEntity.where(realm, roomId, readReceipt.eventId).findFirst() ?: return@use
|
latestEventIsFromSelf(realm, roomId, userId) -> true
|
||||||
readReceiptEvent.isMoreRecentThan(eventToCheck)
|
eventToCheck.isBeforeLatestReadReceipt(realm, roomId, userId) -> true
|
||||||
}
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return isEventRead
|
}
|
||||||
|
|
||||||
|
private fun latestEventIsFromSelf(realm: Realm, roomId: String, userId: String) = TimelineEventEntity.latestEvent(realm, roomId, true)
|
||||||
|
?.root?.sender == userId
|
||||||
|
|
||||||
|
private fun TimelineEventEntity.isBeforeLatestReadReceipt(realm: Realm, roomId: String, userId: String): Boolean {
|
||||||
|
return ReadReceiptEntity.where(realm, roomId, userId).findFirst()?.let { readReceipt ->
|
||||||
|
val readReceiptEvent = TimelineEventEntity.where(realm, roomId, readReceipt.eventId).findFirst()
|
||||||
|
readReceiptEvent?.isMoreRecentThan(this)
|
||||||
|
} ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -59,6 +59,7 @@ internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm,
|
|||||||
filters: TimelineEventFilters = TimelineEventFilters()): TimelineEventEntity? {
|
filters: TimelineEventFilters = TimelineEventFilters()): TimelineEventEntity? {
|
||||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: return null
|
val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: return null
|
||||||
val sendingTimelineEvents = roomEntity.sendingTimelineEvents.where().filterEvents(filters)
|
val sendingTimelineEvents = roomEntity.sendingTimelineEvents.where().filterEvents(filters)
|
||||||
|
|
||||||
val liveEvents = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)?.timelineEvents?.where()?.filterEvents(filters)
|
val liveEvents = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)?.timelineEvents?.where()?.filterEvents(filters)
|
||||||
val query = if (includesSending && sendingTimelineEvents.findAll().isNotEmpty()) {
|
val query = if (includesSending && sendingTimelineEvents.findAll().isNotEmpty()) {
|
||||||
sendingTimelineEvents
|
sendingTimelineEvents
|
||||||
@ -100,6 +101,7 @@ internal fun RealmQuery<TimelineEventEntity>.filterEvents(filters: TimelineEvent
|
|||||||
if (filters.filterRedacted) {
|
if (filters.filterRedacted) {
|
||||||
not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED)
|
not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ internal class ThumbnailExtractor @Inject constructor(
|
|||||||
thumbnail.recycle()
|
thumbnail.recycle()
|
||||||
outputStream.reset()
|
outputStream.reset()
|
||||||
} ?: run {
|
} ?: run {
|
||||||
Timber.e("Cannot extract video thumbnail at %s", attachment.queryUri.toString())
|
Timber.e("Cannot extract video thumbnail at ${attachment.queryUri}")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "Cannot extract video thumbnail")
|
Timber.e(e, "Cannot extract video thumbnail")
|
||||||
|
@ -48,6 +48,16 @@ data class RoomEventFilter(
|
|||||||
* a wildcard to match any sequence of characters.
|
* a wildcard to match any sequence of characters.
|
||||||
*/
|
*/
|
||||||
@Json(name = "types") val types: List<String>? = null,
|
@Json(name = "types") val types: List<String>? = null,
|
||||||
|
/**
|
||||||
|
* A list of relation types which must be exist pointing to the event being filtered.
|
||||||
|
* If this list is absent then no filtering is done on relation types.
|
||||||
|
*/
|
||||||
|
@Json(name = "relation_types") val relationTypes: List<String>? = null,
|
||||||
|
/**
|
||||||
|
* A list of senders of relations which must exist pointing to the event being filtered.
|
||||||
|
* If this list is absent then no filtering is done on relation types.
|
||||||
|
*/
|
||||||
|
@Json(name = "relation_senders") val relationSenders: List<String>? = null,
|
||||||
/**
|
/**
|
||||||
* A list of room IDs to include. If this list is absent then all rooms are included.
|
* A list of room IDs to include. If this list is absent then all rooms are included.
|
||||||
*/
|
*/
|
||||||
|
@ -48,8 +48,8 @@ internal class DefaultGetPreviewUrlTask @Inject constructor(
|
|||||||
|
|
||||||
override suspend fun execute(params: GetPreviewUrlTask.Params): PreviewUrlData {
|
override suspend fun execute(params: GetPreviewUrlTask.Params): PreviewUrlData {
|
||||||
return when (params.cacheStrategy) {
|
return when (params.cacheStrategy) {
|
||||||
CacheStrategy.NoCache -> doRequest(params.url, params.timestamp)
|
CacheStrategy.NoCache -> doRequest(params.url, params.timestamp)
|
||||||
is CacheStrategy.TtlCache -> doRequestWithCache(
|
is CacheStrategy.TtlCache -> doRequestWithCache(
|
||||||
params.url,
|
params.url,
|
||||||
params.timestamp,
|
params.timestamp,
|
||||||
params.cacheStrategy.validityDurationInMillis,
|
params.cacheStrategy.validityDurationInMillis,
|
||||||
@ -77,7 +77,9 @@ internal class DefaultGetPreviewUrlTask @Inject constructor(
|
|||||||
siteName = (get("og:site_name") as? String)?.unescapeHtml(),
|
siteName = (get("og:site_name") as? String)?.unescapeHtml(),
|
||||||
title = (get("og:title") as? String)?.unescapeHtml(),
|
title = (get("og:title") as? String)?.unescapeHtml(),
|
||||||
description = (get("og:description") as? String)?.unescapeHtml(),
|
description = (get("og:description") as? String)?.unescapeHtml(),
|
||||||
mxcUrl = get("og:image") as? String
|
mxcUrl = get("og:image") as? String,
|
||||||
|
imageHeight = (get("og:image:height") as? Double)?.toInt(),
|
||||||
|
imageWidth = (get("og:image:width") as? Double)?.toInt(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,7 +116,8 @@ internal class DefaultGetPreviewUrlTask @Inject constructor(
|
|||||||
previewUrlCacheEntity.title = data.title
|
previewUrlCacheEntity.title = data.title
|
||||||
previewUrlCacheEntity.description = data.description
|
previewUrlCacheEntity.description = data.description
|
||||||
previewUrlCacheEntity.mxcUrl = data.mxcUrl
|
previewUrlCacheEntity.mxcUrl = data.mxcUrl
|
||||||
|
previewUrlCacheEntity.imageHeight = data.imageHeight
|
||||||
|
previewUrlCacheEntity.imageWidth = data.imageWidth
|
||||||
previewUrlCacheEntity.lastUpdatedTimestamp = Date().time
|
previewUrlCacheEntity.lastUpdatedTimestamp = Date().time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,5 +27,7 @@ internal fun PreviewUrlCacheEntity.toDomain() = PreviewUrlData(
|
|||||||
siteName = siteName,
|
siteName = siteName,
|
||||||
title = title,
|
title = title,
|
||||||
description = description,
|
description = description,
|
||||||
mxcUrl = mxcUrl
|
mxcUrl = mxcUrl,
|
||||||
|
imageWidth = imageWidth,
|
||||||
|
imageHeight = imageHeight
|
||||||
)
|
)
|
||||||
|
@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.session.room.send.DraftService
|
|||||||
import org.matrix.android.sdk.api.session.room.send.SendService
|
import org.matrix.android.sdk.api.session.room.send.SendService
|
||||||
import org.matrix.android.sdk.api.session.room.state.StateService
|
import org.matrix.android.sdk.api.session.room.state.StateService
|
||||||
import org.matrix.android.sdk.api.session.room.tags.TagsService
|
import org.matrix.android.sdk.api.session.room.tags.TagsService
|
||||||
|
import org.matrix.android.sdk.api.session.room.threads.ThreadsService
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineService
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineService
|
||||||
import org.matrix.android.sdk.api.session.room.typing.TypingService
|
import org.matrix.android.sdk.api.session.room.typing.TypingService
|
||||||
import org.matrix.android.sdk.api.session.room.uploads.UploadsService
|
import org.matrix.android.sdk.api.session.room.uploads.UploadsService
|
||||||
@ -54,6 +55,7 @@ import java.security.InvalidParameterException
|
|||||||
internal class DefaultRoom(override val roomId: String,
|
internal class DefaultRoom(override val roomId: String,
|
||||||
private val roomSummaryDataSource: RoomSummaryDataSource,
|
private val roomSummaryDataSource: RoomSummaryDataSource,
|
||||||
private val timelineService: TimelineService,
|
private val timelineService: TimelineService,
|
||||||
|
private val threadsService: ThreadsService,
|
||||||
private val sendService: SendService,
|
private val sendService: SendService,
|
||||||
private val draftService: DraftService,
|
private val draftService: DraftService,
|
||||||
private val stateService: StateService,
|
private val stateService: StateService,
|
||||||
@ -77,6 +79,7 @@ internal class DefaultRoom(override val roomId: String,
|
|||||||
) :
|
) :
|
||||||
Room,
|
Room,
|
||||||
TimelineService by timelineService,
|
TimelineService by timelineService,
|
||||||
|
ThreadsService by threadsService,
|
||||||
SendService by sendService,
|
SendService by sendService,
|
||||||
DraftService by draftService,
|
DraftService by draftService,
|
||||||
StateService by stateService,
|
StateService by stateService,
|
||||||
|
@ -44,6 +44,7 @@ import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
|||||||
import org.matrix.android.sdk.internal.SessionManager
|
import org.matrix.android.sdk.internal.SessionManager
|
||||||
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.toState
|
import org.matrix.android.sdk.internal.crypto.verification.toState
|
||||||
|
import org.matrix.android.sdk.internal.database.helper.findRootThreadEvent
|
||||||
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
|
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
|
||||||
import org.matrix.android.sdk.internal.database.mapper.EventMapper
|
import org.matrix.android.sdk.internal.database.mapper.EventMapper
|
||||||
import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntity
|
import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntity
|
||||||
@ -332,6 +333,29 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isLocalEcho) {
|
||||||
|
val replaceEvent = TimelineEventEntity.where(realm, roomId, eventId).findFirst()
|
||||||
|
handleThreadSummaryEdition(editedEvent, replaceEvent, existingSummary?.editions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the edition is on the latest thread event, and update it accordingly
|
||||||
|
*/
|
||||||
|
private fun handleThreadSummaryEdition(editedEvent: EventEntity?,
|
||||||
|
replaceEvent: TimelineEventEntity?,
|
||||||
|
editions: List<EditionOfEvent>?) {
|
||||||
|
replaceEvent ?: return
|
||||||
|
editedEvent ?: return
|
||||||
|
editedEvent.findRootThreadEvent()?.apply {
|
||||||
|
val threadSummaryEventId = threadSummaryLatestMessage?.eventId
|
||||||
|
if (editedEvent.eventId == threadSummaryEventId || editions?.any { it.eventId == threadSummaryEventId } == true) {
|
||||||
|
// The edition is for the latest event or for any event replaced, this is to handle multiple
|
||||||
|
// edits of the same latest event
|
||||||
|
threadSummaryLatestMessage = replaceEvent
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleResponse(realm: Realm,
|
private fun handleResponse(realm: Realm,
|
||||||
|
@ -226,7 +226,8 @@ internal interface RoomAPI {
|
|||||||
suspend fun getRelations(@Path("roomId") roomId: String,
|
suspend fun getRelations(@Path("roomId") roomId: String,
|
||||||
@Path("eventId") eventId: String,
|
@Path("eventId") eventId: String,
|
||||||
@Path("relationType") relationType: String,
|
@Path("relationType") relationType: String,
|
||||||
@Path("eventType") eventType: String
|
@Path("eventType") eventType: String,
|
||||||
|
@Query("limit") limit: Int? = null
|
||||||
): RelationsResponse
|
): RelationsResponse
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -35,6 +35,7 @@ import org.matrix.android.sdk.internal.session.room.state.DefaultStateService
|
|||||||
import org.matrix.android.sdk.internal.session.room.state.SendStateTask
|
import org.matrix.android.sdk.internal.session.room.state.SendStateTask
|
||||||
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
|
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
|
||||||
import org.matrix.android.sdk.internal.session.room.tags.DefaultTagsService
|
import org.matrix.android.sdk.internal.session.room.tags.DefaultTagsService
|
||||||
|
import org.matrix.android.sdk.internal.session.room.threads.DefaultThreadsService
|
||||||
import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimelineService
|
import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimelineService
|
||||||
import org.matrix.android.sdk.internal.session.room.typing.DefaultTypingService
|
import org.matrix.android.sdk.internal.session.room.typing.DefaultTypingService
|
||||||
import org.matrix.android.sdk.internal.session.room.uploads.DefaultUploadsService
|
import org.matrix.android.sdk.internal.session.room.uploads.DefaultUploadsService
|
||||||
@ -50,6 +51,7 @@ internal interface RoomFactory {
|
|||||||
internal class DefaultRoomFactory @Inject constructor(private val cryptoService: CryptoService,
|
internal class DefaultRoomFactory @Inject constructor(private val cryptoService: CryptoService,
|
||||||
private val roomSummaryDataSource: RoomSummaryDataSource,
|
private val roomSummaryDataSource: RoomSummaryDataSource,
|
||||||
private val timelineServiceFactory: DefaultTimelineService.Factory,
|
private val timelineServiceFactory: DefaultTimelineService.Factory,
|
||||||
|
private val threadsServiceFactory: DefaultThreadsService.Factory,
|
||||||
private val sendServiceFactory: DefaultSendService.Factory,
|
private val sendServiceFactory: DefaultSendService.Factory,
|
||||||
private val draftServiceFactory: DefaultDraftService.Factory,
|
private val draftServiceFactory: DefaultDraftService.Factory,
|
||||||
private val stateServiceFactory: DefaultStateService.Factory,
|
private val stateServiceFactory: DefaultStateService.Factory,
|
||||||
@ -76,6 +78,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService:
|
|||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
roomSummaryDataSource = roomSummaryDataSource,
|
roomSummaryDataSource = roomSummaryDataSource,
|
||||||
timelineService = timelineServiceFactory.create(roomId),
|
timelineService = timelineServiceFactory.create(roomId),
|
||||||
|
threadsService = threadsServiceFactory.create(roomId),
|
||||||
sendService = sendServiceFactory.create(roomId),
|
sendService = sendServiceFactory.create(roomId),
|
||||||
draftService = draftServiceFactory.create(roomId),
|
draftService = draftServiceFactory.create(roomId),
|
||||||
stateService = stateServiceFactory.create(roomId),
|
stateService = stateServiceFactory.create(roomId),
|
||||||
|
@ -77,6 +77,8 @@ import org.matrix.android.sdk.internal.session.room.relation.DefaultUpdateQuickR
|
|||||||
import org.matrix.android.sdk.internal.session.room.relation.FetchEditHistoryTask
|
import org.matrix.android.sdk.internal.session.room.relation.FetchEditHistoryTask
|
||||||
import org.matrix.android.sdk.internal.session.room.relation.FindReactionEventForUndoTask
|
import org.matrix.android.sdk.internal.session.room.relation.FindReactionEventForUndoTask
|
||||||
import org.matrix.android.sdk.internal.session.room.relation.UpdateQuickReactionTask
|
import org.matrix.android.sdk.internal.session.room.relation.UpdateQuickReactionTask
|
||||||
|
import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadTimelineTask
|
||||||
|
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
|
||||||
import org.matrix.android.sdk.internal.session.room.reporting.DefaultReportContentTask
|
import org.matrix.android.sdk.internal.session.room.reporting.DefaultReportContentTask
|
||||||
import org.matrix.android.sdk.internal.session.room.reporting.ReportContentTask
|
import org.matrix.android.sdk.internal.session.room.reporting.ReportContentTask
|
||||||
import org.matrix.android.sdk.internal.session.room.state.DefaultSendStateTask
|
import org.matrix.android.sdk.internal.session.room.state.DefaultSendStateTask
|
||||||
@ -289,4 +291,7 @@ internal abstract class RoomModule {
|
|||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindGetRoomSummaryTask(task: DefaultGetRoomSummaryTask): GetRoomSummaryTask
|
abstract fun bindGetRoomSummaryTask(task: DefaultGetRoomSummaryTask): GetRoomSummaryTask
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindFetchThreadTimelineTask(task: DefaultFetchThreadTimelineTask): FetchThreadTimelineTask
|
||||||
}
|
}
|
||||||
|
@ -83,7 +83,9 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
val modified = unsignedData.copy(redactedEvent = redactionEvent)
|
val modified = unsignedData.copy(redactedEvent = redactionEvent)
|
||||||
eventToPrune.content = ContentMapper.map(emptyMap())
|
// I Commented the line below, it should not be empty while we lose all the previous info about
|
||||||
|
// the redacted event
|
||||||
|
// eventToPrune.content = ContentMapper.map(emptyMap())
|
||||||
eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified)
|
eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified)
|
||||||
eventToPrune.decryptionResultJson = null
|
eventToPrune.decryptionResultJson = null
|
||||||
eventToPrune.decryptionErrorCode = null
|
eventToPrune.decryptionErrorCode = null
|
||||||
|
@ -21,7 +21,6 @@ import com.zhuinden.monarchy.Monarchy
|
|||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
|
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.PollType
|
import org.matrix.android.sdk.api.session.room.model.message.PollType
|
||||||
@ -38,10 +37,9 @@ import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEnt
|
|||||||
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
|
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
|
||||||
import org.matrix.android.sdk.internal.database.query.where
|
import org.matrix.android.sdk.internal.database.query.where
|
||||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||||
|
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
|
||||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
|
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
|
||||||
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
|
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
|
||||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
|
||||||
import org.matrix.android.sdk.internal.task.configureWith
|
|
||||||
import org.matrix.android.sdk.internal.util.fetchCopyMap
|
import org.matrix.android.sdk.internal.util.fetchCopyMap
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@ -53,10 +51,10 @@ internal class DefaultRelationService @AssistedInject constructor(
|
|||||||
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
|
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
|
||||||
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
|
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
|
||||||
private val fetchEditHistoryTask: FetchEditHistoryTask,
|
private val fetchEditHistoryTask: FetchEditHistoryTask,
|
||||||
|
private val fetchThreadTimelineTask: FetchThreadTimelineTask,
|
||||||
private val timelineEventMapper: TimelineEventMapper,
|
private val timelineEventMapper: TimelineEventMapper,
|
||||||
@SessionDatabase private val monarchy: Monarchy,
|
@SessionDatabase private val monarchy: Monarchy
|
||||||
private val taskExecutor: TaskExecutor) :
|
) : RelationService {
|
||||||
RelationService {
|
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
@ -78,39 +76,31 @@ internal class DefaultRelationService @AssistedInject constructor(
|
|||||||
.none { it.addedByMe && it.key == reaction }) {
|
.none { it.addedByMe && it.key == reaction }) {
|
||||||
val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction)
|
val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction)
|
||||||
.also { saveLocalEcho(it) }
|
.also { saveLocalEcho(it) }
|
||||||
return eventSenderProcessor.postEvent(event, false /* reaction are not encrypted*/)
|
eventSenderProcessor.postEvent(event, false /* reaction are not encrypted*/)
|
||||||
} else {
|
} else {
|
||||||
Timber.w("Reaction already added")
|
Timber.w("Reaction already added")
|
||||||
NoOpCancellable
|
NoOpCancellable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun undoReaction(targetEventId: String, reaction: String): Cancelable {
|
override suspend fun undoReaction(targetEventId: String, reaction: String): Cancelable {
|
||||||
val params = FindReactionEventForUndoTask.Params(
|
val params = FindReactionEventForUndoTask.Params(
|
||||||
roomId,
|
roomId,
|
||||||
targetEventId,
|
targetEventId,
|
||||||
reaction
|
reaction
|
||||||
)
|
)
|
||||||
// TODO We should avoid using MatrixCallback internally
|
|
||||||
val callback = object : MatrixCallback<FindReactionEventForUndoTask.Result> {
|
val data = findReactionEventForUndoTask.executeRetry(params, Int.MAX_VALUE)
|
||||||
override fun onSuccess(data: FindReactionEventForUndoTask.Result) {
|
|
||||||
if (data.redactEventId == null) {
|
return if (data.redactEventId == null) {
|
||||||
Timber.w("Cannot find reaction to undo (not yet synced?)")
|
Timber.w("Cannot find reaction to undo (not yet synced?)")
|
||||||
// TODO?
|
// TODO?
|
||||||
}
|
NoOpCancellable
|
||||||
data.redactEventId?.let { toRedact ->
|
} else {
|
||||||
val redactEvent = eventFactory.createRedactEvent(roomId, toRedact, null)
|
val redactEvent = eventFactory.createRedactEvent(roomId, data.redactEventId, null)
|
||||||
.also { saveLocalEcho(it) }
|
.also { saveLocalEcho(it) }
|
||||||
eventSenderProcessor.postRedaction(redactEvent, null)
|
eventSenderProcessor.postRedaction(redactEvent, null)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return findReactionEventForUndoTask
|
|
||||||
.configureWith(params) {
|
|
||||||
this.retryCount = Int.MAX_VALUE
|
|
||||||
this.callback = callback
|
|
||||||
}
|
|
||||||
.executeBy(taskExecutor)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun editPoll(targetEvent: TimelineEvent,
|
override fun editPoll(targetEvent: TimelineEvent,
|
||||||
@ -139,8 +129,20 @@ internal class DefaultRelationService @AssistedInject constructor(
|
|||||||
return fetchEditHistoryTask.execute(FetchEditHistoryTask.Params(roomId, eventId))
|
return fetchEditHistoryTask.execute(FetchEditHistoryTask.Params(roomId, eventId))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun replyToMessage(eventReplied: TimelineEvent, replyText: CharSequence, autoMarkdown: Boolean): Cancelable? {
|
override fun replyToMessage(
|
||||||
val event = eventFactory.createReplyTextEvent(roomId, eventReplied, replyText, autoMarkdown)
|
eventReplied: TimelineEvent,
|
||||||
|
replyText: CharSequence,
|
||||||
|
autoMarkdown: Boolean,
|
||||||
|
showInThread: Boolean,
|
||||||
|
rootThreadEventId: String?
|
||||||
|
): Cancelable? {
|
||||||
|
val event = eventFactory.createReplyTextEvent(
|
||||||
|
roomId = roomId,
|
||||||
|
eventReplied = eventReplied,
|
||||||
|
replyText = replyText,
|
||||||
|
autoMarkdown = autoMarkdown,
|
||||||
|
rootThreadEventId = rootThreadEventId,
|
||||||
|
showInThread = showInThread)
|
||||||
?.also { saveLocalEcho(it) }
|
?.also { saveLocalEcho(it) }
|
||||||
?: return null
|
?: return null
|
||||||
|
|
||||||
@ -166,6 +168,47 @@ internal class DefaultRelationService @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun replyInThread(
|
||||||
|
rootThreadEventId: String,
|
||||||
|
replyInThreadText: CharSequence,
|
||||||
|
msgType: String,
|
||||||
|
autoMarkdown: Boolean,
|
||||||
|
formattedText: String?,
|
||||||
|
eventReplied: TimelineEvent?): Cancelable? {
|
||||||
|
val event = if (eventReplied != null) {
|
||||||
|
// Reply within a thread
|
||||||
|
eventFactory.createReplyTextEvent(
|
||||||
|
roomId = roomId,
|
||||||
|
eventReplied = eventReplied,
|
||||||
|
replyText = replyInThreadText,
|
||||||
|
autoMarkdown = autoMarkdown,
|
||||||
|
rootThreadEventId = rootThreadEventId,
|
||||||
|
showInThread = false
|
||||||
|
)
|
||||||
|
?.also {
|
||||||
|
saveLocalEcho(it)
|
||||||
|
}
|
||||||
|
?: return null
|
||||||
|
} else {
|
||||||
|
// Normal thread reply
|
||||||
|
eventFactory.createThreadTextEvent(
|
||||||
|
rootThreadEventId = rootThreadEventId,
|
||||||
|
roomId = roomId,
|
||||||
|
text = replyInThreadText,
|
||||||
|
msgType = msgType,
|
||||||
|
autoMarkdown = autoMarkdown,
|
||||||
|
formattedText = formattedText)
|
||||||
|
.also {
|
||||||
|
saveLocalEcho(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun fetchThreadTimeline(rootThreadEventId: String): Boolean {
|
||||||
|
return fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(roomId, rootThreadEventId))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the event in database as a local echo.
|
* Saves the event in database as a local echo.
|
||||||
* SendState is set to UNSENT and it's added to a the sendingTimelineEvents list of the room.
|
* SendState is set to UNSENT and it's added to a the sendingTimelineEvents list of the room.
|
||||||
|
@ -97,7 +97,13 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor:
|
|||||||
val roomId = replyToEdit.roomId
|
val roomId = replyToEdit.roomId
|
||||||
if (replyToEdit.root.sendState.hasFailed()) {
|
if (replyToEdit.root.sendState.hasFailed()) {
|
||||||
// We create a new in memory event for the EventSenderProcessor but we keep the eventId of the failed event.
|
// We create a new in memory event for the EventSenderProcessor but we keep the eventId of the failed event.
|
||||||
val editedEvent = eventFactory.createReplyTextEvent(roomId, originalTimelineEvent, newBodyText, false)?.copy(
|
val editedEvent = eventFactory.createReplyTextEvent(
|
||||||
|
roomId = roomId,
|
||||||
|
eventReplied = originalTimelineEvent,
|
||||||
|
replyText = newBodyText,
|
||||||
|
autoMarkdown = false,
|
||||||
|
showInThread = false
|
||||||
|
)?.copy(
|
||||||
eventId = replyToEdit.eventId
|
eventId = replyToEdit.eventId
|
||||||
) ?: return NoOpCancellable
|
) ?: return NoOpCancellable
|
||||||
updateFailedEchoWithEvent(roomId, replyToEdit.eventId, editedEvent)
|
updateFailedEchoWithEvent(roomId, replyToEdit.eventId, editedEvent)
|
||||||
|
@ -0,0 +1,207 @@
|
|||||||
|
/*
|
||||||
|
* 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.relation.threads
|
||||||
|
|
||||||
|
import com.zhuinden.monarchy.Monarchy
|
||||||
|
import io.realm.Realm
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||||
|
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
|
||||||
|
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
|
||||||
|
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||||
|
import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
|
||||||
|
import org.matrix.android.sdk.internal.database.helper.updateThreadSummaryIfNeeded
|
||||||
|
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
||||||
|
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.CurrentStateEventEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.EventInsertType
|
||||||
|
import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
|
||||||
|
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.getOrNull
|
||||||
|
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.network.GlobalErrorReceiver
|
||||||
|
import org.matrix.android.sdk.internal.network.executeRequest
|
||||||
|
import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent
|
||||||
|
import org.matrix.android.sdk.internal.session.room.RoomAPI
|
||||||
|
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
|
||||||
|
import org.matrix.android.sdk.internal.task.Task
|
||||||
|
import org.matrix.android.sdk.internal.util.awaitTransaction
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal interface FetchThreadTimelineTask : Task<FetchThreadTimelineTask.Params, Boolean> {
|
||||||
|
data class Params(
|
||||||
|
val roomId: String,
|
||||||
|
val rootThreadEventId: String
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DefaultFetchThreadTimelineTask @Inject constructor(
|
||||||
|
private val roomAPI: RoomAPI,
|
||||||
|
private val globalErrorReceiver: GlobalErrorReceiver,
|
||||||
|
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
|
||||||
|
@SessionDatabase private val monarchy: Monarchy,
|
||||||
|
@UserId private val userId: String,
|
||||||
|
private val cryptoService: DefaultCryptoService
|
||||||
|
) : FetchThreadTimelineTask {
|
||||||
|
|
||||||
|
override suspend fun execute(params: FetchThreadTimelineTask.Params): Boolean {
|
||||||
|
val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId)
|
||||||
|
val response = executeRequest(globalErrorReceiver) {
|
||||||
|
roomAPI.getRelations(
|
||||||
|
roomId = params.roomId,
|
||||||
|
eventId = params.rootThreadEventId,
|
||||||
|
relationType = RelationType.IO_THREAD,
|
||||||
|
eventType = if (isRoomEncrypted) EventType.ENCRYPTED else EventType.MESSAGE,
|
||||||
|
limit = 2000
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val threadList = response.chunks + listOfNotNull(response.originalEvent)
|
||||||
|
|
||||||
|
return storeNewEventsIfNeeded(threadList, params.roomId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store new events if they are not already received, and returns weather or not,
|
||||||
|
* a timeline update should be made
|
||||||
|
* @param threadList is the list containing the thread replies
|
||||||
|
* @param roomId the roomId of the the thread
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private suspend fun storeNewEventsIfNeeded(threadList: List<Event>, roomId: String): Boolean {
|
||||||
|
var eventsSkipped = 0
|
||||||
|
monarchy
|
||||||
|
.awaitTransaction { realm ->
|
||||||
|
val chunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)
|
||||||
|
|
||||||
|
val optimizedThreadSummaryMap = hashMapOf<String, EventEntity>()
|
||||||
|
val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
|
||||||
|
|
||||||
|
for (event in threadList.reversed()) {
|
||||||
|
if (event.eventId == null || event.senderId == null || event.type == null) {
|
||||||
|
eventsSkipped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EventEntity.where(realm, event.eventId).findFirst() != null) {
|
||||||
|
// Skip if event already exists
|
||||||
|
eventsSkipped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (event.isEncrypted()) {
|
||||||
|
// Decrypt events that will be stored
|
||||||
|
decryptIfNeeded(event, roomId)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleReaction(realm, event, roomId)
|
||||||
|
|
||||||
|
val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
|
||||||
|
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC)
|
||||||
|
|
||||||
|
// Sender info
|
||||||
|
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
|
||||||
|
rootStateEvent?.asDomain()?.getFixedRoomMemberContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk?.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
|
||||||
|
eventEntity.rootThreadEventId?.let {
|
||||||
|
// This is a thread event
|
||||||
|
optimizedThreadSummaryMap[it] = eventEntity
|
||||||
|
} ?: run {
|
||||||
|
// This is a normal event or a root thread one
|
||||||
|
optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(
|
||||||
|
roomId = roomId,
|
||||||
|
realm = realm,
|
||||||
|
currentUserId = userId,
|
||||||
|
shouldUpdateNotifications = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Timber.i("----> size: ${threadList.size} | skipped: $eventsSkipped | threads: ${threadList.map { it.eventId }}")
|
||||||
|
|
||||||
|
return eventsSkipped == threadList.size
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoke the event decryption mechanism for a specific event
|
||||||
|
*/
|
||||||
|
|
||||||
|
private fun decryptIfNeeded(event: Event, roomId: String) {
|
||||||
|
try {
|
||||||
|
// Event from sync does not have roomId, so add it to the event first
|
||||||
|
val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "")
|
||||||
|
event.mxDecryptionResult = OlmDecryptionResult(
|
||||||
|
payload = result.clearEvent,
|
||||||
|
senderKey = result.senderCurve25519Key,
|
||||||
|
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
|
||||||
|
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||||
|
)
|
||||||
|
} catch (e: MXCryptoError) {
|
||||||
|
if (e is MXCryptoError.Base) {
|
||||||
|
event.mCryptoError = e.errorType
|
||||||
|
event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleReaction(realm: Realm,
|
||||||
|
event: Event,
|
||||||
|
roomId: String) {
|
||||||
|
val unsignedData = event.unsignedData ?: return
|
||||||
|
val relatedEventId = event.eventId ?: return
|
||||||
|
|
||||||
|
unsignedData.relations?.annotations?.chunk?.forEach { relationChunk ->
|
||||||
|
|
||||||
|
if (relationChunk.type == EventType.REACTION) {
|
||||||
|
val reaction = relationChunk.key
|
||||||
|
Timber.i("----> Annotation found in ${event.eventId} ${relationChunk.key} ")
|
||||||
|
|
||||||
|
val eventSummary = EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, relatedEventId)
|
||||||
|
var sum = eventSummary.reactionsSummary.find { it.key == reaction }
|
||||||
|
|
||||||
|
if (sum == null) {
|
||||||
|
sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java)
|
||||||
|
sum.key = reaction
|
||||||
|
sum.firstTimestamp = event.originServerTs ?: 0
|
||||||
|
Timber.v("Adding synced reaction $reaction")
|
||||||
|
sum.count = 1
|
||||||
|
// reactionEventId not included in the /relations API
|
||||||
|
// sum.sourceEvents.add(reactionEventId)
|
||||||
|
eventSummary.reactionsSummary.add(sum)
|
||||||
|
} else {
|
||||||
|
sum.count += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -98,8 +98,14 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||||||
.let { sendEvent(it) }
|
.let { sendEvent(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun sendQuotedTextMessage(quotedEvent: TimelineEvent, text: String, autoMarkdown: Boolean): Cancelable {
|
override fun sendQuotedTextMessage(quotedEvent: TimelineEvent, text: String, autoMarkdown: Boolean, rootThreadEventId: String?): Cancelable {
|
||||||
return localEchoEventFactory.createQuotedTextEvent(roomId, quotedEvent, text, autoMarkdown)
|
return localEchoEventFactory.createQuotedTextEvent(
|
||||||
|
roomId = roomId,
|
||||||
|
quotedEvent = quotedEvent,
|
||||||
|
text = text,
|
||||||
|
autoMarkdown = autoMarkdown,
|
||||||
|
rootThreadEventId = rootThreadEventId
|
||||||
|
)
|
||||||
.also { createLocalEcho(it) }
|
.also { createLocalEcho(it) }
|
||||||
.let { sendEvent(it) }
|
.let { sendEvent(it) }
|
||||||
}
|
}
|
||||||
@ -254,22 +260,37 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||||||
|
|
||||||
override fun sendMedias(attachments: List<ContentAttachmentData>,
|
override fun sendMedias(attachments: List<ContentAttachmentData>,
|
||||||
compressBeforeSending: Boolean,
|
compressBeforeSending: Boolean,
|
||||||
roomIds: Set<String>): Cancelable {
|
roomIds: Set<String>,
|
||||||
|
rootThreadEventId: String?
|
||||||
|
): Cancelable {
|
||||||
return attachments.mapTo(CancelableBag()) {
|
return attachments.mapTo(CancelableBag()) {
|
||||||
sendMedia(it, compressBeforeSending, roomIds)
|
sendMedia(
|
||||||
|
attachment = it,
|
||||||
|
compressBeforeSending = compressBeforeSending,
|
||||||
|
roomIds = roomIds,
|
||||||
|
rootThreadEventId = rootThreadEventId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun sendMedia(attachment: ContentAttachmentData,
|
override fun sendMedia(attachment: ContentAttachmentData,
|
||||||
compressBeforeSending: Boolean,
|
compressBeforeSending: Boolean,
|
||||||
roomIds: Set<String>): Cancelable {
|
roomIds: Set<String>,
|
||||||
|
rootThreadEventId: String?
|
||||||
|
): Cancelable {
|
||||||
|
// Ensure that the event will not be send in a thread if we are a different flow.
|
||||||
|
// Like sending files to multiple rooms
|
||||||
|
val rootThreadId = if (roomIds.isNotEmpty()) null else rootThreadEventId
|
||||||
|
|
||||||
// Create an event with the media file path
|
// Create an event with the media file path
|
||||||
// Ensure current roomId is included in the set
|
// Ensure current roomId is included in the set
|
||||||
val allRoomIds = (roomIds + roomId).toList()
|
val allRoomIds = (roomIds + roomId).toList()
|
||||||
|
|
||||||
// Create local echo for each room
|
// Create local echo for each room
|
||||||
val allLocalEchoes = allRoomIds.map {
|
val allLocalEchoes = allRoomIds.map {
|
||||||
localEchoEventFactory.createMediaEvent(it, attachment).also { event ->
|
localEchoEventFactory.createMediaEvent(
|
||||||
|
roomId = it,
|
||||||
|
attachment = attachment,
|
||||||
|
rootThreadEventId = rootThreadId).also { event ->
|
||||||
createLocalEcho(event)
|
createLocalEcho(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ import org.matrix.android.sdk.api.session.events.model.LocalEcho
|
|||||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||||
import org.matrix.android.sdk.api.session.events.model.UnsignedData
|
import org.matrix.android.sdk.api.session.events.model.UnsignedData
|
||||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.AudioInfo
|
import org.matrix.android.sdk.api.session.room.model.message.AudioInfo
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.AudioWaveformInfo
|
import org.matrix.android.sdk.api.session.room.model.message.AudioWaveformInfo
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.FileInfo
|
import org.matrix.android.sdk.api.session.room.model.message.FileInfo
|
||||||
@ -45,6 +46,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
|
|||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
|
||||||
@ -292,13 +294,16 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createMediaEvent(roomId: String, attachment: ContentAttachmentData): Event {
|
fun createMediaEvent(roomId: String,
|
||||||
|
attachment: ContentAttachmentData,
|
||||||
|
rootThreadEventId: String?
|
||||||
|
): Event {
|
||||||
return when (attachment.type) {
|
return when (attachment.type) {
|
||||||
ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment)
|
ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment, rootThreadEventId)
|
||||||
ContentAttachmentData.Type.VIDEO -> createVideoEvent(roomId, attachment)
|
ContentAttachmentData.Type.VIDEO -> createVideoEvent(roomId, attachment, rootThreadEventId)
|
||||||
ContentAttachmentData.Type.AUDIO -> createAudioEvent(roomId, attachment, isVoiceMessage = false)
|
ContentAttachmentData.Type.AUDIO -> createAudioEvent(roomId, attachment, isVoiceMessage = false, rootThreadEventId = rootThreadEventId)
|
||||||
ContentAttachmentData.Type.VOICE_MESSAGE -> createAudioEvent(roomId, attachment, isVoiceMessage = true)
|
ContentAttachmentData.Type.VOICE_MESSAGE -> createAudioEvent(roomId, attachment, isVoiceMessage = true, rootThreadEventId = rootThreadEventId)
|
||||||
ContentAttachmentData.Type.FILE -> createFileEvent(roomId, attachment)
|
ContentAttachmentData.Type.FILE -> createFileEvent(roomId, attachment, rootThreadEventId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -321,7 +326,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
unsignedData = UnsignedData(age = null, transactionId = localId))
|
unsignedData = UnsignedData(age = null, transactionId = localId))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createImageEvent(roomId: String, attachment: ContentAttachmentData): Event {
|
private fun createImageEvent(roomId: String, attachment: ContentAttachmentData, rootThreadEventId: String?): Event {
|
||||||
var width = attachment.width
|
var width = attachment.width
|
||||||
var height = attachment.height
|
var height = attachment.height
|
||||||
|
|
||||||
@ -345,12 +350,19 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
height = height?.toInt() ?: 0,
|
height = height?.toInt() ?: 0,
|
||||||
size = attachment.size
|
size = attachment.size
|
||||||
),
|
),
|
||||||
url = attachment.queryUri.toString()
|
url = attachment.queryUri.toString(),
|
||||||
|
relatesTo = rootThreadEventId?.let {
|
||||||
|
RelationDefaultContent(
|
||||||
|
type = RelationType.IO_THREAD,
|
||||||
|
eventId = it,
|
||||||
|
inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
return createMessageEvent(roomId, content)
|
return createMessageEvent(roomId, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createVideoEvent(roomId: String, attachment: ContentAttachmentData): Event {
|
private fun createVideoEvent(roomId: String, attachment: ContentAttachmentData, rootThreadEventId: String?): Event {
|
||||||
val mediaDataRetriever = MediaMetadataRetriever()
|
val mediaDataRetriever = MediaMetadataRetriever()
|
||||||
mediaDataRetriever.setDataSource(context, attachment.queryUri)
|
mediaDataRetriever.setDataSource(context, attachment.queryUri)
|
||||||
|
|
||||||
@ -381,12 +393,23 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
thumbnailUrl = attachment.queryUri.toString(),
|
thumbnailUrl = attachment.queryUri.toString(),
|
||||||
thumbnailInfo = thumbnailInfo
|
thumbnailInfo = thumbnailInfo
|
||||||
),
|
),
|
||||||
url = attachment.queryUri.toString()
|
url = attachment.queryUri.toString(),
|
||||||
|
relatesTo = rootThreadEventId?.let {
|
||||||
|
RelationDefaultContent(
|
||||||
|
type = RelationType.IO_THREAD,
|
||||||
|
eventId = it,
|
||||||
|
inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
return createMessageEvent(roomId, content)
|
return createMessageEvent(roomId, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createAudioEvent(roomId: String, attachment: ContentAttachmentData, isVoiceMessage: Boolean): Event {
|
private fun createAudioEvent(roomId: String,
|
||||||
|
attachment: ContentAttachmentData,
|
||||||
|
isVoiceMessage: Boolean,
|
||||||
|
rootThreadEventId: String?
|
||||||
|
): Event {
|
||||||
val content = MessageAudioContent(
|
val content = MessageAudioContent(
|
||||||
msgType = MessageType.MSGTYPE_AUDIO,
|
msgType = MessageType.MSGTYPE_AUDIO,
|
||||||
body = attachment.name ?: "audio",
|
body = attachment.name ?: "audio",
|
||||||
@ -400,12 +423,19 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
duration = attachment.duration?.toInt(),
|
duration = attachment.duration?.toInt(),
|
||||||
waveform = waveformSanitizer.sanitize(attachment.waveform)
|
waveform = waveformSanitizer.sanitize(attachment.waveform)
|
||||||
),
|
),
|
||||||
voiceMessageIndicator = if (!isVoiceMessage) null else emptyMap()
|
voiceMessageIndicator = if (!isVoiceMessage) null else emptyMap(),
|
||||||
|
relatesTo = rootThreadEventId?.let {
|
||||||
|
RelationDefaultContent(
|
||||||
|
type = RelationType.IO_THREAD,
|
||||||
|
eventId = it,
|
||||||
|
inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
return createMessageEvent(roomId, content)
|
return createMessageEvent(roomId, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createFileEvent(roomId: String, attachment: ContentAttachmentData): Event {
|
private fun createFileEvent(roomId: String, attachment: ContentAttachmentData, rootThreadEventId: String?): Event {
|
||||||
val content = MessageFileContent(
|
val content = MessageFileContent(
|
||||||
msgType = MessageType.MSGTYPE_FILE,
|
msgType = MessageType.MSGTYPE_FILE,
|
||||||
body = attachment.name ?: "file",
|
body = attachment.name ?: "file",
|
||||||
@ -413,7 +443,14 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() },
|
mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() },
|
||||||
size = attachment.size
|
size = attachment.size
|
||||||
),
|
),
|
||||||
url = attachment.queryUri.toString()
|
url = attachment.queryUri.toString(),
|
||||||
|
relatesTo = rootThreadEventId?.let {
|
||||||
|
RelationDefaultContent(
|
||||||
|
type = RelationType.IO_THREAD,
|
||||||
|
eventId = it,
|
||||||
|
inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
return createMessageEvent(roomId, content)
|
return createMessageEvent(roomId, content)
|
||||||
}
|
}
|
||||||
@ -423,6 +460,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun createEvent(roomId: String, type: String, content: Content?): Event {
|
fun createEvent(roomId: String, type: String, content: Content?): Event {
|
||||||
|
val newContent = enhanceStickerIfNeeded(type, content) ?: content
|
||||||
val localId = LocalEcho.createLocalEchoId()
|
val localId = LocalEcho.createLocalEchoId()
|
||||||
return Event(
|
return Event(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
@ -430,19 +468,65 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
senderId = userId,
|
senderId = userId,
|
||||||
eventId = localId,
|
eventId = localId,
|
||||||
type = type,
|
type = type,
|
||||||
content = content,
|
content = newContent,
|
||||||
unsignedData = UnsignedData(age = null, transactionId = localId)
|
unsignedData = UnsignedData(age = null, transactionId = localId)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enhance sticker to support threads fallback if needed
|
||||||
|
*/
|
||||||
|
private fun enhanceStickerIfNeeded(type: String, content: Content?): Content? {
|
||||||
|
var newContent: Content? = null
|
||||||
|
if (type == EventType.STICKER) {
|
||||||
|
val isThread = (content.toModel<MessageStickerContent>())?.relatesTo?.type == RelationType.IO_THREAD
|
||||||
|
val rootThreadEventId = (content.toModel<MessageStickerContent>())?.relatesTo?.eventId
|
||||||
|
if (isThread && rootThreadEventId != null) {
|
||||||
|
val newRelationalDefaultContent = (content.toModel<MessageStickerContent>())?.relatesTo?.copy(
|
||||||
|
inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId))
|
||||||
|
)
|
||||||
|
newContent = (content.toModel<MessageStickerContent>())?.copy(
|
||||||
|
relatesTo = newRelationalDefaultContent
|
||||||
|
).toContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newContent
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a thread event related to the already existing root event
|
||||||
|
*/
|
||||||
|
fun createThreadTextEvent(
|
||||||
|
rootThreadEventId: String,
|
||||||
|
roomId: String,
|
||||||
|
text: CharSequence,
|
||||||
|
msgType: String,
|
||||||
|
autoMarkdown: Boolean,
|
||||||
|
formattedText: String?): Event {
|
||||||
|
val content = formattedText?.let { TextContent(text.toString(), it) } ?: createTextContent(text, autoMarkdown)
|
||||||
|
return createEvent(
|
||||||
|
roomId,
|
||||||
|
EventType.MESSAGE,
|
||||||
|
content.toThreadTextContent(
|
||||||
|
rootThreadEventId = rootThreadEventId,
|
||||||
|
latestThreadEventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId),
|
||||||
|
msgType = msgType)
|
||||||
|
.toContent())
|
||||||
|
}
|
||||||
|
|
||||||
private fun dummyOriginServerTs(): Long {
|
private fun dummyOriginServerTs(): Long {
|
||||||
return System.currentTimeMillis()
|
return System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a reply to a regular timeline Event or a thread Event if needed
|
||||||
|
*/
|
||||||
fun createReplyTextEvent(roomId: String,
|
fun createReplyTextEvent(roomId: String,
|
||||||
eventReplied: TimelineEvent,
|
eventReplied: TimelineEvent,
|
||||||
replyText: CharSequence,
|
replyText: CharSequence,
|
||||||
autoMarkdown: Boolean): Event? {
|
autoMarkdown: Boolean,
|
||||||
|
rootThreadEventId: String? = null,
|
||||||
|
showInThread: Boolean): Event? {
|
||||||
// Fallbacks and event representation
|
// Fallbacks and event representation
|
||||||
// TODO Add error/warning logs when any of this is null
|
// TODO Add error/warning logs when any of this is null
|
||||||
val permalink = permalinkFactory.createPermalink(eventReplied.root, false) ?: return null
|
val permalink = permalinkFactory.createPermalink(eventReplied.root, false) ?: return null
|
||||||
@ -473,11 +557,33 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
format = MessageFormat.FORMAT_MATRIX_HTML,
|
format = MessageFormat.FORMAT_MATRIX_HTML,
|
||||||
body = replyFallback,
|
body = replyFallback,
|
||||||
formattedBody = replyFormatted,
|
formattedBody = replyFormatted,
|
||||||
relatesTo = RelationDefaultContent(null, null, ReplyToContent(eventId))
|
relatesTo = generateReplyRelationContent(
|
||||||
)
|
eventId = eventId,
|
||||||
|
rootThreadEventId = rootThreadEventId,
|
||||||
|
showAsReply = showInThread))
|
||||||
return createMessageEvent(roomId, content)
|
return createMessageEvent(roomId, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the appropriate relatesTo object for a reply event.
|
||||||
|
* It can either be a regular reply or a reply within a thread
|
||||||
|
* "m.relates_to": {
|
||||||
|
* "rel_type": "m.thread",
|
||||||
|
* "event_id": "$thread_root",
|
||||||
|
* "m.in_reply_to": {
|
||||||
|
* "event_id": "$event_target",
|
||||||
|
* "render_in": ["m.thread"]
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
private fun generateReplyRelationContent(eventId: String, rootThreadEventId: String? = null, showAsReply: Boolean): RelationDefaultContent =
|
||||||
|
rootThreadEventId?.let {
|
||||||
|
RelationDefaultContent(
|
||||||
|
type = RelationType.IO_THREAD,
|
||||||
|
eventId = it,
|
||||||
|
inReplyTo = ReplyToContent(eventId = eventId, renderIn = if (showAsReply) arrayListOf("m.thread") else null))
|
||||||
|
} ?: RelationDefaultContent(null, null, ReplyToContent(eventId = eventId))
|
||||||
|
|
||||||
private fun buildFormattedReply(permalink: String, userLink: String, userId: String, bodyFormatted: String, newBodyFormatted: String): String {
|
private fun buildFormattedReply(permalink: String, userLink: String, userId: String, bodyFormatted: String, newBodyFormatted: String): String {
|
||||||
return REPLY_PATTERN.format(
|
return REPLY_PATTERN.format(
|
||||||
permalink,
|
permalink,
|
||||||
@ -488,6 +594,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
newBodyFormatted
|
newBodyFormatted
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildReplyFallback(body: TextContent, originalSenderId: String?, newBodyText: String): String {
|
private fun buildReplyFallback(body: TextContent, originalSenderId: String?, newBodyText: String): String {
|
||||||
return buildString {
|
return buildString {
|
||||||
append("> <")
|
append("> <")
|
||||||
@ -593,11 +700,28 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
quotedEvent: TimelineEvent,
|
quotedEvent: TimelineEvent,
|
||||||
text: String,
|
text: String,
|
||||||
autoMarkdown: Boolean,
|
autoMarkdown: Boolean,
|
||||||
|
rootThreadEventId: String?
|
||||||
): Event {
|
): Event {
|
||||||
val messageContent = quotedEvent.getLastMessageContent()
|
val messageContent = quotedEvent.getLastMessageContent()
|
||||||
val textMsg = messageContent?.body
|
val textMsg = messageContent?.body
|
||||||
val quoteText = legacyRiotQuoteText(textMsg, text)
|
val quoteText = legacyRiotQuoteText(textMsg, text)
|
||||||
return createFormattedTextEvent(roomId, markdownParser.parse(quoteText, force = true, advanced = autoMarkdown), MessageType.MSGTYPE_TEXT)
|
|
||||||
|
return if (rootThreadEventId != null) {
|
||||||
|
createMessageEvent(
|
||||||
|
roomId,
|
||||||
|
markdownParser
|
||||||
|
.parse(quoteText, force = true, advanced = autoMarkdown)
|
||||||
|
.toThreadTextContent(
|
||||||
|
rootThreadEventId = rootThreadEventId,
|
||||||
|
latestThreadEventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId),
|
||||||
|
msgType = MessageType.MSGTYPE_TEXT)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
createFormattedTextEvent(
|
||||||
|
roomId,
|
||||||
|
markdownParser.parse(quoteText, force = true, advanced = autoMarkdown),
|
||||||
|
MessageType.MSGTYPE_TEXT)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun legacyRiotQuoteText(quotedText: String?, myText: String): String {
|
private fun legacyRiotQuoteText(quotedText: String?, myText: String): String {
|
||||||
@ -631,6 +755,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
// </mx-reply>
|
// </mx-reply>
|
||||||
// No whitespace because currently breaks temporary formatted text to Span
|
// No whitespace because currently breaks temporary formatted text to Span
|
||||||
const val REPLY_PATTERN = """<mx-reply><blockquote><a href="%s">In reply to</a> <a href="%s">%s</a><br />%s</blockquote></mx-reply>%s"""
|
const val REPLY_PATTERN = """<mx-reply><blockquote><a href="%s">In reply to</a> <a href="%s">%s</a><br />%s</blockquote></mx-reply>%s"""
|
||||||
|
const val QUOTE_PATTERN = """<blockquote><p>%s</p></blockquote><p>%s</p>"""
|
||||||
|
|
||||||
// This is used to replace inner mx-reply tags
|
// This is used to replace inner mx-reply tags
|
||||||
val MX_REPLY_REGEX = "<mx-reply>.*</mx-reply>".toRegex()
|
val MX_REPLY_REGEX = "<mx-reply>.*</mx-reply>".toRegex()
|
||||||
|
@ -138,7 +138,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteFailedEchoAsync(roomId: String, eventId: String?) {
|
fun deleteFailedEchoAsync(roomId: String, eventId: String?) {
|
||||||
monarchy.runTransactionSync { realm ->
|
monarchy.runTransactionSync { realm ->
|
||||||
TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId ?: "").findFirst()?.deleteFromRealm()
|
TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId ?: "").findFirst()?.deleteFromRealm()
|
||||||
EventEntity.where(realm, eventId = eventId ?: "").findFirst()?.deleteFromRealm()
|
EventEntity.where(realm, eventId = eventId ?: "").findFirst()?.deleteFromRealm()
|
||||||
@ -215,4 +215,13 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the latest known thread event message, or the rootThreadEventId if no other event found
|
||||||
|
*/
|
||||||
|
fun getLatestThreadEvent(rootThreadEventId: String): String {
|
||||||
|
return realmSessionProvider.withRealm { realm ->
|
||||||
|
EventEntity.where(realm, eventId = rootThreadEventId).findFirst()?.threadSummaryLatestMessage?.eventId
|
||||||
|
} ?: rootThreadEventId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,9 +16,12 @@
|
|||||||
|
|
||||||
package org.matrix.android.sdk.internal.session.room.send
|
package org.matrix.android.sdk.internal.session.room.send
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
|
import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.relation.ReplyToContent
|
||||||
import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromHtmlReply
|
import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromHtmlReply
|
||||||
import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromReply
|
import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromReply
|
||||||
|
|
||||||
@ -41,6 +44,29 @@ fun TextContent.toMessageTextContent(msgType: String = MessageType.MSGTYPE_TEXT)
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform a TextContent to a thread message content. It will also add the inReplyTo
|
||||||
|
* latestThreadEventId in order for the clients without threads enabled to render it appropriately
|
||||||
|
* If latest event not found, we pass rootThreadEventId
|
||||||
|
*/
|
||||||
|
fun TextContent.toThreadTextContent(
|
||||||
|
rootThreadEventId: String,
|
||||||
|
latestThreadEventId: String,
|
||||||
|
msgType: String = MessageType.MSGTYPE_TEXT): MessageTextContent {
|
||||||
|
return MessageTextContent(
|
||||||
|
msgType = msgType,
|
||||||
|
format = MessageFormat.FORMAT_MATRIX_HTML.takeIf { formattedText != null },
|
||||||
|
body = text,
|
||||||
|
relatesTo = RelationDefaultContent(
|
||||||
|
type = RelationType.IO_THREAD,
|
||||||
|
eventId = rootThreadEventId,
|
||||||
|
inReplyTo = ReplyToContent(
|
||||||
|
eventId = latestThreadEventId
|
||||||
|
)),
|
||||||
|
formattedBody = formattedText
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun TextContent.removeInReplyFallbacks(): TextContent {
|
fun TextContent.removeInReplyFallbacks(): TextContent {
|
||||||
return copy(
|
return copy(
|
||||||
text = extractUsefulTextFromReply(this.text),
|
text = extractUsefulTextFromReply(this.text),
|
||||||
|
@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.room.threads
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import com.zhuinden.monarchy.Monarchy
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import io.realm.Realm
|
||||||
|
import org.matrix.android.sdk.api.session.room.threads.ThreadsService
|
||||||
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
|
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
|
||||||
|
import org.matrix.android.sdk.internal.database.helper.findAllLocalThreadNotificationsForRoomId
|
||||||
|
import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId
|
||||||
|
import org.matrix.android.sdk.internal.database.helper.isUserParticipatingInThread
|
||||||
|
import org.matrix.android.sdk.internal.database.helper.mapEventsWithEdition
|
||||||
|
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
|
||||||
|
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
|
||||||
|
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.util.awaitTransaction
|
||||||
|
|
||||||
|
internal class DefaultThreadsService @AssistedInject constructor(
|
||||||
|
@Assisted private val roomId: String,
|
||||||
|
@UserId private val userId: String,
|
||||||
|
@SessionDatabase private val monarchy: Monarchy,
|
||||||
|
private val timelineEventMapper: TimelineEventMapper,
|
||||||
|
) : ThreadsService {
|
||||||
|
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory {
|
||||||
|
fun create(roomId: String): DefaultThreadsService
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMarkedThreadNotificationsLive(): LiveData<List<TimelineEvent>> {
|
||||||
|
return monarchy.findAllMappedWithChanges(
|
||||||
|
{ TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) },
|
||||||
|
{ timelineEventMapper.map(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMarkedThreadNotifications(): List<TimelineEvent> {
|
||||||
|
return monarchy.fetchAllMappedSync(
|
||||||
|
{ TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) },
|
||||||
|
{ timelineEventMapper.map(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAllThreadsLive(): LiveData<List<TimelineEvent>> {
|
||||||
|
return monarchy.findAllMappedWithChanges(
|
||||||
|
{ TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
|
||||||
|
{ timelineEventMapper.map(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAllThreads(): List<TimelineEvent> {
|
||||||
|
return monarchy.fetchAllMappedSync(
|
||||||
|
{ TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
|
||||||
|
{ timelineEventMapper.map(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isUserParticipatingInThread(rootThreadEventId: String): Boolean {
|
||||||
|
return Realm.getInstance(monarchy.realmConfiguration).use {
|
||||||
|
TimelineEventEntity.isUserParticipatingInThread(
|
||||||
|
realm = it,
|
||||||
|
roomId = roomId,
|
||||||
|
rootThreadEventId = rootThreadEventId,
|
||||||
|
senderId = userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mapEventsWithEdition(threads: List<TimelineEvent>): List<TimelineEvent> {
|
||||||
|
return Realm.getInstance(monarchy.realmConfiguration).use {
|
||||||
|
threads.mapEventsWithEdition(it, roomId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun markThreadAsRead(rootThreadEventId: String) {
|
||||||
|
monarchy.awaitTransaction {
|
||||||
|
EventEntity.where(
|
||||||
|
realm = it,
|
||||||
|
eventId = rootThreadEventId).findFirst()?.threadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user