mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-24 10:25:51 +03:00
Merge branch 'develop' into feature/aris/threads
# Conflicts: # matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt # matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt # matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt # matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt # matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt # matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt # tools/check/forbidden_strings_in_code.txt # vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt # vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt # vector/src/main/res/menu/menu_timeline.xml
This commit is contained in:
commit
ac5caccdf9
405 changed files with 11744 additions and 6137 deletions
6
.github/workflows/triage-incoming.yml
vendored
6
.github/workflows/triage-incoming.yml
vendored
|
@ -2,13 +2,13 @@ name: Move new issues onto Issue triage board
|
|||
|
||||
on:
|
||||
issues:
|
||||
types: [ opened ]
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
automate-project-columns:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'vector-im/element-android' # Skip in forks
|
||||
# Skip in forks
|
||||
if: github.repository == 'vector-im/element-android'
|
||||
steps:
|
||||
- uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488
|
||||
with:
|
||||
|
|
31
.github/workflows/triage-move-labelled.yml
vendored
31
.github/workflows/triage-move-labelled.yml
vendored
|
@ -2,14 +2,14 @@ name: Move labelled issues to correct boards and columns
|
|||
|
||||
on:
|
||||
issues:
|
||||
types: [ labeled ]
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
move_needs_info_issues:
|
||||
name: X-Needs-Info issues to Need info column on triage board
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'vector-im/element-android' # Skip in forks
|
||||
# Skip in forks
|
||||
if: github.repository == 'vector-im/element-android'
|
||||
steps:
|
||||
- uses: konradpabjan/move-labeled-or-milestoned-issue@219d384e03fa4b6460cd24f9f37d19eb033a4338
|
||||
with:
|
||||
|
@ -21,8 +21,9 @@ jobs:
|
|||
add_priority_design_issues_to_project:
|
||||
name: P1 X-Needs-Design to Design project board
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'vector-im/element-android' && # Skip in forks
|
||||
# Skip in forks
|
||||
if: >
|
||||
github.repository == 'vector-im/element-android' &&
|
||||
contains(github.event.issue.labels.*.name, 'X-Needs-Design') &&
|
||||
(contains(github.event.issue.labels.*.name, 'S-Critical') &&
|
||||
(contains(github.event.issue.labels.*.name, 'O-Frequent') ||
|
||||
|
@ -53,8 +54,9 @@ jobs:
|
|||
# delight_issues_to_board:
|
||||
# name: Spaces issues to new Delight project board
|
||||
# runs-on: ubuntu-latest
|
||||
# if: |
|
||||
# github.repository == 'vector-im/element-android' && # Skip in forks
|
||||
# # Skip in forks
|
||||
# if: >
|
||||
# github.repository == 'vector-im/element-android' &&
|
||||
# contains(github.event.issue.labels.*.name, 'A-Spaces') ||
|
||||
# contains(github.event.issue.labels.*.name, 'A-Space-Settings') ||
|
||||
# contains(github.event.issue.labels.*.name, 'A-Subspaces')
|
||||
|
@ -79,8 +81,9 @@ jobs:
|
|||
move_voice-message_issues:
|
||||
name: A-Voice Messages to voice message board
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'vector-im/element-android' && # Skip in forks
|
||||
# Skip in forks
|
||||
if: >
|
||||
github.repository == 'vector-im/element-android' &&
|
||||
contains(github.event.issue.labels.*.name, 'A-Voice Messages')
|
||||
steps:
|
||||
- uses: octokit/graphql-action@v2.x
|
||||
|
@ -103,8 +106,9 @@ jobs:
|
|||
move_threads_issues:
|
||||
name: A-Threads to Thread board
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'vector-im/element-android' && # Skip in forks
|
||||
# Skip in forks
|
||||
if: >
|
||||
github.repository == 'vector-im/element-android' &&
|
||||
contains(github.event.issue.labels.*.name, 'A-Threads')
|
||||
steps:
|
||||
- uses: octokit/graphql-action@v2.x
|
||||
|
@ -127,8 +131,9 @@ jobs:
|
|||
move_message_bubbles_issues:
|
||||
name: A-Message-Bubbles to Message bubbles board
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'vector-im/element-android' && # Skip in forks
|
||||
# Skip in forks
|
||||
if: >
|
||||
github.repository == 'vector-im/element-android' &&
|
||||
contains(github.event.issue.labels.*.name, 'A-Message-Bubbles')
|
||||
steps:
|
||||
- uses: octokit/graphql-action@v2.x
|
||||
|
|
7
.github/workflows/triage-move-unlabelled.yml
vendored
7
.github/workflows/triage-move-unlabelled.yml
vendored
|
@ -2,14 +2,15 @@ name: Move unlabelled from needs info columns to triaged
|
|||
|
||||
on:
|
||||
issues:
|
||||
types: [ unlabeled ]
|
||||
types: [unlabeled]
|
||||
|
||||
jobs:
|
||||
Move_Unabeled_Issue_On_Project_Board:
|
||||
name: Move no longer X-Needs-Info issues to Triaged
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'vector-im/element-android' && # Skip in forks
|
||||
# Skip in forks
|
||||
if: >
|
||||
github.repository == 'vector-im/element-android' &&
|
||||
!contains(github.event.issue.labels.*.name, 'X-Needs-Info')
|
||||
env:
|
||||
BOARD_NAME: "Issue triage"
|
||||
|
|
12
.github/workflows/triage-priority-bugs.yml
vendored
12
.github/workflows/triage-priority-bugs.yml
vendored
|
@ -2,13 +2,14 @@ name: Move P1 bugs to boards
|
|||
|
||||
on:
|
||||
issues:
|
||||
types: [ labeled, unlabeled ]
|
||||
types: [labeled, unlabeled]
|
||||
|
||||
jobs:
|
||||
p1_issues_to_team_workboard:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'vector-im/element-android' && # Skip in forks
|
||||
# Skip in forks
|
||||
if: >
|
||||
github.repository == 'vector-im/element-android' &&
|
||||
(!contains(github.event.issue.labels.*.name, 'A-E2EE') &&
|
||||
!contains(github.event.issue.labels.*.name, 'A-E2EE-Cross-Signing') &&
|
||||
!contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') &&
|
||||
|
@ -34,8 +35,9 @@ jobs:
|
|||
|
||||
P1_issues_to_crypto_team_workboard:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.repository == 'vector-im/element-android' && # Skip in forks
|
||||
# Skip in forks
|
||||
if: >
|
||||
github.repository == 'vector-im/element-android' &&
|
||||
(contains(github.event.issue.labels.*.name, 'A-E2EE') ||
|
||||
contains(github.event.issue.labels.*.name, 'A-E2EE-Cross-Signing') ||
|
||||
contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') ||
|
||||
|
|
|
@ -46,3 +46,9 @@ If you would like to receive releases more quickly (bearing in mind that they ma
|
|||
Please refer to [CONTRIBUTING.md](https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md) if you want to contribute on Matrix Android projects!
|
||||
|
||||
Come chat with the community in the dedicated Matrix [room](https://matrix.to/#/#element-android:matrix.org).
|
||||
|
||||
## Triaging issues
|
||||
|
||||
Issues are triaged by community members and the Android App Team, following the [triage process](https://github.com/vector-im/element-meta/wiki/Triage-process).
|
||||
|
||||
We use [issue labels](https://github.com/vector-im/element-meta/wiki/Issue-labelling) to sort all incoming issues.
|
1
changelog.d/2133.feature
Normal file
1
changelog.d/2133.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Add labs support for rendering LaTeX maths (MSC2191)
|
1
changelog.d/2614.feature
Normal file
1
changelog.d/2614.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Allow changing nick colors from the member detail screen
|
|
@ -1 +0,0 @@
|
|||
Attachment picker UI improvements
|
1
changelog.d/3444.feature
Normal file
1
changelog.d/3444.feature
Normal file
|
@ -0,0 +1 @@
|
|||
New attachment picker UI
|
1
changelog.d/4382.feature
Normal file
1
changelog.d/4382.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Updates onboarding splash screen to have a dedicated sign in button and removes the dual purpose sign in/up stage
|
1
changelog.d/4405.feature
Normal file
1
changelog.d/4405.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Change internal timeline management.
|
1
changelog.d/4405.removal
Normal file
1
changelog.d/4405.removal
Normal file
|
@ -0,0 +1 @@
|
|||
Introduce method onStateUpdated on Timeline.Callback
|
1
changelog.d/4644.misc
Normal file
1
changelog.d/4644.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Toolbar is added to a views with QR code scan
|
1
changelog.d/4719.feature
Normal file
1
changelog.d/4719.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Analytics: Track Errors
|
1
changelog.d/4745.misc
Normal file
1
changelog.d/4745.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Open share UI provides by the system when sharing media or text.
|
1
changelog.d/4749.bugfix
Normal file
1
changelog.d/4749.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Fix for broken unread message indicator on the room list when there are no messages in the room.
|
1
changelog.d/4753.removal
Normal file
1
changelog.d/4753.removal
Normal file
|
@ -0,0 +1 @@
|
|||
Support tagged events in Room Account Data (MSC2437)
|
1
changelog.d/4781.bugfix
Normal file
1
changelog.d/4781.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Tentative fix for the speaker being used instead of earpiece for the outgoing call ringtone on lineage os
|
1
changelog.d/4789.bugfix
Normal file
1
changelog.d/4789.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Fixing crashes when quickly scrolling or restoring the room timeline
|
1
changelog.d/4837.bugfix
Normal file
1
changelog.d/4837.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Stop using CharSequence as EpoxyAttribute because it can lead to crash if the CharSequence mutates during rendering.
|
1
changelog.d/4847.bugfix
Normal file
1
changelog.d/4847.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Translate the error observed when the user is not allowed to join a room
|
1
changelog.d/4872.misc
Normal file
1
changelog.d/4872.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Enabling new FTUE Auth onboarding base, includes the "I already have an account" button in the splash
|
|
@ -95,6 +95,8 @@ ext.libs = [
|
|||
],
|
||||
markwon : [
|
||||
'core' : "io.noties.markwon:core:$markwon",
|
||||
'extLatex' : "io.noties.markwon:ext-latex:$markwon",
|
||||
'inlineParser' : "io.noties.markwon:inline-parser:$markwon",
|
||||
'html' : "io.noties.markwon:html:$markwon"
|
||||
],
|
||||
airbnb : [
|
||||
|
|
|
@ -179,6 +179,7 @@ ext.groups = [
|
|||
'org.sonatype.oss',
|
||||
'org.testng',
|
||||
'org.threeten',
|
||||
'ru.noties',
|
||||
'xerces',
|
||||
'xml-apis',
|
||||
]
|
||||
|
|
2
fastlane/metadata/android/cs-CZ/changelogs/40103090.txt
Normal file
2
fastlane/metadata/android/cs-CZ/changelogs/40103090.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Hlavní změny v této verzi: Přidání podpory pro návrh hlasové zprávy. Opravy mnoha chyb!
|
||||
Úplný seznam změn: https://github.com/vector-im/element-android/releases/tag/v1.3.9
|
2
fastlane/metadata/android/de-DE/changelogs/40103050.txt
Normal file
2
fastlane/metadata/android/de-DE/changelogs/40103050.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Änderungen in dieser Version: Unterstützung für Anwesenheitsstatus in Direktnachrichten (Momentan auf matrix.org deaktiviert), Android Auto funktioniert wieder.
|
||||
Änderungsliste: https://github.com/vector-im/element-android/releases/tag/v1.3.5
|
2
fastlane/metadata/android/de-DE/changelogs/40103060.txt
Normal file
2
fastlane/metadata/android/de-DE/changelogs/40103060.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Änderungen in dieser Version: Unterstützung für Anwesenheitsstatus in Direktnachrichten (Momentan auf matrix.org deaktiviert), Android Auto funktioniert wieder.
|
||||
Änderungsliste: https://github.com/vector-im/element-android/releases/tag/v1.3.6
|
2
fastlane/metadata/android/de-DE/changelogs/40103090.txt
Normal file
2
fastlane/metadata/android/de-DE/changelogs/40103090.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Hauptänderungen: Verbesserungen bei Sprachnachrichten, Bugfixes.
|
||||
Änderungsliste: https://github.com/vector-im/element-android/releases/tag/v1.3.9
|
2
fastlane/metadata/android/et/changelogs/40103090.txt
Normal file
2
fastlane/metadata/android/et/changelogs/40103090.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Põhilised muutused selles versioonis: Häälsõnumite võimalus. Palju veaparandusi!
|
||||
Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases/tag/v1.3.9
|
2
fastlane/metadata/android/fa/changelogs/40103090.txt
Normal file
2
fastlane/metadata/android/fa/changelogs/40103090.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
تغییرات عمده در این نگارش: افزودن پشتیبان از چرکنویسهای صوتی. رفع چندین مشکل!
|
||||
گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.3.9
|
2
fastlane/metadata/android/fr-FR/changelogs/40103090.txt
Normal file
2
fastlane/metadata/android/fr-FR/changelogs/40103090.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Principaux changements pour cette version : Ajout du support pour les brouillons de messages vocaux. Beaucoup de corrections de bugs !
|
||||
Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.3.9
|
2
fastlane/metadata/android/hu-HU/changelogs/40103090.txt
Normal file
2
fastlane/metadata/android/hu-HU/changelogs/40103090.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Fő változás ebben a verzióban: Hang üzenet piszkozat támogatás. Sok egyéb hibajavítás.
|
||||
Teljes változásnapló: https://github.com/vector-im/element-android/releases/tag/v1.3.9
|
2
fastlane/metadata/android/id/changelogs/40103090.txt
Normal file
2
fastlane/metadata/android/id/changelogs/40103090.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Perubahan utama di versi ini: Tambahkan dukungan untuk draf pesan suara. Banyak perbaikan bug!
|
||||
Changelog lengkap: https://github.com/vector-im/element-android/releases/tag/v1.3.9
|
|
@ -1 +1 @@
|
|||
Perpesanan grup - perpesanan, panggilan suara dan video grup terenkripsi
|
||||
Perpesanan grup — perpesanan, panggilan suara dan video grup terenkripsi
|
||||
|
|
|
@ -1 +1 @@
|
|||
Element - Perpesanan Aman
|
||||
Element — Perpesanan Aman
|
||||
|
|
2
fastlane/metadata/android/it-IT/changelogs/40103090.txt
Normal file
2
fastlane/metadata/android/it-IT/changelogs/40103090.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Modifiche principali in questa versione: aggiunto supporto per le bozze dei vocali. Molte correzioni!
|
||||
Cronologia completa: https://github.com/vector-im/element-android/releases/tag/v1.3.9
|
2
fastlane/metadata/android/pt-BR/changelogs/40103090.txt
Normal file
2
fastlane/metadata/android/pt-BR/changelogs/40103090.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Principais mudanças nesta versão: Adicionar suporte para rascunho de mensagem de voz. Muitos consertos de bugs!
|
||||
Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.3.9
|
2
fastlane/metadata/android/sk/changelogs/40103060.txt
Normal file
2
fastlane/metadata/android/sk/changelogs/40103060.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Hlavné zmeny v tejto verzii: Pridanie podpory prítomnosti pre miestnosť s priamymi správami (poznámka: prítomnosť je na matrix.org vypnutá). Opätovné pridanie podpory Android Auto.
|
||||
Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.3.6
|
2
fastlane/metadata/android/sk/changelogs/40103090.txt
Normal file
2
fastlane/metadata/android/sk/changelogs/40103090.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Hlavné zmeny v tejto verzii: Pridanie podpory pre návrh hlasovej správy. Oprava mnohých chýb!
|
||||
Úplný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.3.9
|
|
@ -1,30 +1,41 @@
|
|||
Element je inovatívny kolaboračný komunikátor a messenger ktorý:
|
||||
Element je zabezpečený messenger a zároveň aplikácia na tímovú spoluprácu, ktorá je ideálna na skupinové konverzácie pri práci na diaľku. Táto komunikačná aplikácia využíva end-to-end šifrovanie na poskytovanie výkonných videokonferencií, zdieľania súborov a hlasových hovorov.
|
||||
|
||||
1. Ponecháva kontrolu nad vaším súkromím
|
||||
2. Umožňuje komunikovať s kýmkoľvek v sieti Matrix a vďaka integráciám aj s rôznymi inými aplikáciami ako napríklad Slack
|
||||
3. Chráni vás pred reklamami, zhromažďovaním údajov a uzavretými platformami
|
||||
4. Posilňuje vašu bezpečnosť vďaka E2E šifrovaniu a krížovému podpisovaniu určenému na overovanie ostatných
|
||||
<b>Funkcie aplikácie Element zahŕňajú:</b>
|
||||
- Pokročilé nástroje na online komunikáciu
|
||||
- Plne šifrované správy umožňujúce bezpečnejšiu firemnú komunikáciu aj pre pracovníkov na diaľku
|
||||
- Decentralizované konverzácie založené na open source frameworku Matrix
|
||||
- Bezpečné zdieľanie súborov so šifrovanými údajmi pri správe projektov
|
||||
- Videochaty s funkciou Voice over IP a zdieľaním obrazovky
|
||||
- Jednoduchá integrácia s obľúbenými nástrojmi na online spoluprácu, nástrojmi na riadenie projektov, službami VoIP a inými aplikáciami na tímovú komunikáciu
|
||||
|
||||
Element sa od ostatných komunikačných a kolaboračných aplikácií odlišuje tým, že je decentralizovaný a open-source.
|
||||
Element sa úplne líši od ostatných aplikácií na zasielanie správ a spoluprácu. Funguje na Matrixe, otvorenej sieti na bezpečné posielanie správ a decentralizovanú komunikáciu. Umožňuje vlastný hosting, aby používatelia získali maximálne vlastníctvo a kontrolu nad svojimi údajmi a správami.
|
||||
|
||||
S Elementom sa môžete pripojiť k vlastnému serveru alebo si môžete vybrať server s dôveryhodným poskytovateľom, čím si zachováte súkromie, vlastníctvo a kontrolu nad vašimi konverzáciami a údajmi. Získate tak prístup do otvorenej siete a teda nie ste limitovaní na komunikáciu len s ostatnými Element používateľmi. A samozrejme je vaša komunikácia dobre zabezpečná.
|
||||
<b>Súkromie a šifrovanie správ</b>
|
||||
Element vás chráni pred nežiaducimi reklamami, ťažbou údajov a tzv. walled gardens. Zabezpečuje tiež všetky vaše údaje, video a hlasovú komunikáciu jeden na jedného prostredníctvom end-to-end šifrovania a overovania zariadení krížovým podpisovaním
|
||||
Element vám poskytuje kontrolu nad vaším súkromím a zároveň vám umožňuje bezpečne komunikovať s kýmkoľvek v sieti Matrix alebo s inými nástrojmi na podnikovú spoluprácu vďaka integrácii s aplikáciami, ako je napríklad Slack.
|
||||
|
||||
Element všetko toto dokáže vďaka tomu, že pracuje podľa protokolu Matrix - štandardu na otvorenú, decentralizovanú komunikáciu.
|
||||
<b>Element môže byť na vašom vlastnom serveri</b>.
|
||||
Aby ste mali väčšiu kontrolu nad svojimi citlivými údajmi a konverzáciami, Element môže byť na vašom vlastnom serveri alebo si môžete vybrať ľubovoľný hosting založený na systéme Matrix - štandarde pre decentralizovanú komunikáciu s otvoreným zdrojovým kódom. Element vám poskytuje súkromie, súlad s bezpečnostnými predpismi a flexibilitu integrácie.
|
||||
|
||||
Element vám dáva kontrolu tým, že si samy vyberiete, ako budete spravovať (ang. host) vaše konverzácie. Priamo v aplikácii Element si môžete vybrať z rôznych spôsobov hostovania:
|
||||
<b>Vlastnite svoje údaje</b>
|
||||
Vy rozhodujete o tom, kde budú vaše údaje a správy uložené. Bez rizika ťažby údajov alebo prístupu tretích strán.
|
||||
|
||||
1. Získajte účet zdarma na verejnom servery matrix.org od vývojárov protokolu Matrix alebo si vyberte z tísíce iných serverov hostovaných dobrovoľníkmi
|
||||
2. Hostujte si účet spustením vlastného servera použitím vlastného hardvéru
|
||||
3. Prihláste sa k účtu na vlastnom servery objednaním služieb na platforme Element Matrix Services
|
||||
Element vám dáva kontrolu rôznymi spôsobmi:
|
||||
1. Získajte bezplatné konto na verejnom serveri matrix.org, ktorý hostia vývojári Matrixu, alebo si vyberte z tisícov verejných serverov, ktoré hostia dobrovoľníci.
|
||||
2. Vlastný hosting účtu spustením servera na vlastnej IT infraštruktúre.
|
||||
3. Zaregistrujte si účet na vlastnom serveri tak, že si jednoducho predplatíte hostingovú platformu Element Matrix Services.
|
||||
|
||||
<b>Prečo si vybrať Element?</b>
|
||||
<b>Otvorené zasielanie správ a spolupráca</b>
|
||||
Môžete komunikovať s kýmkoľvek v sieti Matrix, či už používa aplikáciu Element, inú aplikáciu Matrix alebo dokonca ak používa inú aplikáciu na zasielanie správ.
|
||||
|
||||
<b>PONECHAJTE SI VAŠE ÚDAJE</b>: Len vy rozhodujete o tom, kde si budete uchovávať vaše správy a ostatné údaje. Len vy vlastníte vaše údaje a riadite zaobchádzanie s nimi, nie nejaká megakorporácia, ktorá z nich ťaží alebo ich poskytuje tretím stranám.
|
||||
<b>Vynikajúce zabezpečenie</b>
|
||||
Skutočné end-to-end šifrovanie (správy môžu dešifrovať len účastníci konverzácie) a krížové overovanie zariadení.
|
||||
|
||||
<b>OTVORENÁ KOMUNIKÁCIA a KOLABORÁCIA</b>: Konverzovať môžete s kýmkoľvek v otvorenej sieti Matrix nezávisle na tom, či používa Element, inú kompatibilnú aplikáciu, ba dokkonca aj s tými, ktorí používajú úplne inú platformu určenú na okamžitú komunikáciu ako sú Slack, IRC alebo XMPP.
|
||||
<b>Kompletná komunikácia a integrácia</b>
|
||||
Správy, hlasové a video hovory, zdieľanie súborov, zdieľanie obrazovky a celý rad integrácií, botov a widgetov. Vytvárajte miestnosti, komunity, zostaňte v kontakte a vybavujte veci.
|
||||
|
||||
<b>VEĽMI VYSOKÉ ZABEZPEČENIE</b>: Skutočné šifrovanie od zariadenia k zariadeniu (len diskutujúci môžu dešifrovať správy) a krížové podpisovanie určené na overovanie jednotlivých zariadení členov konverzácií.
|
||||
<b>Nadviažte tam, kde ste skončili</b>
|
||||
Buďte v kontakte, nech ste kdekoľvek, vďaka plne synchronizovanej histórii správ vo všetkých zariadeniach a na webe na adrese https://app.element.io.
|
||||
|
||||
<b>KOMPLETNÁ KOMUNIKÁCIA</b>: Okamžité správy, telefonáty a video hovory, zdieľanie súborov, zdieľanie obrazovky a veľké množstvo integrácií, botov a widgetov. Vytvorte si vlastné miestnosti, založte komunity, ostante v kontakte a vyriešte problémy.
|
||||
|
||||
<b>KDEKOĽVEK SA NACHÁDZATE</b>: Ostante v kontakte kdekoľvek ste s plne synchronizovanou históriou konverzácií naprieč všetkými vašimi zariadeniami a aj cez web na adrese https://app.element.io.
|
||||
<b>Otvorený zdroj</b>
|
||||
Element Android je projekt s otvoreným zdrojovým kódom, ktorého hostiteľom je GitHub. Nahlasujte chyby a/alebo prispievajte k jeho vývoju na adrese https://github.com/vector-im/element-android.
|
||||
|
|
|
@ -1 +1 @@
|
|||
Element (kedysi Riot.im)
|
||||
Element - Bezpečný messenger
|
||||
|
|
2
fastlane/metadata/android/sq/changelogs/40103090.txt
Normal file
2
fastlane/metadata/android/sq/changelogs/40103090.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Ndryshimet kryesore në këtë version: Shtim mbulimi për skica mesazhesh zanore. Mjaft ndreqje të metash!
|
||||
Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.3.9
|
2
fastlane/metadata/android/sv-SE/changelogs/40103090.txt
Normal file
2
fastlane/metadata/android/sv-SE/changelogs/40103090.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Huvudsakliga ändringar i den här versionen: Lägg till stöd för röstmeddelandeutkast. Många buggfixar!
|
||||
Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.3.9
|
2
fastlane/metadata/android/uk/changelogs/40103090.txt
Normal file
2
fastlane/metadata/android/uk/changelogs/40103090.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Основні зміни в цій версії: підтримка чернеток голосових повідомлень. Багато виправлень помилок!
|
||||
Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.3.9
|
2
fastlane/metadata/android/zh-CN/changelogs/40103090.txt
Normal file
2
fastlane/metadata/android/zh-CN/changelogs/40103090.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
版本的主要变化:增加了对语音信息草稿的支持。许多修正!
|
||||
完整更新日志:https://github.com/vector-im/element-android/releases/tag/v1.3.9
|
2
fastlane/metadata/android/zh-TW/changelogs/40103090.txt
Normal file
2
fastlane/metadata/android/zh-TW/changelogs/40103090.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
此版本中的主要變動:新增對語音訊息草稿的支援。許多臭蟲修復!
|
||||
完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.9
|
|
@ -1,11 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<declare-styleable name="PollResultLineView">
|
||||
<attr name="optionName" format="string" localization="suggested" />
|
||||
<attr name="optionCount" format="string" />
|
||||
<attr name="optionSelected" format="boolean" />
|
||||
<attr name="optionIsWinner" format="boolean" />
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
|
@ -145,36 +145,9 @@ class CommonTestHelper(context: Context) {
|
|||
* @param nbOfMessages the number of time the message will be sent
|
||||
*/
|
||||
fun sendTextMessage(room: Room, message: String, nbOfMessages: Int, timeout: Long = TestConstants.timeOutMillis): List<TimelineEvent> {
|
||||
val sentEvents = ArrayList<TimelineEvent>(nbOfMessages)
|
||||
val timeline = room.createTimeline(null, TimelineSettings(10))
|
||||
timeline.start()
|
||||
waitWithLatch(timeout + 1_000L * nbOfMessages) { latch ->
|
||||
val timelineListener = object : Timeline.Listener {
|
||||
override fun onTimelineFailure(throwable: Throwable) {
|
||||
}
|
||||
|
||||
override fun onNewTimelineEvents(eventIds: List<String>) {
|
||||
// noop
|
||||
}
|
||||
|
||||
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
||||
val newMessages = snapshot
|
||||
.filter { it.root.sendState == SendState.SYNCED }
|
||||
.filter { it.root.getClearType() == EventType.MESSAGE }
|
||||
.filter { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(message) == true }
|
||||
|
||||
Timber.v("New synced message size: ${newMessages.size}")
|
||||
if (newMessages.size == nbOfMessages) {
|
||||
sentEvents.addAll(newMessages)
|
||||
// Remove listener now, if not at the next update sendEvents could change
|
||||
timeline.removeListener(this)
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
timeline.addListener(timelineListener)
|
||||
sendTextMessagesBatched(room, message, nbOfMessages)
|
||||
}
|
||||
val sentEvents = sendTextMessagesBatched(timeline, room, message, nbOfMessages, timeout)
|
||||
timeline.dispose()
|
||||
// Check that all events has been created
|
||||
assertEquals("Message number do not match $sentEvents", nbOfMessages.toLong(), sentEvents.size.toLong())
|
||||
|
@ -182,9 +155,10 @@ class CommonTestHelper(context: Context) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Will send nb of messages provided by count parameter but waits a bit every 10 messages to avoid gap in sync
|
||||
* Will send nb of messages provided by count parameter but waits every 10 messages to avoid gap in sync
|
||||
*/
|
||||
private fun sendTextMessagesBatched(room: Room, message: String, count: Int, rootThreadEventId: String? = null) {
|
||||
private fun sendTextMessagesBatched(timeline: Timeline, room: Room, message: String, count: Int, timeout: Long,rootThreadEventId: String? = null): List<TimelineEvent> {
|
||||
val sentEvents = ArrayList<TimelineEvent>(count)
|
||||
(1 until count + 1)
|
||||
.map { "$message #$it" }
|
||||
.chunked(10)
|
||||
|
@ -198,8 +172,34 @@ class CommonTestHelper(context: Context) {
|
|||
room.sendTextMessage(formattedMessage)
|
||||
}
|
||||
}
|
||||
Thread.sleep(1_000L)
|
||||
waitWithLatch(timeout) { latch ->
|
||||
val timelineListener = object : Timeline.Listener {
|
||||
|
||||
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
||||
val allSentMessages = snapshot
|
||||
.filter { it.root.sendState == SendState.SYNCED }
|
||||
.filter { it.root.getClearType() == EventType.MESSAGE }
|
||||
.filter { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(message) == true }
|
||||
|
||||
val hasSyncedAllBatchedMessages = allSentMessages
|
||||
.map {
|
||||
it.root.getClearContent().toModel<MessageContent>()?.body
|
||||
}
|
||||
.containsAll(batchedMessages)
|
||||
|
||||
if (allSentMessages.size == count) {
|
||||
sentEvents.addAll(allSentMessages)
|
||||
}
|
||||
if (hasSyncedAllBatchedMessages) {
|
||||
timeline.removeListener(this)
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
timeline.addListener(timelineListener)
|
||||
}
|
||||
}
|
||||
return sentEvents
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -387,13 +387,6 @@ class CommonTestHelper(context: Context) {
|
|||
|
||||
fun createEventListener(latch: CountDownLatch, predicate: (List<TimelineEvent>) -> Boolean): Timeline.Listener {
|
||||
return object : Timeline.Listener {
|
||||
override fun onTimelineFailure(throwable: Throwable) {
|
||||
// noop
|
||||
}
|
||||
|
||||
override fun onNewTimelineEvents(eventIds: List<String>) {
|
||||
// noop
|
||||
}
|
||||
|
||||
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
||||
if (predicate(snapshot)) {
|
||||
|
|
|
@ -246,8 +246,7 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
|
|||
val bobRoomSummariesLive = bob.getRoomSummariesLive(roomSummaryQueryParams { })
|
||||
val newRoomObserver = object : Observer<List<RoomSummary>> {
|
||||
override fun onChanged(t: List<RoomSummary>?) {
|
||||
val indexOfFirst = t?.indexOfFirst { it.roomId == roomId } ?: -1
|
||||
if (indexOfFirst != -1) {
|
||||
if (t?.any { it.roomId == roomId }.orFalse()) {
|
||||
bobRoomSummariesLive.removeObserver(this)
|
||||
latch.countDown()
|
||||
}
|
||||
|
|
|
@ -1,183 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.session.room.timeline
|
||||
|
||||
import org.amshove.kluent.shouldBeFalse
|
||||
import org.amshove.kluent.shouldBeTrue
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
import org.junit.runners.MethodSorters
|
||||
import org.matrix.android.sdk.InstrumentedTest
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||
import org.matrix.android.sdk.common.checkSendOrder
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
@RunWith(JUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
class TimelineBackToPreviousLastForwardTest : InstrumentedTest {
|
||||
|
||||
private val commonTestHelper = CommonTestHelper(context())
|
||||
private val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||
|
||||
/**
|
||||
* This test ensure that if we have a chunk in the timeline which is due to a sync, and we click to permalink of an
|
||||
* even contained in a previous lastForward chunk, we will be able to go back to the live
|
||||
*/
|
||||
@Test
|
||||
fun backToPreviousLastForwardTest() {
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val bobSession = cryptoTestData.secondSession!!
|
||||
val aliceRoomId = cryptoTestData.roomId
|
||||
|
||||
aliceSession.cryptoService().setWarnOnUnknownDevices(false)
|
||||
bobSession.cryptoService().setWarnOnUnknownDevices(false)
|
||||
|
||||
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
|
||||
val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
|
||||
|
||||
val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(30))
|
||||
bobTimeline.start()
|
||||
|
||||
var roomCreationEventId: String? = null
|
||||
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root}")
|
||||
}
|
||||
|
||||
roomCreationEventId = snapshot.lastOrNull()?.root?.eventId
|
||||
// Ok, we have the 8 first messages of the initial sync (room creation and bob join event)
|
||||
snapshot.size == 8
|
||||
}
|
||||
|
||||
bobTimeline.addListener(eventsListener)
|
||||
commonTestHelper.await(lock)
|
||||
bobTimeline.removeAllListeners()
|
||||
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
|
||||
}
|
||||
|
||||
// Bob stop to sync
|
||||
bobSession.stopSync()
|
||||
|
||||
val messageRoot = "First messages from Alice"
|
||||
|
||||
// Alice sends 30 messages
|
||||
commonTestHelper.sendTextMessage(
|
||||
roomFromAlicePOV,
|
||||
messageRoot,
|
||||
30)
|
||||
|
||||
// Bob start to sync
|
||||
bobSession.startSync(true)
|
||||
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root}")
|
||||
}
|
||||
|
||||
// Ok, we have the 10 last messages from Alice.
|
||||
snapshot.size == 10 &&
|
||||
snapshot.all { it.root.content.toModel<MessageContent>()?.body?.startsWith(messageRoot).orFalse() }
|
||||
}
|
||||
|
||||
bobTimeline.addListener(eventsListener)
|
||||
commonTestHelper.await(lock)
|
||||
bobTimeline.removeAllListeners()
|
||||
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue()
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
|
||||
}
|
||||
|
||||
// Bob navigate to the first event (room creation event), so inside the previous last forward chunk
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root}")
|
||||
}
|
||||
|
||||
// The event is in db, so it is fetch and auto pagination occurs, half of the number of events we have for this chunk (?)
|
||||
snapshot.size == 4
|
||||
}
|
||||
|
||||
bobTimeline.addListener(eventsListener)
|
||||
|
||||
// Restart the timeline to the first sent event, which is already in the database, so pagination should start automatically
|
||||
assertTrue(roomFromBobPOV.getTimeLineEvent(roomCreationEventId!!) != null)
|
||||
|
||||
bobTimeline.restartWithEventId(roomCreationEventId)
|
||||
|
||||
commonTestHelper.await(lock)
|
||||
bobTimeline.removeAllListeners()
|
||||
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue()
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
|
||||
}
|
||||
|
||||
// Bob scroll to the future
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root}")
|
||||
}
|
||||
|
||||
// Bob can see the first event of the room (so Back pagination has worked)
|
||||
snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE &&
|
||||
// 8 for room creation item, and 30 for the forward pagination
|
||||
snapshot.size == 38 &&
|
||||
snapshot.checkSendOrder(messageRoot, 30, 0)
|
||||
}
|
||||
|
||||
bobTimeline.addListener(eventsListener)
|
||||
|
||||
bobTimeline.paginate(Timeline.Direction.FORWARDS, 50)
|
||||
|
||||
commonTestHelper.await(lock)
|
||||
bobTimeline.removeAllListeners()
|
||||
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
|
||||
}
|
||||
bobTimeline.dispose()
|
||||
|
||||
cryptoTestData.cleanUp(commonTestHelper)
|
||||
}
|
||||
}
|
|
@ -16,6 +16,8 @@
|
|||
|
||||
package org.matrix.android.sdk.session.room.timeline
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.amshove.kluent.internal.assertEquals
|
||||
import org.amshove.kluent.shouldBeFalse
|
||||
import org.amshove.kluent.shouldBeTrue
|
||||
import org.junit.FixMethodOrder
|
||||
|
@ -123,54 +125,29 @@ class TimelineForwardPaginationTest : InstrumentedTest {
|
|||
// Alice paginates BACKWARD and FORWARD of 50 events each
|
||||
// Then she can only navigate FORWARD
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Alice timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root.content}")
|
||||
}
|
||||
|
||||
// Alice can see the first event of the room (so Back pagination has worked)
|
||||
snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE &&
|
||||
// 6 for room creation item (backward pagination), 1 for the context, and 50 for the forward pagination
|
||||
snapshot.size == 57 // 6 + 1 + 50
|
||||
val snapshot = runBlocking {
|
||||
aliceTimeline.awaitPaginate(Timeline.Direction.BACKWARDS, 50)
|
||||
aliceTimeline.awaitPaginate(Timeline.Direction.FORWARDS, 50)
|
||||
}
|
||||
|
||||
aliceTimeline.addListener(aliceEventsListener)
|
||||
|
||||
// Restart the timeline to the first sent event
|
||||
// We ask to load event backward and forward
|
||||
aliceTimeline.paginate(Timeline.Direction.BACKWARDS, 50)
|
||||
aliceTimeline.paginate(Timeline.Direction.FORWARDS, 50)
|
||||
|
||||
commonTestHelper.await(lock)
|
||||
aliceTimeline.removeAllListeners()
|
||||
|
||||
aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue()
|
||||
aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
|
||||
|
||||
assertEquals(EventType.STATE_ROOM_CREATE, snapshot.lastOrNull()?.root?.getClearType())
|
||||
// 6 for room creation item (backward pagination), 1 for the context, and 50 for the forward pagination
|
||||
// 6 + 1 + 50
|
||||
assertEquals(57, snapshot.size)
|
||||
}
|
||||
|
||||
// Alice paginates once again FORWARD for 50 events
|
||||
// All the timeline is retrieved, she cannot paginate anymore in both direction
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Alice timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root.content}")
|
||||
}
|
||||
// 6 for room creation item (backward pagination),and numberOfMessagesToSend (all the message of the room)
|
||||
snapshot.size == 6 + numberOfMessagesToSend &&
|
||||
snapshot.checkSendOrder(message, numberOfMessagesToSend, 0)
|
||||
}
|
||||
|
||||
aliceTimeline.addListener(aliceEventsListener)
|
||||
|
||||
// Ask for a forward pagination
|
||||
aliceTimeline.paginate(Timeline.Direction.FORWARDS, 50)
|
||||
|
||||
commonTestHelper.await(lock)
|
||||
aliceTimeline.removeAllListeners()
|
||||
val snapshot = runBlocking {
|
||||
aliceTimeline.awaitPaginate(Timeline.Direction.FORWARDS, 50)
|
||||
}
|
||||
// 6 for room creation item (backward pagination),and numberOfMessagesToSend (all the message of the room)
|
||||
snapshot.size == 6 + numberOfMessagesToSend &&
|
||||
snapshot.checkSendOrder(message, numberOfMessagesToSend, 0)
|
||||
|
||||
// The timeline is fully loaded
|
||||
aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
|
||||
|
|
|
@ -168,10 +168,8 @@ class TimelinePreviousLastForwardTest : InstrumentedTest {
|
|||
|
||||
bobTimeline.addListener(eventsListener)
|
||||
|
||||
// Restart the timeline to the first sent event, and paginate in both direction
|
||||
// Restart the timeline to the first sent event
|
||||
bobTimeline.restartWithEventId(firstMessageFromAliceId)
|
||||
bobTimeline.paginate(Timeline.Direction.BACKWARDS, 50)
|
||||
bobTimeline.paginate(Timeline.Direction.FORWARDS, 50)
|
||||
|
||||
commonTestHelper.await(lock)
|
||||
bobTimeline.removeAllListeners()
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.session.room.timeline
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.amshove.kluent.internal.assertEquals
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
import org.junit.runners.MethodSorters
|
||||
import org.matrix.android.sdk.InstrumentedTest
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.events.model.isTextMessage
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||
import org.matrix.android.sdk.common.TestConstants
|
||||
|
||||
@RunWith(JUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
class TimelineSimpleBackPaginationTest : InstrumentedTest {
|
||||
|
||||
private val commonTestHelper = CommonTestHelper(context())
|
||||
private val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||
|
||||
@Test
|
||||
fun timeline_backPaginate_shouldReachEndOfTimeline() {
|
||||
val numberOfMessagesToSent = 200
|
||||
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val bobSession = cryptoTestData.secondSession!!
|
||||
val roomId = cryptoTestData.roomId
|
||||
|
||||
aliceSession.cryptoService().setWarnOnUnknownDevices(false)
|
||||
bobSession.cryptoService().setWarnOnUnknownDevices(false)
|
||||
|
||||
val roomFromAlicePOV = aliceSession.getRoom(roomId)!!
|
||||
val roomFromBobPOV = bobSession.getRoom(roomId)!!
|
||||
|
||||
// Alice sends X messages
|
||||
val message = "Message from Alice"
|
||||
commonTestHelper.sendTextMessage(
|
||||
roomFromAlicePOV,
|
||||
message,
|
||||
numberOfMessagesToSent)
|
||||
|
||||
val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(30))
|
||||
bobTimeline.start()
|
||||
|
||||
commonTestHelper.waitWithLatch(timeout = TestConstants.timeOutMillis * 10) {
|
||||
val listener = object : Timeline.Listener {
|
||||
|
||||
override fun onStateUpdated(direction: Timeline.Direction, state: Timeline.PaginationState) {
|
||||
if (direction == Timeline.Direction.FORWARDS) {
|
||||
return
|
||||
}
|
||||
if (state.hasMoreToLoad && !state.loading) {
|
||||
bobTimeline.paginate(Timeline.Direction.BACKWARDS, 30)
|
||||
} else if (!state.hasMoreToLoad) {
|
||||
bobTimeline.removeListener(this)
|
||||
it.countDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
bobTimeline.addListener(listener)
|
||||
bobTimeline.paginate(Timeline.Direction.BACKWARDS, 30)
|
||||
}
|
||||
assertEquals(false, bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS))
|
||||
assertEquals(false, bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS))
|
||||
|
||||
val onlySentEvents = runBlocking {
|
||||
bobTimeline.getSnapshot()
|
||||
}
|
||||
.filter {
|
||||
it.root.isTextMessage()
|
||||
}.filter {
|
||||
(it.root.content.toModel<MessageTextContent>())?.body?.startsWith(message).orFalse()
|
||||
}
|
||||
assertEquals(numberOfMessagesToSent, onlySentEvents.size)
|
||||
|
||||
bobTimeline.dispose()
|
||||
cryptoTestData.cleanUp(commonTestHelper)
|
||||
}
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.session.room.timeline
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import org.matrix.android.sdk.InstrumentedTest
|
||||
|
||||
internal class TimelineTest : InstrumentedTest {
|
||||
|
||||
companion object {
|
||||
private const val ROOM_ID = "roomId"
|
||||
}
|
||||
|
||||
private lateinit var monarchy: Monarchy
|
||||
|
||||
// @Before
|
||||
// fun setup() {
|
||||
// Timber.plant(Timber.DebugTree())
|
||||
// Realm.init(context())
|
||||
// val testConfiguration = RealmConfiguration.Builder().name("test-realm")
|
||||
// .modules(SessionRealmModule()).build()
|
||||
//
|
||||
// Realm.deleteRealm(testConfiguration)
|
||||
// monarchy = Monarchy.Builder().setRealmConfiguration(testConfiguration).build()
|
||||
// RoomDataHelper.fakeInitialSync(monarchy, ROOM_ID)
|
||||
// }
|
||||
//
|
||||
// private fun createTimeline(initialEventId: String? = null): Timeline {
|
||||
// val taskExecutor = TaskExecutor(testCoroutineDispatchers)
|
||||
// val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy)
|
||||
// val paginationTask = FakePaginationTask @Inject constructor(tokenChunkEventPersistor)
|
||||
// val getContextOfEventTask = FakeGetContextOfEventTask @Inject constructor(tokenChunkEventPersistor)
|
||||
// val roomMemberExtractor = SenderRoomMemberExtractor(ROOM_ID)
|
||||
// val timelineEventFactory = TimelineEventFactory(roomMemberExtractor, EventRelationExtractor())
|
||||
// return DefaultTimeline(
|
||||
// ROOM_ID,
|
||||
// initialEventId,
|
||||
// monarchy.realmConfiguration,
|
||||
// taskExecutor,
|
||||
// getContextOfEventTask,
|
||||
// timelineEventFactory,
|
||||
// paginationTask,
|
||||
// null)
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// fun backPaginate_shouldLoadMoreEvents_whenPaginateIsCalled() {
|
||||
// val timeline = createTimeline()
|
||||
// timeline.start()
|
||||
// val paginationCount = 30
|
||||
// var initialLoad = 0
|
||||
// val latch = CountDownLatch(2)
|
||||
// var timelineEvents: List<TimelineEvent> = emptyList()
|
||||
// timeline.listener = object : Timeline.Listener {
|
||||
// override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
||||
// if (snapshot.isNotEmpty()) {
|
||||
// if (initialLoad == 0) {
|
||||
// initialLoad = snapshot.size
|
||||
// }
|
||||
// timelineEvents = snapshot
|
||||
// latch.countDown()
|
||||
// timeline.paginate(Timeline.Direction.BACKWARDS, paginationCount)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// latch.await()
|
||||
// timelineEvents.size shouldBeEqualTo initialLoad + paginationCount
|
||||
// timeline.dispose()
|
||||
// }
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.commonmark.ext.maths
|
||||
|
||||
import org.commonmark.node.CustomBlock
|
||||
|
||||
class DisplayMaths(private val delimiter: DisplayDelimiter) : CustomBlock() {
|
||||
enum class DisplayDelimiter {
|
||||
DOUBLE_DOLLAR,
|
||||
SQUARE_BRACKET_ESCAPED
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.commonmark.ext.maths
|
||||
|
||||
import org.commonmark.node.CustomNode
|
||||
import org.commonmark.node.Delimited
|
||||
|
||||
class InlineMaths(private val delimiter: InlineDelimiter) : CustomNode(), Delimited {
|
||||
enum class InlineDelimiter {
|
||||
SINGLE_DOLLAR,
|
||||
ROUND_BRACKET_ESCAPED
|
||||
}
|
||||
|
||||
override fun getOpeningDelimiter(): String {
|
||||
return when (delimiter) {
|
||||
InlineDelimiter.SINGLE_DOLLAR -> "$"
|
||||
InlineDelimiter.ROUND_BRACKET_ESCAPED -> "\\("
|
||||
}
|
||||
}
|
||||
|
||||
override fun getClosingDelimiter(): String {
|
||||
return when (delimiter) {
|
||||
InlineDelimiter.SINGLE_DOLLAR -> "$"
|
||||
InlineDelimiter.ROUND_BRACKET_ESCAPED -> "\\)"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.commonmark.ext.maths
|
||||
|
||||
import org.commonmark.Extension
|
||||
import org.commonmark.ext.maths.internal.DollarMathsDelimiterProcessor
|
||||
import org.commonmark.ext.maths.internal.MathsHtmlNodeRenderer
|
||||
import org.commonmark.parser.Parser
|
||||
import org.commonmark.renderer.html.HtmlRenderer
|
||||
|
||||
class MathsExtension private constructor() : Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension {
|
||||
override fun extend(parserBuilder: Parser.Builder) {
|
||||
parserBuilder.customDelimiterProcessor(DollarMathsDelimiterProcessor())
|
||||
}
|
||||
|
||||
override fun extend(rendererBuilder: HtmlRenderer.Builder) {
|
||||
rendererBuilder.nodeRendererFactory { context -> MathsHtmlNodeRenderer(context) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(): Extension {
|
||||
return MathsExtension()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.commonmark.ext.maths.internal
|
||||
|
||||
import org.commonmark.ext.maths.DisplayMaths
|
||||
import org.commonmark.ext.maths.InlineMaths
|
||||
import org.commonmark.node.Text
|
||||
import org.commonmark.parser.delimiter.DelimiterProcessor
|
||||
import org.commonmark.parser.delimiter.DelimiterRun
|
||||
|
||||
class DollarMathsDelimiterProcessor : DelimiterProcessor {
|
||||
override fun getOpeningCharacter() = '$'
|
||||
|
||||
override fun getClosingCharacter() = '$'
|
||||
|
||||
override fun getMinLength() = 1
|
||||
|
||||
override fun getDelimiterUse(opener: DelimiterRun, closer: DelimiterRun): Int {
|
||||
return if (opener.length() == 1 && closer.length() == 1) 1 // inline
|
||||
else if (opener.length() == 2 && closer.length() == 2) 2 // display
|
||||
else 0
|
||||
}
|
||||
|
||||
override fun process(opener: Text, closer: Text, delimiterUse: Int) {
|
||||
val maths = if (delimiterUse == 1) {
|
||||
InlineMaths(InlineMaths.InlineDelimiter.SINGLE_DOLLAR)
|
||||
} else {
|
||||
DisplayMaths(DisplayMaths.DisplayDelimiter.DOUBLE_DOLLAR)
|
||||
}
|
||||
var tmp = opener.next
|
||||
while (tmp != null && tmp !== closer) {
|
||||
val next = tmp.next
|
||||
maths.appendChild(tmp)
|
||||
tmp = next
|
||||
}
|
||||
opener.insertAfter(maths)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.commonmark.ext.maths.internal
|
||||
|
||||
import org.commonmark.ext.maths.DisplayMaths
|
||||
import org.commonmark.node.Node
|
||||
import org.commonmark.node.Text
|
||||
import org.commonmark.renderer.html.HtmlNodeRendererContext
|
||||
import org.commonmark.renderer.html.HtmlWriter
|
||||
import java.util.Collections
|
||||
|
||||
class MathsHtmlNodeRenderer(private val context: HtmlNodeRendererContext) : MathsNodeRenderer() {
|
||||
private val html: HtmlWriter = context.writer
|
||||
override fun render(node: Node) {
|
||||
val display = node.javaClass == DisplayMaths::class.java
|
||||
val contents = node.firstChild // should be the only child
|
||||
val latex = (contents as Text).literal
|
||||
val attributes = context.extendAttributes(node, if (display) "div" else "span", Collections.singletonMap("data-mx-maths",
|
||||
latex))
|
||||
html.tag(if (display) "div" else "span", attributes)
|
||||
html.tag("code")
|
||||
context.render(contents)
|
||||
html.tag("/code")
|
||||
html.tag(if (display) "/div" else "/span")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.commonmark.ext.maths.internal
|
||||
|
||||
import org.commonmark.ext.maths.DisplayMaths
|
||||
import org.commonmark.ext.maths.InlineMaths
|
||||
import org.commonmark.node.Node
|
||||
import org.commonmark.renderer.NodeRenderer
|
||||
import java.util.HashSet
|
||||
|
||||
abstract class MathsNodeRenderer : NodeRenderer {
|
||||
override fun getNodeTypes(): Set<Class<out Node>> {
|
||||
val types: MutableSet<Class<out Node>> = HashSet()
|
||||
types.add(InlineMaths::class.java)
|
||||
types.add(DisplayMaths::class.java)
|
||||
return types
|
||||
}
|
||||
}
|
|
@ -27,4 +27,5 @@ object UserAccountDataTypes {
|
|||
const val TYPE_ALLOWED_WIDGETS = "im.vector.setting.allowed_widgets"
|
||||
const val TYPE_IDENTITY_SERVER = "m.identity_server"
|
||||
const val TYPE_ACCEPTED_TERMS = "m.accepted_terms"
|
||||
const val TYPE_OVERRIDE_COLORS = "im.vector.setting.override_colors"
|
||||
}
|
||||
|
|
|
@ -21,4 +21,5 @@ object RoomAccountDataTypes {
|
|||
const val EVENT_TYPE_TAG = "m.tag"
|
||||
const val EVENT_TYPE_FULLY_READ = "m.fully_read"
|
||||
const val EVENT_TYPE_SPACE_ORDER = "org.matrix.msc3230.space_order" // m.space_order
|
||||
const val EVENT_TYPE_TAGGED_EVENTS = "m.tagged_events"
|
||||
}
|
||||
|
|
|
@ -71,14 +71,10 @@ interface Timeline {
|
|||
fun paginate(direction: Direction, count: Int)
|
||||
|
||||
/**
|
||||
* Returns the number of sending events
|
||||
* This is the same than the regular paginate method but waits for the results instead
|
||||
* of relying on the timeline listener.
|
||||
*/
|
||||
fun pendingEventCount(): Int
|
||||
|
||||
/**
|
||||
* Returns the number of failed sending events.
|
||||
*/
|
||||
fun failedToDeliverEventCount(): Int
|
||||
suspend fun awaitPaginate(direction: Direction, count: Int): List<TimelineEvent>
|
||||
|
||||
/**
|
||||
* Returns the index of a built event or null.
|
||||
|
@ -86,14 +82,14 @@ interface Timeline {
|
|||
fun getIndexOfEvent(eventId: String?): Int?
|
||||
|
||||
/**
|
||||
* Returns the built [TimelineEvent] at index or null
|
||||
* Returns the current pagination state for the direction.
|
||||
*/
|
||||
fun getTimelineEventAtIndex(index: Int): TimelineEvent?
|
||||
fun getPaginationState(direction: Direction): PaginationState
|
||||
|
||||
/**
|
||||
* Returns the built [TimelineEvent] with eventId or null
|
||||
* Returns a snapshot of the timeline in his current state.
|
||||
*/
|
||||
fun getTimelineEventWithId(eventId: String?): TimelineEvent?
|
||||
fun getSnapshot(): List<TimelineEvent>
|
||||
|
||||
interface Listener {
|
||||
/**
|
||||
|
@ -101,19 +97,33 @@ interface Timeline {
|
|||
* The latest event is the first in the list
|
||||
* @param snapshot the most up to date snapshot
|
||||
*/
|
||||
fun onTimelineUpdated(snapshot: List<TimelineEvent>)
|
||||
fun onTimelineUpdated(snapshot: List<TimelineEvent>) = Unit
|
||||
|
||||
/**
|
||||
* Called whenever an error we can't recover from occurred
|
||||
*/
|
||||
fun onTimelineFailure(throwable: Throwable)
|
||||
fun onTimelineFailure(throwable: Throwable) = Unit
|
||||
|
||||
/**
|
||||
* Called when new events come through the sync
|
||||
*/
|
||||
fun onNewTimelineEvents(eventIds: List<String>)
|
||||
fun onNewTimelineEvents(eventIds: List<String>) = Unit
|
||||
|
||||
/**
|
||||
* Called when the pagination state has changed in one direction
|
||||
*/
|
||||
fun onStateUpdated(direction: Direction, state: PaginationState) = Unit
|
||||
}
|
||||
|
||||
/**
|
||||
* Pagination state
|
||||
*/
|
||||
data class PaginationState(
|
||||
val hasMoreToLoad: Boolean = true,
|
||||
val loading: Boolean = false,
|
||||
val inError: Boolean = false
|
||||
)
|
||||
|
||||
/**
|
||||
* This is used to paginate in one or another direction.
|
||||
*/
|
||||
|
|
|
@ -49,6 +49,10 @@ data class TimelineEvent(
|
|||
*/
|
||||
val localId: Long,
|
||||
val eventId: String,
|
||||
/**
|
||||
* This display index is the position in the current chunk.
|
||||
* It's not unique on the timeline as it's reset on each chunk.
|
||||
*/
|
||||
val displayIndex: Int,
|
||||
val senderInfo: SenderInfo,
|
||||
val annotations: EventAnnotationsSummary? = null,
|
||||
|
|
|
@ -133,6 +133,11 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||
if (requestKeysOnFail) {
|
||||
requestKeysForEvent(event, false)
|
||||
}
|
||||
|
||||
throw MXCryptoError.Base(
|
||||
MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX,
|
||||
"UNKNOWN_MESSAGE_INDEX",
|
||||
null)
|
||||
}
|
||||
|
||||
val reason = String.format(MXCryptoError.OLM_REASON, throwable.olmException.message)
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.database
|
||||
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.SessionLifecycleObserver
|
||||
import org.matrix.android.sdk.internal.database.helper.nextDisplayIndex
|
||||
import org.matrix.android.sdk.internal.database.model.ChunkEntity
|
||||
import org.matrix.android.sdk.internal.database.model.ChunkEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||
import org.matrix.android.sdk.internal.database.model.RoomEntity
|
||||
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
|
||||
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.deleteOnCascade
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val MAX_NUMBER_OF_EVENTS_IN_DB = 35_000L
|
||||
private const val MIN_NUMBER_OF_EVENTS_BY_CHUNK = 300
|
||||
|
||||
/**
|
||||
* This class makes sure to stay under a maximum number of events as it makes Realm to be unusable when listening to events
|
||||
* when the database is getting too big. This will try incrementally to remove the biggest chunks until we get below the threshold.
|
||||
* We make sure to still have a minimum number of events so it's not becoming unusable.
|
||||
* So this won't work for users with a big number of very active rooms.
|
||||
*/
|
||||
internal class DatabaseCleaner @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration,
|
||||
private val taskExecutor: TaskExecutor) : SessionLifecycleObserver {
|
||||
|
||||
override fun onSessionStarted(session: Session) {
|
||||
taskExecutor.executorScope.launch(Dispatchers.Default) {
|
||||
awaitTransaction(realmConfiguration) { realm ->
|
||||
val allRooms = realm.where(RoomEntity::class.java).findAll()
|
||||
Timber.v("There are ${allRooms.size} rooms in this session")
|
||||
cleanUp(realm, MAX_NUMBER_OF_EVENTS_IN_DB / 2L)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanUp(realm: Realm, threshold: Long) {
|
||||
val numberOfEvents = realm.where(EventEntity::class.java).findAll().size
|
||||
val numberOfTimelineEvents = realm.where(TimelineEventEntity::class.java).findAll().size
|
||||
Timber.v("Number of events in db: $numberOfEvents | Number of timeline events in db: $numberOfTimelineEvents")
|
||||
if (threshold <= MIN_NUMBER_OF_EVENTS_BY_CHUNK || numberOfTimelineEvents < MAX_NUMBER_OF_EVENTS_IN_DB) {
|
||||
Timber.v("Db is low enough")
|
||||
} else {
|
||||
val thresholdChunks = realm.where(ChunkEntity::class.java)
|
||||
.greaterThan(ChunkEntityFields.NUMBER_OF_TIMELINE_EVENTS, threshold)
|
||||
.findAll()
|
||||
|
||||
Timber.v("There are ${thresholdChunks.size} chunks to clean with more than $threshold events")
|
||||
for (chunk in thresholdChunks) {
|
||||
val maxDisplayIndex = chunk.nextDisplayIndex(PaginationDirection.FORWARDS)
|
||||
val thresholdDisplayIndex = maxDisplayIndex - threshold
|
||||
val eventsToRemove = chunk.timelineEvents.where().lessThan(TimelineEventEntityFields.DISPLAY_INDEX, thresholdDisplayIndex).findAll()
|
||||
Timber.v("There are ${eventsToRemove.size} events to clean in chunk: ${chunk.identifier()} from room ${chunk.room?.first()?.roomId}")
|
||||
chunk.numberOfTimelineEvents = chunk.numberOfTimelineEvents - eventsToRemove.size
|
||||
eventsToRemove.forEach {
|
||||
val canDeleteRoot = it.root?.stateKey == null
|
||||
it.deleteOnCascade(canDeleteRoot)
|
||||
}
|
||||
// We reset the prevToken so we will need to fetch again.
|
||||
chunk.prevToken = null
|
||||
}
|
||||
cleanUp(realm, (threshold / 1.5).toLong())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.room.model.VersioningState
|
|||
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
|
||||
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
|
||||
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
|
||||
import org.matrix.android.sdk.internal.database.model.ChunkEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.EditionOfEventFields
|
||||
|
@ -55,7 +56,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
|||
) : RealmMigration {
|
||||
|
||||
companion object {
|
||||
const val SESSION_STORE_SCHEMA_VERSION = 20L
|
||||
const val SESSION_STORE_SCHEMA_VERSION = 21L
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -88,6 +89,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
|||
if (oldVersion <= 17) migrateTo18(realm)
|
||||
if (oldVersion <= 18) migrateTo19(realm)
|
||||
if (oldVersion <= 19) migrateTo20(realm)
|
||||
if (oldVersion <= 20) migrateTo21(realm)
|
||||
}
|
||||
|
||||
private fun migrateTo1(realm: DynamicRealm) {
|
||||
|
@ -395,6 +397,28 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
|||
|
||||
private fun migrateTo20(realm: DynamicRealm) {
|
||||
Timber.d("Step 19 -> 20")
|
||||
realm.schema.get("ChunkEntity")?.apply {
|
||||
if (hasField("numberOfTimelineEvents")) {
|
||||
removeField("numberOfTimelineEvents")
|
||||
}
|
||||
var cleanOldChunks = false
|
||||
if (!hasField(ChunkEntityFields.NEXT_CHUNK.`$`)) {
|
||||
cleanOldChunks = true
|
||||
addRealmObjectField(ChunkEntityFields.NEXT_CHUNK.`$`, this)
|
||||
}
|
||||
if (!hasField(ChunkEntityFields.PREV_CHUNK.`$`)) {
|
||||
cleanOldChunks = true
|
||||
addRealmObjectField(ChunkEntityFields.PREV_CHUNK.`$`, this)
|
||||
}
|
||||
if (cleanOldChunks) {
|
||||
val chunkEntities = realm.where("ChunkEntity").equalTo(ChunkEntityFields.IS_LAST_FORWARD, false).findAll()
|
||||
chunkEntities.deleteAllFromRealm()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun migrateTo21(realm: DynamicRealm) {
|
||||
Timber.d("Step 20 -> 21")
|
||||
val eventEntity = realm.schema.get("TimelineEventEntity") ?: return
|
||||
|
||||
realm.schema.get("EventEntity")
|
||||
|
|
|
@ -111,7 +111,7 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
|
|||
true
|
||||
}
|
||||
}
|
||||
numberOfTimelineEvents++
|
||||
// numberOfTimelineEvents++
|
||||
timelineEvents.add(timelineEventEntity)
|
||||
}
|
||||
|
||||
|
@ -204,3 +204,29 @@ internal fun ChunkEntity.nextDisplayIndex(direction: PaginationDirection): Int {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ChunkEntity.doesNextChunksVerifyCondition(linkCondition: (ChunkEntity) -> Boolean): Boolean {
|
||||
var nextChunkToCheck = this.nextChunk
|
||||
while (nextChunkToCheck != null) {
|
||||
if (linkCondition(nextChunkToCheck)) {
|
||||
return true
|
||||
}
|
||||
nextChunkToCheck = nextChunkToCheck.nextChunk
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
internal fun ChunkEntity.isMoreRecentThan(chunkToCheck: ChunkEntity): Boolean {
|
||||
if (this.isLastForward) return true
|
||||
if (chunkToCheck.isLastForward) return false
|
||||
// Check if the chunk to check is linked to this one
|
||||
if (chunkToCheck.doesNextChunksVerifyCondition { it == this }) {
|
||||
return true
|
||||
}
|
||||
// Otherwise check if this chunk is linked to last forward
|
||||
if (this.doesNextChunksVerifyCondition { it.isLastForward }) {
|
||||
return true
|
||||
}
|
||||
// We don't know, so we assume it's false
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -28,3 +28,13 @@ internal fun TimelineEventEntity.Companion.nextId(realm: Realm): Long {
|
|||
currentIdNum.toLong() + 1
|
||||
}
|
||||
}
|
||||
|
||||
internal fun TimelineEventEntity.isMoreRecentThan(eventToCheck: TimelineEventEntity): Boolean {
|
||||
val currentChunk = this.chunk?.first() ?: return false
|
||||
val chunkToCheck = eventToCheck.chunk?.firstOrNull() ?: return false
|
||||
return if (currentChunk == chunkToCheck) {
|
||||
this.displayIndex >= eventToCheck.displayIndex
|
||||
} else {
|
||||
currentChunk.isMoreRecentThan(chunkToCheck)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,9 +27,10 @@ import org.matrix.android.sdk.internal.extensions.clearWith
|
|||
internal open class ChunkEntity(@Index var prevToken: String? = null,
|
||||
// Because of gaps we can have several chunks with nextToken == null
|
||||
@Index var nextToken: String? = null,
|
||||
var prevChunk: ChunkEntity? = null,
|
||||
var nextChunk: ChunkEntity? = null,
|
||||
var stateEvents: RealmList<EventEntity> = RealmList(),
|
||||
var timelineEvents: RealmList<TimelineEventEntity> = RealmList(),
|
||||
var numberOfTimelineEvents: Long = 0,
|
||||
// Only one chunk will have isLastForward == true
|
||||
@Index var isLastForward: Boolean = false,
|
||||
@Index var isLastBackward: Boolean = false
|
||||
|
|
|
@ -41,8 +41,6 @@ internal open class EventEntity(@Index var eventId: String = "",
|
|||
var unsignedData: String? = null,
|
||||
var redacts: String? = null,
|
||||
var decryptionResultJson: String? = null,
|
||||
var decryptionErrorCode: String? = null,
|
||||
var decryptionErrorReason: String? = null,
|
||||
var ageLocalTs: Long? = null,
|
||||
// Thread related, no need to create a new Entity for performance
|
||||
@Index var isRootThread: Boolean = false,
|
||||
|
@ -70,6 +68,16 @@ internal open class EventEntity(@Index var eventId: String = "",
|
|||
threadNotificationStateStr = value.name
|
||||
}
|
||||
|
||||
var decryptionErrorCode: String? = null
|
||||
set(value) {
|
||||
if (value != field) field = value
|
||||
}
|
||||
|
||||
var decryptionErrorReason: String? = null
|
||||
set(value) {
|
||||
if (value != field) field = value
|
||||
}
|
||||
|
||||
companion object
|
||||
|
||||
fun setDecryptionResult(result: MXEventDecryptionResult, clearEvent: JsonDict? = null) {
|
||||
|
|
|
@ -46,7 +46,5 @@ internal fun TimelineEventEntity.deleteOnCascade(canDeleteRoot: Boolean) {
|
|||
if (canDeleteRoot) {
|
||||
root?.deleteFromRealm()
|
||||
}
|
||||
annotations?.deleteOnCascade()
|
||||
readReceipts?.deleteOnCascade()
|
||||
deleteFromRealm()
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.database.query
|
|||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import org.matrix.android.sdk.api.session.events.model.LocalEcho
|
||||
import org.matrix.android.sdk.internal.database.helper.isMoreRecentThan
|
||||
import org.matrix.android.sdk.internal.database.model.ChunkEntity
|
||||
import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity
|
||||
import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity
|
||||
|
@ -33,28 +34,26 @@ internal fun isEventRead(realmConfiguration: RealmConfiguration,
|
|||
if (LocalEcho.isLocalEchoId(eventId)) {
|
||||
return true
|
||||
}
|
||||
// If we don't know if the event has been read, we assume it's not
|
||||
var isEventRead = false
|
||||
|
||||
Realm.getInstance(realmConfiguration).use { realm ->
|
||||
val liveChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) ?: return@use
|
||||
val eventToCheck = liveChunk.timelineEvents.find(eventId)
|
||||
val latestEvent = TimelineEventEntity.latestEvent(realm, roomId, true)
|
||||
// If latest event is from you we are sure the event is read
|
||||
if (latestEvent?.root?.sender == userId) {
|
||||
return true
|
||||
}
|
||||
val eventToCheck = TimelineEventEntity.where(realm, roomId, eventId).findFirst()
|
||||
isEventRead = when {
|
||||
eventToCheck == null -> hasReadMissingEvent(
|
||||
realm = realm,
|
||||
latestChunkEntity = liveChunk,
|
||||
roomId = roomId,
|
||||
userId = userId,
|
||||
eventId = eventId
|
||||
)
|
||||
eventToCheck == null -> false
|
||||
eventToCheck.root?.sender == userId -> true
|
||||
else -> {
|
||||
val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst() ?: return@use
|
||||
val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.displayIndex ?: Int.MIN_VALUE
|
||||
eventToCheck.displayIndex <= readReceiptIndex
|
||||
val readReceiptEvent = TimelineEventEntity.where(realm, roomId, readReceipt.eventId).findFirst() ?: return@use
|
||||
readReceiptEvent.isMoreRecentThan(eventToCheck)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return isEventRead
|
||||
}
|
||||
|
||||
|
|
|
@ -47,7 +47,6 @@ import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorage
|
|||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultRedactEventTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.RedactEventTask
|
||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationMessageProcessor
|
||||
import org.matrix.android.sdk.internal.database.DatabaseCleaner
|
||||
import org.matrix.android.sdk.internal.database.EventInsertLiveObserver
|
||||
import org.matrix.android.sdk.internal.database.RealmSessionProvider
|
||||
import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory
|
||||
|
@ -339,10 +338,6 @@ internal abstract class SessionModule {
|
|||
@IntoSet
|
||||
abstract fun bindIdentityService(service: DefaultIdentityService): SessionLifecycleObserver
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract fun bindDatabaseCleaner(cleaner: DatabaseCleaner): SessionLifecycleObserver
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract fun bindRealmSessionProvider(provider: RealmSessionProvider): SessionLifecycleObserver
|
||||
|
|
|
@ -19,6 +19,8 @@ package org.matrix.android.sdk.internal.session.room
|
|||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import org.commonmark.Extension
|
||||
import org.commonmark.ext.maths.MathsExtension
|
||||
import org.commonmark.parser.Parser
|
||||
import org.commonmark.renderer.html.HtmlRenderer
|
||||
import org.matrix.android.sdk.api.session.file.FileService
|
||||
|
@ -106,6 +108,8 @@ internal abstract class RoomModule {
|
|||
|
||||
@Module
|
||||
companion object {
|
||||
private val extensions: List<Extension> = listOf(MathsExtension.create())
|
||||
|
||||
@Provides
|
||||
@JvmStatic
|
||||
@SessionScope
|
||||
|
@ -123,7 +127,7 @@ internal abstract class RoomModule {
|
|||
@Provides
|
||||
@JvmStatic
|
||||
fun providesParser(): Parser {
|
||||
return Parser.builder().build()
|
||||
return Parser.builder().extensions(extensions).build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
@ -131,6 +135,7 @@ internal abstract class RoomModule {
|
|||
fun providesHtmlRenderer(): HtmlRenderer {
|
||||
return HtmlRenderer
|
||||
.builder()
|
||||
.extensions(extensions)
|
||||
.softbreak("<br />")
|
||||
.build()
|
||||
}
|
||||
|
|
|
@ -34,12 +34,10 @@ import org.matrix.android.sdk.internal.database.query.isEventRead
|
|||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.di.UserId
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
|
||||
internal class DefaultReadService @AssistedInject constructor(
|
||||
@Assisted private val roomId: String,
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val setReadMarkersTask: SetReadMarkersTask,
|
||||
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
|
||||
@UserId private val userId: String
|
||||
|
|
|
@ -44,10 +44,10 @@ internal class CancelSendTracker @Inject constructor() {
|
|||
}
|
||||
|
||||
fun isCancelRequestedFor(eventId: String?, roomId: String?): Boolean {
|
||||
val index = synchronized(cancellingRequests) {
|
||||
cancellingRequests.indexOfFirst { it.localId == eventId && it.roomId == roomId }
|
||||
val found = synchronized(cancellingRequests) {
|
||||
cancellingRequests.any { it.localId == eventId && it.roomId == roomId }
|
||||
}
|
||||
return index != -1
|
||||
return found
|
||||
}
|
||||
|
||||
fun markCancelled(eventId: String, roomId: String) {
|
||||
|
|
|
@ -32,7 +32,7 @@ internal class MarkdownParser @Inject constructor(
|
|||
private val textPillsUtils: TextPillsUtils
|
||||
) {
|
||||
|
||||
private val mdSpecialChars = "[`_\\-*>.\\[\\]#~]".toRegex()
|
||||
private val mdSpecialChars = "[`_\\-*>.\\[\\]#~$]".toRegex()
|
||||
|
||||
fun parse(text: CharSequence): TextContent {
|
||||
val source = textPillsUtils.processSpecialSpansToMarkdown(text) ?: text.toString()
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.summary
|
|||
|
||||
import io.realm.Realm
|
||||
import io.realm.kotlin.createObject
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
|
@ -136,7 +137,7 @@ internal class RoomSummaryUpdater @Inject constructor(
|
|||
|
||||
roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 ||
|
||||
// avoid this call if we are sure there are unread events
|
||||
!isEventRead(realm.configuration, userId, roomId, latestPreviewableEvent?.eventId)
|
||||
latestPreviewableEvent?.let { !isEventRead(realm.configuration, userId, roomId, it.eventId) } ?: false
|
||||
|
||||
roomSummaryEntity.setDisplayName(roomDisplayNameResolver.resolve(realm, roomId))
|
||||
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(realm, roomId)
|
||||
|
@ -236,7 +237,7 @@ internal class RoomSummaryUpdater @Inject constructor(
|
|||
.findFirst()
|
||||
?.let { childSum ->
|
||||
lookupMap.entries.firstOrNull { it.key.roomId == lookedUp.roomId }?.let { entry ->
|
||||
if (entry.value.indexOfFirst { it.roomId == childSum.roomId } == -1) {
|
||||
if (entry.value.none { it.roomId == childSum.roomId }) {
|
||||
// add looked up as a parent
|
||||
entry.value.add(childSum)
|
||||
}
|
||||
|
@ -299,7 +300,7 @@ internal class RoomSummaryUpdater @Inject constructor(
|
|||
.process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships())
|
||||
.findFirst()
|
||||
?.let { parentSum ->
|
||||
if (lookupMap[parentSum]?.indexOfFirst { it.roomId == lookedUp.roomId } == -1) {
|
||||
if (lookupMap[parentSum]?.none { it.roomId == lookedUp.roomId }.orFalse()) {
|
||||
// add lookedup as a parent
|
||||
lookupMap[parentSum]?.add(lookedUp)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.room.taggedevents
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Keys are event IDs, values are event information.
|
||||
*/
|
||||
typealias TaggedEvent = Map<String, TaggedEventInfo>
|
||||
|
||||
/**
|
||||
* Keys are tagged event names (eg. m.favourite), values are the related events.
|
||||
*/
|
||||
typealias TaggedEvents = Map<String, TaggedEvent>
|
||||
|
||||
/**
|
||||
* Class used to parse the content of a m.tagged_events type event.
|
||||
* This kind of event defines the tagged events in a room.
|
||||
*
|
||||
* The content of this event is a tags key whose value is an object mapping the name of each tag
|
||||
* to another object. The JSON object associated with each tag is an object where the keys are the
|
||||
* event IDs and values give information about the events.
|
||||
*
|
||||
* Ref: https://github.com/matrix-org/matrix-doc/pull/2437
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TaggedEventsContent(
|
||||
@Json(name = "tags")
|
||||
var tags: TaggedEvents = emptyMap()
|
||||
) {
|
||||
val favouriteEvents
|
||||
get() = tags[TAG_FAVOURITE].orEmpty()
|
||||
|
||||
val hiddenEvents
|
||||
get() = tags[TAG_HIDDEN].orEmpty()
|
||||
|
||||
fun tagEvent(eventId: String, info: TaggedEventInfo, tag: String) {
|
||||
val taggedEvents = tags[tag].orEmpty().plus(eventId to info)
|
||||
tags = tags.plus(tag to taggedEvents)
|
||||
}
|
||||
|
||||
fun untagEvent(eventId: String, tag: String) {
|
||||
val taggedEvents = tags[tag]?.minus(eventId).orEmpty()
|
||||
tags = tags.plus(tag to taggedEvents)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG_FAVOURITE = "m.favourite"
|
||||
const val TAG_HIDDEN = "m.hidden"
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TaggedEventInfo(
|
||||
@Json(name = "keywords")
|
||||
val keywords: List<String>? = null,
|
||||
|
||||
@Json(name = "origin_server_ts")
|
||||
val originServerTs: Long? = null,
|
||||
|
||||
@Json(name = "tagged_at")
|
||||
val taggedAt: Long? = null
|
||||
)
|
File diff suppressed because it is too large
Load diff
|
@ -24,6 +24,7 @@ import dagger.assisted.AssistedInject
|
|||
import io.realm.Realm
|
||||
import io.realm.Sort
|
||||
import io.realm.kotlin.where
|
||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.api.session.events.model.isImageMessage
|
||||
import org.matrix.android.sdk.api.session.events.model.isVideoMessage
|
||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
|
@ -63,7 +64,8 @@ internal class DefaultTimelineService @AssistedInject constructor(
|
|||
private val timelineEventMapper: TimelineEventMapper,
|
||||
private val loadRoomMembersTask: LoadRoomMembersTask,
|
||||
private val threadsAwarenessHandler: ThreadsAwarenessHandler,
|
||||
private val readReceiptHandler: ReadReceiptHandler
|
||||
private val readReceiptHandler: ReadReceiptHandler,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers
|
||||
) : TimelineService {
|
||||
|
||||
@AssistedFactory
|
||||
|
@ -75,19 +77,18 @@ internal class DefaultTimelineService @AssistedInject constructor(
|
|||
return DefaultTimeline(
|
||||
roomId = roomId,
|
||||
initialEventId = eventId,
|
||||
settings = settings,
|
||||
realmConfiguration = monarchy.realmConfiguration,
|
||||
taskExecutor = taskExecutor,
|
||||
contextOfEventTask = contextOfEventTask,
|
||||
coroutineDispatchers = coroutineDispatchers,
|
||||
paginationTask = paginationTask,
|
||||
timelineEventMapper = timelineEventMapper,
|
||||
settings = settings,
|
||||
timelineInput = timelineInput,
|
||||
eventDecryptor = eventDecryptor,
|
||||
fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
|
||||
realmSessionProvider = realmSessionProvider,
|
||||
loadRoomMembersTask = loadRoomMembersTask,
|
||||
threadsAwarenessHandler = threadsAwarenessHandler,
|
||||
readReceiptHandler = readReceiptHandler
|
||||
readReceiptHandler = readReceiptHandler,
|
||||
getEventTask = contextOfEventTask,
|
||||
threadsAwarenessHandler = threadsAwarenessHandler
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -16,9 +16,8 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.session.room.timeline
|
||||
|
||||
internal data class TimelineState(
|
||||
val hasReachedEnd: Boolean = false,
|
||||
val hasMoreInCache: Boolean = true,
|
||||
val isPaginating: Boolean = false,
|
||||
val requestedPaginationCount: Int = 0
|
||||
)
|
||||
internal enum class LoadMoreResult {
|
||||
REACHED_END,
|
||||
SUCCESS,
|
||||
FAILURE
|
||||
}
|
|
@ -0,0 +1,232 @@
|
|||
/*
|
||||
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.room.timeline
|
||||
|
||||
import io.realm.OrderedCollectionChangeSet
|
||||
import io.realm.OrderedRealmCollectionChangeListener
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmResults
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
|
||||
import org.matrix.android.sdk.internal.database.model.ChunkEntity
|
||||
import org.matrix.android.sdk.internal.database.model.ChunkEntityFields
|
||||
import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
/**
|
||||
* This class is responsible for keeping an instance of chunkEntity and timelineChunk according to the strategy.
|
||||
* There is 2 different mode: Live and Permalink.
|
||||
* In Live, we will query for the live chunk (isLastForward = true).
|
||||
* In Permalink, we will query for the chunk including the eventId we are looking for.
|
||||
* Once we got a ChunkEntity we wrap it with TimelineChunk class so we dispatch any methods for loading data.
|
||||
*/
|
||||
|
||||
internal class LoadTimelineStrategy(
|
||||
private val roomId: String,
|
||||
private val timelineId: String,
|
||||
private val mode: Mode,
|
||||
private val dependencies: Dependencies) {
|
||||
|
||||
sealed interface Mode {
|
||||
object Live : Mode
|
||||
data class Permalink(val originEventId: String) : Mode
|
||||
|
||||
fun originEventId(): String? {
|
||||
return if (this is Permalink) {
|
||||
originEventId
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class Dependencies(
|
||||
val timelineSettings: TimelineSettings,
|
||||
val realm: AtomicReference<Realm>,
|
||||
val eventDecryptor: TimelineEventDecryptor,
|
||||
val paginationTask: PaginationTask,
|
||||
val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
|
||||
val getContextOfEventTask: GetContextOfEventTask,
|
||||
val timelineInput: TimelineInput,
|
||||
val timelineEventMapper: TimelineEventMapper,
|
||||
val threadsAwarenessHandler: ThreadsAwarenessHandler,
|
||||
val onEventsUpdated: (Boolean) -> Unit,
|
||||
val onLimitedTimeline: () -> Unit,
|
||||
val onNewTimelineEvents: (List<String>) -> Unit
|
||||
)
|
||||
|
||||
private var getContextLatch: CompletableDeferred<Unit>? = null
|
||||
private var chunkEntity: RealmResults<ChunkEntity>? = null
|
||||
private var timelineChunk: TimelineChunk? = null
|
||||
|
||||
private val chunkEntityListener = OrderedRealmCollectionChangeListener { _: RealmResults<ChunkEntity>, changeSet: OrderedCollectionChangeSet ->
|
||||
// Can be call either when you open a permalink on an unknown event
|
||||
// or when there is a gap in the timeline.
|
||||
val shouldRebuildChunk = changeSet.insertions.isNotEmpty()
|
||||
if (shouldRebuildChunk) {
|
||||
timelineChunk?.close(closeNext = true, closePrev = true)
|
||||
timelineChunk = chunkEntity?.createTimelineChunk()
|
||||
// If we are waiting for a result of get context, post completion
|
||||
getContextLatch?.complete(Unit)
|
||||
// If we have a gap, just tell the timeline about it.
|
||||
if (timelineChunk?.hasReachedLastForward().orFalse()) {
|
||||
dependencies.onLimitedTimeline()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val uiEchoManagerListener = object : UIEchoManager.Listener {
|
||||
override fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?): Boolean {
|
||||
return timelineChunk?.rebuildEvent(eventId, builder, searchInNext = true, searchInPrev = true).orFalse()
|
||||
}
|
||||
}
|
||||
|
||||
private val timelineInputListener = object : TimelineInput.Listener {
|
||||
|
||||
override fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) {
|
||||
if (roomId != this@LoadTimelineStrategy.roomId) {
|
||||
return
|
||||
}
|
||||
if (uiEchoManager.onLocalEchoCreated(timelineEvent)) {
|
||||
dependencies.onNewTimelineEvents(listOf(timelineEvent.eventId))
|
||||
dependencies.onEventsUpdated(false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState) {
|
||||
if (roomId != this@LoadTimelineStrategy.roomId) {
|
||||
return
|
||||
}
|
||||
if (uiEchoManager.onSendStateUpdated(eventId, sendState)) {
|
||||
dependencies.onEventsUpdated(false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewTimelineEvents(roomId: String, eventIds: List<String>) {
|
||||
if (roomId == this@LoadTimelineStrategy.roomId && hasReachedLastForward()) {
|
||||
dependencies.onNewTimelineEvents(eventIds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val uiEchoManager = UIEchoManager(uiEchoManagerListener)
|
||||
private val sendingEventsDataSource: SendingEventsDataSource = RealmSendingEventsDataSource(
|
||||
roomId = roomId,
|
||||
realm = dependencies.realm,
|
||||
uiEchoManager = uiEchoManager,
|
||||
timelineEventMapper = dependencies.timelineEventMapper,
|
||||
onEventsUpdated = dependencies.onEventsUpdated
|
||||
)
|
||||
|
||||
fun onStart() {
|
||||
dependencies.eventDecryptor.start()
|
||||
dependencies.timelineInput.listeners.add(timelineInputListener)
|
||||
val realm = dependencies.realm.get()
|
||||
sendingEventsDataSource.start()
|
||||
chunkEntity = getChunkEntity(realm).also {
|
||||
it.addChangeListener(chunkEntityListener)
|
||||
timelineChunk = it.createTimelineChunk()
|
||||
}
|
||||
}
|
||||
|
||||
fun onStop() {
|
||||
dependencies.eventDecryptor.destroy()
|
||||
dependencies.timelineInput.listeners.remove(timelineInputListener)
|
||||
chunkEntity?.removeChangeListener(chunkEntityListener)
|
||||
sendingEventsDataSource.stop()
|
||||
timelineChunk?.close(closeNext = true, closePrev = true)
|
||||
getContextLatch?.cancel()
|
||||
chunkEntity = null
|
||||
timelineChunk = null
|
||||
}
|
||||
|
||||
suspend fun loadMore(count: Int, direction: Timeline.Direction, fetchOnServerIfNeeded: Boolean = true): LoadMoreResult {
|
||||
if (mode is Mode.Permalink && timelineChunk == null) {
|
||||
val params = GetContextOfEventTask.Params(roomId, mode.originEventId)
|
||||
try {
|
||||
getContextLatch = CompletableDeferred()
|
||||
dependencies.getContextOfEventTask.execute(params)
|
||||
// waits for the query to be fulfilled
|
||||
getContextLatch?.await()
|
||||
getContextLatch = null
|
||||
} catch (failure: Throwable) {
|
||||
return LoadMoreResult.FAILURE
|
||||
}
|
||||
}
|
||||
return timelineChunk?.loadMore(count, direction, fetchOnServerIfNeeded) ?: LoadMoreResult.FAILURE
|
||||
}
|
||||
|
||||
fun getBuiltEventIndex(eventId: String): Int? {
|
||||
return timelineChunk?.getBuiltEventIndex(eventId, searchInNext = true, searchInPrev = true)
|
||||
}
|
||||
|
||||
fun getBuiltEvent(eventId: String): TimelineEvent? {
|
||||
return timelineChunk?.getBuiltEvent(eventId, searchInNext = true, searchInPrev = true)
|
||||
}
|
||||
|
||||
fun buildSnapshot(): List<TimelineEvent> {
|
||||
return buildSendingEvents() + timelineChunk?.builtItems(includesNext = true, includesPrev = true).orEmpty()
|
||||
}
|
||||
|
||||
private fun buildSendingEvents(): List<TimelineEvent> {
|
||||
return if (hasReachedLastForward()) {
|
||||
sendingEventsDataSource.buildSendingEvents()
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getChunkEntity(realm: Realm): RealmResults<ChunkEntity> {
|
||||
return if (mode is Mode.Permalink) {
|
||||
ChunkEntity.findAllIncludingEvents(realm, listOf(mode.originEventId))
|
||||
} else {
|
||||
ChunkEntity.where(realm, roomId)
|
||||
.equalTo(ChunkEntityFields.IS_LAST_FORWARD, true)
|
||||
.findAll()
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasReachedLastForward(): Boolean {
|
||||
return timelineChunk?.hasReachedLastForward().orFalse()
|
||||
}
|
||||
|
||||
private fun RealmResults<ChunkEntity>.createTimelineChunk(): TimelineChunk? {
|
||||
return firstOrNull()?.let {
|
||||
return TimelineChunk(
|
||||
chunkEntity = it,
|
||||
timelineSettings = dependencies.timelineSettings,
|
||||
roomId = roomId,
|
||||
timelineId = timelineId,
|
||||
eventDecryptor = dependencies.eventDecryptor,
|
||||
paginationTask = dependencies.paginationTask,
|
||||
fetchTokenAndPaginateTask = dependencies.fetchTokenAndPaginateTask,
|
||||
timelineEventMapper = dependencies.timelineEventMapper,
|
||||
uiEchoManager = uiEchoManager,
|
||||
threadsAwarenessHandler = dependencies.threadsAwarenessHandler,
|
||||
initialEventId = mode.originEventId(),
|
||||
onBuiltEvents = dependencies.onEventsUpdated
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.room.timeline
|
||||
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmChangeListener
|
||||
import io.realm.RealmList
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
|
||||
import org.matrix.android.sdk.internal.database.model.RoomEntity
|
||||
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
internal interface SendingEventsDataSource {
|
||||
fun start()
|
||||
fun stop()
|
||||
fun buildSendingEvents(): List<TimelineEvent>
|
||||
}
|
||||
|
||||
internal class RealmSendingEventsDataSource(
|
||||
private val roomId: String,
|
||||
private val realm: AtomicReference<Realm>,
|
||||
private val uiEchoManager: UIEchoManager,
|
||||
private val timelineEventMapper: TimelineEventMapper,
|
||||
private val onEventsUpdated: (Boolean) -> Unit
|
||||
) : SendingEventsDataSource {
|
||||
|
||||
private var roomEntity: RoomEntity? = null
|
||||
private var sendingTimelineEvents: RealmList<TimelineEventEntity>? = null
|
||||
private var frozenSendingTimelineEvents: RealmList<TimelineEventEntity>? = null
|
||||
|
||||
private val sendingTimelineEventsListener = RealmChangeListener<RealmList<TimelineEventEntity>> { events ->
|
||||
uiEchoManager.onSentEventsInDatabase(events.map { it.eventId })
|
||||
frozenSendingTimelineEvents = sendingTimelineEvents?.freeze()
|
||||
onEventsUpdated(false)
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
val safeRealm = realm.get()
|
||||
roomEntity = RoomEntity.where(safeRealm, roomId = roomId).findFirst()
|
||||
sendingTimelineEvents = roomEntity?.sendingTimelineEvents
|
||||
sendingTimelineEvents?.addChangeListener(sendingTimelineEventsListener)
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
sendingTimelineEvents?.removeChangeListener(sendingTimelineEventsListener)
|
||||
sendingTimelineEvents = null
|
||||
roomEntity = null
|
||||
}
|
||||
|
||||
override fun buildSendingEvents(): List<TimelineEvent> {
|
||||
val builtSendingEvents = mutableListOf<TimelineEvent>()
|
||||
uiEchoManager.getInMemorySendingEvents()
|
||||
.addWithUiEcho(builtSendingEvents)
|
||||
frozenSendingTimelineEvents
|
||||
?.filter { timelineEvent ->
|
||||
builtSendingEvents.none { it.eventId == timelineEvent.eventId }
|
||||
}
|
||||
?.map {
|
||||
timelineEventMapper.map(it)
|
||||
}?.addWithUiEcho(builtSendingEvents)
|
||||
|
||||
return builtSendingEvents
|
||||
}
|
||||
|
||||
private fun List<TimelineEvent>.addWithUiEcho(target: MutableList<TimelineEvent>) {
|
||||
target.addAll(
|
||||
map { uiEchoManager.updateSentStateWithUiEcho(it) }
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,479 @@
|
|||
/*
|
||||
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.room.timeline
|
||||
|
||||
import io.realm.OrderedCollectionChangeSet
|
||||
import io.realm.OrderedRealmCollectionChangeListener
|
||||
import io.realm.RealmObjectChangeListener
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.RealmResults
|
||||
import io.realm.Sort
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||
import org.matrix.android.sdk.internal.database.mapper.EventMapper
|
||||
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
|
||||
import org.matrix.android.sdk.internal.database.model.ChunkEntity
|
||||
import org.matrix.android.sdk.internal.database.model.ChunkEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
|
||||
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
|
||||
import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
|
||||
import timber.log.Timber
|
||||
import java.util.Collections
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
/**
|
||||
* This is a wrapper around a ChunkEntity in the database.
|
||||
* It does mainly listen to the db timeline events.
|
||||
* It also triggers pagination to the server when needed, or dispatch to the prev or next chunk if any.
|
||||
*/
|
||||
internal class TimelineChunk(private val chunkEntity: ChunkEntity,
|
||||
private val timelineSettings: TimelineSettings,
|
||||
private val roomId: String,
|
||||
private val timelineId: String,
|
||||
private val eventDecryptor: TimelineEventDecryptor,
|
||||
private val paginationTask: PaginationTask,
|
||||
private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
|
||||
private val timelineEventMapper: TimelineEventMapper,
|
||||
private val uiEchoManager: UIEchoManager? = null,
|
||||
private val threadsAwarenessHandler: ThreadsAwarenessHandler,
|
||||
private val initialEventId: String?,
|
||||
private val onBuiltEvents: (Boolean) -> Unit) {
|
||||
|
||||
private val isLastForward = AtomicBoolean(chunkEntity.isLastForward)
|
||||
private val isLastBackward = AtomicBoolean(chunkEntity.isLastBackward)
|
||||
private var prevChunkLatch: CompletableDeferred<Unit>? = null
|
||||
private var nextChunkLatch: CompletableDeferred<Unit>? = null
|
||||
|
||||
private val chunkObjectListener = RealmObjectChangeListener<ChunkEntity> { _, changeSet ->
|
||||
if (changeSet == null) return@RealmObjectChangeListener
|
||||
if (changeSet.isDeleted.orFalse()) {
|
||||
return@RealmObjectChangeListener
|
||||
}
|
||||
Timber.v("on chunk (${chunkEntity.identifier()}) changed: ${changeSet.changedFields?.joinToString(",")}")
|
||||
if (changeSet.isFieldChanged(ChunkEntityFields.IS_LAST_FORWARD)) {
|
||||
isLastForward.set(chunkEntity.isLastForward)
|
||||
}
|
||||
if (changeSet.isFieldChanged(ChunkEntityFields.IS_LAST_BACKWARD)) {
|
||||
isLastBackward.set(chunkEntity.isLastBackward)
|
||||
}
|
||||
if (changeSet.isFieldChanged(ChunkEntityFields.NEXT_CHUNK.`$`)) {
|
||||
nextChunk = createTimelineChunk(chunkEntity.nextChunk)
|
||||
nextChunkLatch?.complete(Unit)
|
||||
}
|
||||
if (changeSet.isFieldChanged(ChunkEntityFields.PREV_CHUNK.`$`)) {
|
||||
prevChunk = createTimelineChunk(chunkEntity.prevChunk)
|
||||
prevChunkLatch?.complete(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
private val timelineEventsChangeListener =
|
||||
OrderedRealmCollectionChangeListener { results: RealmResults<TimelineEventEntity>, changeSet: OrderedCollectionChangeSet ->
|
||||
Timber.v("on timeline events chunk update")
|
||||
val frozenResults = results.freeze()
|
||||
handleDatabaseChangeSet(frozenResults, changeSet)
|
||||
}
|
||||
|
||||
private var timelineEventEntities: RealmResults<TimelineEventEntity> = chunkEntity.sortedTimelineEvents()
|
||||
private val builtEvents: MutableList<TimelineEvent> = Collections.synchronizedList(ArrayList())
|
||||
private val builtEventsIndexes: MutableMap<String, Int> = Collections.synchronizedMap(HashMap<String, Int>())
|
||||
|
||||
private var nextChunk: TimelineChunk? = null
|
||||
private var prevChunk: TimelineChunk? = null
|
||||
|
||||
init {
|
||||
timelineEventEntities.addChangeListener(timelineEventsChangeListener)
|
||||
chunkEntity.addChangeListener(chunkObjectListener)
|
||||
}
|
||||
|
||||
fun hasReachedLastForward(): Boolean {
|
||||
return if (isLastForward.get()) {
|
||||
true
|
||||
} else {
|
||||
nextChunk?.hasReachedLastForward().orFalse()
|
||||
}
|
||||
}
|
||||
|
||||
fun builtItems(includesNext: Boolean, includesPrev: Boolean): List<TimelineEvent> {
|
||||
val deepBuiltItems = ArrayList<TimelineEvent>(builtEvents.size)
|
||||
if (includesNext) {
|
||||
val nextEvents = nextChunk?.builtItems(includesNext = true, includesPrev = false).orEmpty()
|
||||
deepBuiltItems.addAll(nextEvents)
|
||||
}
|
||||
deepBuiltItems.addAll(builtEvents)
|
||||
if (includesPrev) {
|
||||
val prevEvents = prevChunk?.builtItems(includesNext = false, includesPrev = true).orEmpty()
|
||||
deepBuiltItems.addAll(prevEvents)
|
||||
}
|
||||
return deepBuiltItems
|
||||
}
|
||||
|
||||
/**
|
||||
* This will take care of loading and building events of this chunk for the given direction and count.
|
||||
* If @param fetchFromServerIfNeeded is true, it will try to fetch more events on server to get the right amount of data.
|
||||
* This method will also post a snapshot as soon the data is built from db to avoid waiting for server response.
|
||||
*/
|
||||
suspend fun loadMore(count: Int, direction: Timeline.Direction, fetchOnServerIfNeeded: Boolean = true): LoadMoreResult {
|
||||
if (direction == Timeline.Direction.FORWARDS && nextChunk != null) {
|
||||
return nextChunk?.loadMore(count, direction, fetchOnServerIfNeeded) ?: LoadMoreResult.FAILURE
|
||||
} else if (direction == Timeline.Direction.BACKWARDS && prevChunk != null) {
|
||||
return prevChunk?.loadMore(count, direction, fetchOnServerIfNeeded) ?: LoadMoreResult.FAILURE
|
||||
}
|
||||
val loadFromStorageCount = loadFromStorage(count, direction)
|
||||
Timber.v("Has loaded $loadFromStorageCount items from storage in $direction")
|
||||
val offsetCount = count - loadFromStorageCount
|
||||
return if (direction == Timeline.Direction.FORWARDS && isLastForward.get()) {
|
||||
LoadMoreResult.REACHED_END
|
||||
} else if (direction == Timeline.Direction.BACKWARDS && isLastBackward.get()) {
|
||||
LoadMoreResult.REACHED_END
|
||||
} else if (offsetCount == 0) {
|
||||
LoadMoreResult.SUCCESS
|
||||
} else {
|
||||
delegateLoadMore(fetchOnServerIfNeeded, offsetCount, direction)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun delegateLoadMore(fetchFromServerIfNeeded: Boolean, offsetCount: Int, direction: Timeline.Direction): LoadMoreResult {
|
||||
return if (direction == Timeline.Direction.FORWARDS) {
|
||||
val nextChunkEntity = chunkEntity.nextChunk
|
||||
when {
|
||||
nextChunkEntity != null -> {
|
||||
if (nextChunk == null) {
|
||||
nextChunk = createTimelineChunk(nextChunkEntity)
|
||||
}
|
||||
nextChunk?.loadMore(offsetCount, direction, fetchFromServerIfNeeded) ?: LoadMoreResult.FAILURE
|
||||
}
|
||||
fetchFromServerIfNeeded -> {
|
||||
fetchFromServer(offsetCount, chunkEntity.nextToken, direction)
|
||||
}
|
||||
else -> {
|
||||
LoadMoreResult.SUCCESS
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val prevChunkEntity = chunkEntity.prevChunk
|
||||
when {
|
||||
prevChunkEntity != null -> {
|
||||
if (prevChunk == null) {
|
||||
prevChunk = createTimelineChunk(prevChunkEntity)
|
||||
}
|
||||
prevChunk?.loadMore(offsetCount, direction, fetchFromServerIfNeeded) ?: LoadMoreResult.FAILURE
|
||||
}
|
||||
fetchFromServerIfNeeded -> {
|
||||
fetchFromServer(offsetCount, chunkEntity.prevToken, direction)
|
||||
}
|
||||
else -> {
|
||||
LoadMoreResult.SUCCESS
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getBuiltEventIndex(eventId: String, searchInNext: Boolean, searchInPrev: Boolean): Int? {
|
||||
val builtEventIndex = builtEventsIndexes[eventId]
|
||||
if (builtEventIndex != null) {
|
||||
return getOffsetIndex() + builtEventIndex
|
||||
}
|
||||
if (searchInNext) {
|
||||
val nextBuiltEventIndex = nextChunk?.getBuiltEventIndex(eventId, searchInNext = true, searchInPrev = false)
|
||||
if (nextBuiltEventIndex != null) {
|
||||
return nextBuiltEventIndex
|
||||
}
|
||||
}
|
||||
if (searchInPrev) {
|
||||
val prevBuiltEventIndex = prevChunk?.getBuiltEventIndex(eventId, searchInNext = false, searchInPrev = true)
|
||||
if (prevBuiltEventIndex != null) {
|
||||
return prevBuiltEventIndex
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getBuiltEvent(eventId: String, searchInNext: Boolean, searchInPrev: Boolean): TimelineEvent? {
|
||||
val builtEventIndex = builtEventsIndexes[eventId]
|
||||
if (builtEventIndex != null) {
|
||||
return builtEvents.getOrNull(builtEventIndex)
|
||||
}
|
||||
if (searchInNext) {
|
||||
val nextBuiltEvent = nextChunk?.getBuiltEvent(eventId, searchInNext = true, searchInPrev = false)
|
||||
if (nextBuiltEvent != null) {
|
||||
return nextBuiltEvent
|
||||
}
|
||||
}
|
||||
if (searchInPrev) {
|
||||
val prevBuiltEvent = prevChunk?.getBuiltEvent(eventId, searchInNext = false, searchInPrev = true)
|
||||
if (prevBuiltEvent != null) {
|
||||
return prevBuiltEvent
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?, searchInNext: Boolean, searchInPrev: Boolean): Boolean {
|
||||
return tryOrNull {
|
||||
val builtIndex = getBuiltEventIndex(eventId, searchInNext = false, searchInPrev = false)
|
||||
if (builtIndex == null) {
|
||||
val foundInPrev = searchInPrev && prevChunk?.rebuildEvent(eventId, builder, searchInNext = false, searchInPrev = true).orFalse()
|
||||
if (foundInPrev) {
|
||||
return true
|
||||
}
|
||||
if (searchInNext) {
|
||||
return prevChunk?.rebuildEvent(eventId, builder, searchInPrev = false, searchInNext = true).orFalse()
|
||||
}
|
||||
return false
|
||||
}
|
||||
// Update the relation of existing event
|
||||
builtEvents.getOrNull(builtIndex)?.let { te ->
|
||||
val rebuiltEvent = builder(te)
|
||||
builtEvents[builtIndex] = rebuiltEvent!!
|
||||
true
|
||||
}
|
||||
}
|
||||
?: false
|
||||
}
|
||||
|
||||
fun close(closeNext: Boolean, closePrev: Boolean) {
|
||||
if (closeNext) {
|
||||
nextChunk?.close(closeNext = true, closePrev = false)
|
||||
}
|
||||
if (closePrev) {
|
||||
prevChunk?.close(closeNext = false, closePrev = true)
|
||||
}
|
||||
nextChunk = null
|
||||
nextChunkLatch?.cancel()
|
||||
prevChunk = null
|
||||
prevChunkLatch?.cancel()
|
||||
chunkEntity.removeChangeListener(chunkObjectListener)
|
||||
timelineEventEntities.removeChangeListener(timelineEventsChangeListener)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method tries to read events from the current chunk.
|
||||
*/
|
||||
private suspend fun loadFromStorage(count: Int, direction: Timeline.Direction): Int {
|
||||
val displayIndex = getNextDisplayIndex(direction) ?: return 0
|
||||
val baseQuery = timelineEventEntities.where()
|
||||
val timelineEvents = baseQuery.offsets(direction, count, displayIndex).findAll().orEmpty()
|
||||
if (timelineEvents.isEmpty()) return 0
|
||||
fetchRootThreadEventsIfNeeded(timelineEvents)
|
||||
if (direction == Timeline.Direction.FORWARDS) {
|
||||
builtEventsIndexes.entries.forEach { it.setValue(it.value + timelineEvents.size) }
|
||||
}
|
||||
timelineEvents
|
||||
.mapIndexed { index, timelineEventEntity ->
|
||||
val timelineEvent = timelineEventEntity.buildAndDecryptIfNeeded()
|
||||
if (timelineEvent.root.type == EventType.STATE_ROOM_CREATE) {
|
||||
isLastBackward.set(true)
|
||||
}
|
||||
if (direction == Timeline.Direction.FORWARDS) {
|
||||
builtEventsIndexes[timelineEvent.eventId] = index
|
||||
builtEvents.add(index, timelineEvent)
|
||||
} else {
|
||||
builtEventsIndexes[timelineEvent.eventId] = builtEvents.size
|
||||
builtEvents.add(timelineEvent)
|
||||
}
|
||||
}
|
||||
return timelineEvents.size
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is responsible to fetch and store the root event of a thread event
|
||||
* in order to be able to display the event to the user appropriately
|
||||
*/
|
||||
private suspend fun fetchRootThreadEventsIfNeeded(offsetResults: List<TimelineEventEntity>) {
|
||||
val eventEntityList = offsetResults
|
||||
.mapNotNull {
|
||||
it.root
|
||||
}.map {
|
||||
EventMapper.map(it)
|
||||
}
|
||||
threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(eventEntityList)
|
||||
}
|
||||
|
||||
private fun TimelineEventEntity.buildAndDecryptIfNeeded(): TimelineEvent {
|
||||
val timelineEvent = buildTimelineEvent(this)
|
||||
val transactionId = timelineEvent.root.unsignedData?.transactionId
|
||||
uiEchoManager?.onSyncedEvent(transactionId)
|
||||
if (timelineEvent.isEncrypted() &&
|
||||
timelineEvent.root.mxDecryptionResult == null) {
|
||||
timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineId)) }
|
||||
}
|
||||
return timelineEvent
|
||||
}
|
||||
|
||||
private fun buildTimelineEvent(eventEntity: TimelineEventEntity) = timelineEventMapper.map(
|
||||
timelineEventEntity = eventEntity,
|
||||
buildReadReceipts = timelineSettings.buildReadReceipts
|
||||
).let {
|
||||
// eventually enhance with ui echo?
|
||||
(uiEchoManager?.decorateEventWithReactionUiEcho(it) ?: it)
|
||||
}
|
||||
|
||||
/**
|
||||
* Will try to fetch a new chunk on the home server.
|
||||
* It will take care to update the database by inserting new events and linking new chunk
|
||||
* with this one.
|
||||
*/
|
||||
private suspend fun fetchFromServer(count: Int, token: String?, direction: Timeline.Direction): LoadMoreResult {
|
||||
val latch = if (direction == Timeline.Direction.FORWARDS) {
|
||||
nextChunkLatch = CompletableDeferred()
|
||||
nextChunkLatch
|
||||
} else {
|
||||
prevChunkLatch = CompletableDeferred()
|
||||
prevChunkLatch
|
||||
}
|
||||
val loadMoreResult = try {
|
||||
if (token == null) {
|
||||
if (direction == Timeline.Direction.BACKWARDS || !chunkEntity.hasBeenALastForwardChunk()) return LoadMoreResult.REACHED_END
|
||||
val lastKnownEventId = chunkEntity.sortedTimelineEvents().firstOrNull()?.eventId ?: return LoadMoreResult.FAILURE
|
||||
val taskParams = FetchTokenAndPaginateTask.Params(roomId, lastKnownEventId, direction.toPaginationDirection(), count)
|
||||
fetchTokenAndPaginateTask.execute(taskParams).toLoadMoreResult()
|
||||
} else {
|
||||
Timber.v("Fetch $count more events on server")
|
||||
val taskParams = PaginationTask.Params(roomId, token, direction.toPaginationDirection(), count)
|
||||
paginationTask.execute(taskParams).toLoadMoreResult()
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e("Failed to fetch from server: $failure", failure)
|
||||
LoadMoreResult.FAILURE
|
||||
}
|
||||
return if (loadMoreResult == LoadMoreResult.SUCCESS) {
|
||||
latch?.await()
|
||||
loadMore(count, direction, fetchOnServerIfNeeded = false)
|
||||
} else {
|
||||
loadMoreResult
|
||||
}
|
||||
}
|
||||
|
||||
private fun TokenChunkEventPersistor.Result.toLoadMoreResult(): LoadMoreResult {
|
||||
return when (this) {
|
||||
TokenChunkEventPersistor.Result.REACHED_END -> LoadMoreResult.REACHED_END
|
||||
TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE,
|
||||
TokenChunkEventPersistor.Result.SUCCESS -> LoadMoreResult.SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
private fun getOffsetIndex(): Int {
|
||||
var offset = 0
|
||||
var currentNextChunk = nextChunk
|
||||
while (currentNextChunk != null) {
|
||||
offset += currentNextChunk.builtEvents.size
|
||||
currentNextChunk = currentNextChunk.nextChunk
|
||||
}
|
||||
return offset
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is responsible for managing insertions and updates of events on this chunk.
|
||||
*
|
||||
*/
|
||||
private fun handleDatabaseChangeSet(frozenResults: RealmResults<TimelineEventEntity>, changeSet: OrderedCollectionChangeSet) {
|
||||
val insertions = changeSet.insertionRanges
|
||||
for (range in insertions) {
|
||||
val newItems = frozenResults
|
||||
.subList(range.startIndex, range.startIndex + range.length)
|
||||
.map { it.buildAndDecryptIfNeeded() }
|
||||
builtEventsIndexes.entries.filter { it.value >= range.startIndex }.forEach { it.setValue(it.value + range.length) }
|
||||
newItems.mapIndexed { index, timelineEvent ->
|
||||
if (timelineEvent.root.type == EventType.STATE_ROOM_CREATE) {
|
||||
isLastBackward.set(true)
|
||||
}
|
||||
val correctedIndex = range.startIndex + index
|
||||
builtEvents.add(correctedIndex, timelineEvent)
|
||||
builtEventsIndexes[timelineEvent.eventId] = correctedIndex
|
||||
}
|
||||
}
|
||||
val modifications = changeSet.changeRanges
|
||||
for (range in modifications) {
|
||||
for (modificationIndex in (range.startIndex until range.startIndex + range.length)) {
|
||||
val updatedEntity = frozenResults[modificationIndex] ?: continue
|
||||
try {
|
||||
builtEvents[modificationIndex] = updatedEntity.buildAndDecryptIfNeeded()
|
||||
} catch (failure: Throwable) {
|
||||
Timber.v("Fail to update items at index: $modificationIndex")
|
||||
}
|
||||
}
|
||||
}
|
||||
if (insertions.isNotEmpty() || modifications.isNotEmpty()) {
|
||||
onBuiltEvents(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNextDisplayIndex(direction: Timeline.Direction): Int? {
|
||||
val frozenTimelineEvents = timelineEventEntities.freeze()
|
||||
if (frozenTimelineEvents.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
return if (builtEvents.isEmpty()) {
|
||||
if (initialEventId != null) {
|
||||
frozenTimelineEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, initialEventId).findFirst()?.displayIndex
|
||||
} else if (direction == Timeline.Direction.BACKWARDS) {
|
||||
frozenTimelineEvents.first()?.displayIndex
|
||||
} else {
|
||||
frozenTimelineEvents.last()?.displayIndex
|
||||
}
|
||||
} else if (direction == Timeline.Direction.FORWARDS) {
|
||||
builtEvents.first().displayIndex + 1
|
||||
} else {
|
||||
builtEvents.last().displayIndex - 1
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTimelineChunk(chunkEntity: ChunkEntity?): TimelineChunk? {
|
||||
if (chunkEntity == null) return null
|
||||
return TimelineChunk(
|
||||
chunkEntity = chunkEntity,
|
||||
timelineSettings = timelineSettings,
|
||||
roomId = roomId,
|
||||
timelineId = timelineId,
|
||||
eventDecryptor = eventDecryptor,
|
||||
paginationTask = paginationTask,
|
||||
fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
|
||||
timelineEventMapper = timelineEventMapper,
|
||||
uiEchoManager = uiEchoManager,
|
||||
threadsAwarenessHandler = threadsAwarenessHandler,
|
||||
initialEventId = null,
|
||||
onBuiltEvents = this.onBuiltEvents
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun RealmQuery<TimelineEventEntity>.offsets(
|
||||
direction: Timeline.Direction,
|
||||
count: Int,
|
||||
startDisplayIndex: Int
|
||||
): RealmQuery<TimelineEventEntity> {
|
||||
sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
||||
if (direction == Timeline.Direction.BACKWARDS) {
|
||||
lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex)
|
||||
} else {
|
||||
greaterThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex)
|
||||
}
|
||||
return limit(count.toLong())
|
||||
}
|
||||
|
||||
private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
|
||||
return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS
|
||||
}
|
||||
|
||||
private fun ChunkEntity.sortedTimelineEvents(): RealmResults<TimelineEventEntity> {
|
||||
return timelineEvents.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
||||
}
|
|
@ -23,6 +23,9 @@ import javax.inject.Inject
|
|||
|
||||
@SessionScope
|
||||
internal class TimelineInput @Inject constructor() {
|
||||
|
||||
val listeners = mutableSetOf<Listener>()
|
||||
|
||||
fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) {
|
||||
listeners.toSet().forEach { it.onLocalEchoCreated(roomId, timelineEvent) }
|
||||
}
|
||||
|
@ -35,8 +38,6 @@ internal class TimelineInput @Inject constructor() {
|
|||
listeners.toSet().forEach { it.onNewTimelineEvents(roomId, eventIds) }
|
||||
}
|
||||
|
||||
val listeners = mutableSetOf<Listener>()
|
||||
|
||||
internal interface Listener {
|
||||
fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) = Unit
|
||||
fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState) = Unit
|
||||
|
|
|
@ -32,92 +32,24 @@ import org.matrix.android.sdk.internal.database.model.ChunkEntity
|
|||
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.RoomEntity
|
||||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
|
||||
import org.matrix.android.sdk.internal.database.model.deleteOnCascade
|
||||
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
|
||||
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
|
||||
import org.matrix.android.sdk.internal.database.query.create
|
||||
import org.matrix.android.sdk.internal.database.query.find
|
||||
import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents
|
||||
import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
|
||||
import org.matrix.android.sdk.internal.database.query.getOrCreate
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.di.UserId
|
||||
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryEventsHelper
|
||||
import org.matrix.android.sdk.internal.util.awaitTransaction
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Insert Chunk in DB, and eventually merge with existing chunk event
|
||||
* Insert Chunk in DB, and eventually link next and previous chunk in db.
|
||||
*/
|
||||
internal class TokenChunkEventPersistor @Inject constructor(
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
@UserId private val userId: String) {
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* ========================================================================================================
|
||||
* | Backward case |
|
||||
* ========================================================================================================
|
||||
*
|
||||
* *--------------------------* *--------------------------*
|
||||
* | startToken1 | | startToken1 |
|
||||
* *--------------------------* *--------------------------*
|
||||
* | | | |
|
||||
* | | | |
|
||||
* | receivedChunk backward | | |
|
||||
* | Events | | |
|
||||
* | | | |
|
||||
* | | | |
|
||||
* | | | |
|
||||
* *--------------------------* *--------------------------* | |
|
||||
* | startToken0 | | endToken1 | => | Merged chunk |
|
||||
* *--------------------------* *--------------------------* | Events |
|
||||
* | | | |
|
||||
* | | | |
|
||||
* | Current Chunk | | |
|
||||
* | Events | | |
|
||||
* | | | |
|
||||
* | | | |
|
||||
* | | | |
|
||||
* *--------------------------* *--------------------------*
|
||||
* | endToken0 | | endToken0 |
|
||||
* *--------------------------* *--------------------------*
|
||||
*
|
||||
*
|
||||
* ========================================================================================================
|
||||
* | Forward case |
|
||||
* ========================================================================================================
|
||||
*
|
||||
* *--------------------------* *--------------------------*
|
||||
* | startToken0 | | startToken0 |
|
||||
* *--------------------------* *--------------------------*
|
||||
* | | | |
|
||||
* | | | |
|
||||
* | Current Chunk | | |
|
||||
* | Events | | |
|
||||
* | | | |
|
||||
* | | | |
|
||||
* | | | |
|
||||
* *--------------------------* *--------------------------* | |
|
||||
* | endToken0 | | startToken1 | => | Merged chunk |
|
||||
* *--------------------------* *--------------------------* | Events |
|
||||
* | | | |
|
||||
* | | | |
|
||||
* | receivedChunk forward | | |
|
||||
* | Events | | |
|
||||
* | | | |
|
||||
* | | | |
|
||||
* | | | |
|
||||
* *--------------------------* *--------------------------*
|
||||
* | endToken1 | | endToken1 |
|
||||
* *--------------------------* *--------------------------*
|
||||
*
|
||||
* ========================================================================================================
|
||||
* </pre>
|
||||
*/
|
||||
|
||||
enum class Result {
|
||||
SHOULD_FETCH_MORE,
|
||||
REACHED_END,
|
||||
|
@ -141,21 +73,21 @@ internal class TokenChunkEventPersistor @Inject constructor(
|
|||
prevToken = receivedChunk.end
|
||||
}
|
||||
|
||||
val existingChunk = ChunkEntity.find(realm, roomId, prevToken = prevToken, nextToken = nextToken)
|
||||
if (existingChunk != null) {
|
||||
Timber.v("This chunk is already in the db, returns")
|
||||
return@awaitTransaction
|
||||
}
|
||||
val prevChunk = ChunkEntity.find(realm, roomId, nextToken = prevToken)
|
||||
val nextChunk = ChunkEntity.find(realm, roomId, prevToken = nextToken)
|
||||
|
||||
// The current chunk is the one we will keep all along the merge processChanges.
|
||||
// We try to look for a chunk next to the token,
|
||||
// otherwise we create a whole new one which is unlinked (not live)
|
||||
val currentChunk = if (direction == PaginationDirection.FORWARDS) {
|
||||
prevChunk?.apply { this.nextToken = nextToken }
|
||||
} else {
|
||||
nextChunk?.apply { this.prevToken = prevToken }
|
||||
val currentChunk = ChunkEntity.create(realm, prevToken = prevToken, nextToken = nextToken).apply {
|
||||
this.nextChunk = nextChunk
|
||||
this.prevChunk = prevChunk
|
||||
}
|
||||
?: ChunkEntity.create(realm, prevToken, nextToken)
|
||||
|
||||
if (receivedChunk.events.isNullOrEmpty() && !receivedChunk.hasMore()) {
|
||||
handleReachEnd(realm, roomId, direction, currentChunk)
|
||||
nextChunk?.prevChunk = currentChunk
|
||||
prevChunk?.nextChunk = currentChunk
|
||||
if (receivedChunk.events.isEmpty() && !receivedChunk.hasMore()) {
|
||||
handleReachEnd(roomId, direction, currentChunk)
|
||||
} else {
|
||||
handlePagination(realm, roomId, direction, receivedChunk, currentChunk)
|
||||
}
|
||||
|
@ -172,17 +104,10 @@ internal class TokenChunkEventPersistor @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleReachEnd(realm: Realm, roomId: String, direction: PaginationDirection, currentChunk: ChunkEntity) {
|
||||
private fun handleReachEnd(roomId: String, direction: PaginationDirection, currentChunk: ChunkEntity) {
|
||||
Timber.v("Reach end of $roomId")
|
||||
if (direction == PaginationDirection.FORWARDS) {
|
||||
val currentLastForwardChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)
|
||||
if (currentChunk != currentLastForwardChunk) {
|
||||
currentChunk.isLastForward = true
|
||||
currentLastForwardChunk?.deleteOnCascade(deleteStateEvents = false, canDeleteRoot = false)
|
||||
RoomSummaryEntity.where(realm, roomId).findFirst()?.apply {
|
||||
latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
|
||||
}
|
||||
}
|
||||
Timber.v("We should keep the lastForward chunk unique, the one from sync")
|
||||
} else {
|
||||
currentChunk.isLastBackward = true
|
||||
}
|
||||
|
@ -210,60 +135,62 @@ internal class TokenChunkEventPersistor @Inject constructor(
|
|||
roomMemberContentsByUser[stateEvent.stateKey] = stateEvent.content.toModel<RoomMemberContent>()
|
||||
}
|
||||
}
|
||||
val eventIds = ArrayList<String>(eventList.size)
|
||||
|
||||
val optimizedThreadSummaryMap = hashMapOf<String, EventEntity>()
|
||||
eventList.forEach { event ->
|
||||
if (event.eventId == null || event.senderId == null) {
|
||||
return@forEach
|
||||
}
|
||||
val ageLocalTs = event.unsignedData?.age?.let { now - it }
|
||||
eventIds.add(event.eventId)
|
||||
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
|
||||
if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null) {
|
||||
val contentToUse = if (direction == PaginationDirection.BACKWARDS) {
|
||||
event.prevContent
|
||||
} else {
|
||||
event.content
|
||||
run processTimelineEvents@{
|
||||
eventList.forEach { event ->
|
||||
if (event.eventId == null || event.senderId == null) {
|
||||
return@forEach
|
||||
}
|
||||
// We check for the timeline event with this id
|
||||
val eventId = event.eventId
|
||||
val existingTimelineEvent = TimelineEventEntity.where(realm, roomId, eventId).findFirst()
|
||||
// If it exists, we want to stop here, just link the prevChunk
|
||||
val existingChunk = existingTimelineEvent?.chunk?.firstOrNull()
|
||||
if (existingChunk != null) {
|
||||
when (direction) {
|
||||
PaginationDirection.BACKWARDS -> {
|
||||
if (currentChunk.nextChunk == existingChunk) {
|
||||
Timber.w("Avoid double link, shouldn't happen in an ideal world")
|
||||
} else {
|
||||
currentChunk.prevChunk = existingChunk
|
||||
existingChunk.nextChunk = currentChunk
|
||||
}
|
||||
}
|
||||
PaginationDirection.FORWARDS -> {
|
||||
if (currentChunk.prevChunk == existingChunk) {
|
||||
Timber.w("Avoid double link, shouldn't happen in an ideal world")
|
||||
} else {
|
||||
currentChunk.nextChunk = existingChunk
|
||||
existingChunk.prevChunk = currentChunk
|
||||
}
|
||||
}
|
||||
}
|
||||
// Stop processing here
|
||||
return@processTimelineEvents
|
||||
}
|
||||
val ageLocalTs = event.unsignedData?.age?.let { now - it }
|
||||
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
|
||||
if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null) {
|
||||
val contentToUse = if (direction == PaginationDirection.BACKWARDS) {
|
||||
event.prevContent
|
||||
} else {
|
||||
event.content
|
||||
}
|
||||
roomMemberContentsByUser[event.stateKey] = contentToUse.toModel<RoomMemberContent>()
|
||||
}
|
||||
currentChunk.addTimelineEvent(roomId, eventEntity, direction, 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
|
||||
}
|
||||
roomMemberContentsByUser[event.stateKey] = contentToUse.toModel<RoomMemberContent>()
|
||||
}
|
||||
|
||||
currentChunk.addTimelineEvent(roomId, eventEntity, direction, 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
|
||||
}
|
||||
}
|
||||
|
||||
// Find all the chunks which contain at least one event from the list of eventIds
|
||||
val chunks = ChunkEntity.findAllIncludingEvents(realm, eventIds)
|
||||
Timber.d("Found ${chunks.size} chunks containing at least one of the eventIds")
|
||||
val chunksToDelete = ArrayList<ChunkEntity>()
|
||||
chunks.forEach {
|
||||
if (it != currentChunk) {
|
||||
Timber.d("Merge $it")
|
||||
currentChunk.merge(roomId, it, direction)
|
||||
chunksToDelete.add(it)
|
||||
}
|
||||
}
|
||||
chunksToDelete.forEach {
|
||||
it.deleteOnCascade(deleteStateEvents = false, canDeleteRoot = false)
|
||||
}
|
||||
val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
|
||||
val shouldUpdateSummary = roomSummaryEntity.latestPreviewableEvent == null ||
|
||||
(chunksToDelete.isNotEmpty() && currentChunk.isLastForward && direction == PaginationDirection.FORWARDS)
|
||||
if (shouldUpdateSummary) {
|
||||
roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
|
||||
}
|
||||
if (currentChunk.isValid) {
|
||||
RoomEntity.where(realm, roomId).findFirst()?.addIfNecessary(currentChunk)
|
||||
}
|
||||
|
||||
optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(roomId = roomId, realm = realm, currentUserId = userId)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,14 +24,10 @@ import org.matrix.android.sdk.api.session.room.model.ReactionAggregatedSummary
|
|||
import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||
import timber.log.Timber
|
||||
import java.util.Collections
|
||||
|
||||
internal class UIEchoManager(
|
||||
private val settings: TimelineSettings,
|
||||
private val listener: Listener
|
||||
) {
|
||||
internal class UIEchoManager(private val listener: Listener) {
|
||||
|
||||
interface Listener {
|
||||
fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?): Boolean
|
||||
|
@ -70,13 +66,12 @@ internal class UIEchoManager(
|
|||
return existingState != sendState
|
||||
}
|
||||
|
||||
fun onLocalEchoCreated(timelineEvent: TimelineEvent) {
|
||||
// Manage some ui echos (do it before filter because actual event could be filtered out)
|
||||
fun onLocalEchoCreated(timelineEvent: TimelineEvent): Boolean {
|
||||
when (timelineEvent.root.getClearType()) {
|
||||
EventType.REDACTION -> {
|
||||
}
|
||||
EventType.REACTION -> {
|
||||
val content = timelineEvent.root.content?.toModel<ReactionContent>()
|
||||
val content: ReactionContent? = timelineEvent.root.content?.toModel<ReactionContent>()
|
||||
if (RelationType.ANNOTATION == content?.relatesTo?.type) {
|
||||
val reaction = content.relatesTo.key
|
||||
val relatedEventID = content.relatesTo.eventId
|
||||
|
@ -96,11 +91,12 @@ internal class UIEchoManager(
|
|||
}
|
||||
Timber.v("On local echo created: ${timelineEvent.eventId}")
|
||||
inMemorySendingEvents.add(0, timelineEvent)
|
||||
return true
|
||||
}
|
||||
|
||||
fun decorateEventWithReactionUiEcho(timelineEvent: TimelineEvent): TimelineEvent? {
|
||||
fun decorateEventWithReactionUiEcho(timelineEvent: TimelineEvent): TimelineEvent {
|
||||
val relatedEventID = timelineEvent.eventId
|
||||
val contents = inMemoryReactions[relatedEventID] ?: return null
|
||||
val contents = inMemoryReactions[relatedEventID] ?: return timelineEvent
|
||||
|
||||
var existingAnnotationSummary = timelineEvent.annotations ?: EventAnnotationsSummary(
|
||||
relatedEventID
|
||||
|
|
|
@ -348,18 +348,17 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||
syncLocalTimestampMillis: Long,
|
||||
aggregator: SyncResponsePostTreatmentAggregator): ChunkEntity {
|
||||
val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId)
|
||||
|
||||
if (isLimited && lastChunk != null) {
|
||||
lastChunk.deleteOnCascade(deleteStateEvents = true, canDeleteRoot = true)
|
||||
}
|
||||
val chunkEntity = if (!isLimited && lastChunk != null) {
|
||||
// There are no more events to fetch
|
||||
lastChunk
|
||||
} else {
|
||||
realm.createObject<ChunkEntity>().apply { this.prevToken = prevToken }
|
||||
realm.createObject<ChunkEntity>().apply {
|
||||
this.prevToken = prevToken
|
||||
this.isLastForward = true
|
||||
}
|
||||
}
|
||||
|
||||
// Only one chunk has isLastForward set to true
|
||||
lastChunk?.isLastForward = false
|
||||
chunkEntity.isLastForward = true
|
||||
|
||||
val eventIds = ArrayList<String>(eventList.size)
|
||||
val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ import org.matrix.android.sdk.internal.session.sync.model.accountdata.AcceptedTe
|
|||
import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask
|
||||
import org.matrix.android.sdk.internal.session.user.accountdata.UserAccountDataDataSource
|
||||
import org.matrix.android.sdk.internal.util.ensureTrailingSlash
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultTermsService @Inject constructor(
|
||||
|
@ -63,19 +64,28 @@ internal class DefaultTermsService @Inject constructor(
|
|||
*/
|
||||
override suspend fun getHomeserverTerms(baseUrl: String): TermsResponse {
|
||||
return try {
|
||||
val request = baseUrl + NetworkConstants.URI_API_PREFIX_PATH_R0 + "register"
|
||||
executeRequest(null) {
|
||||
termsAPI.register(baseUrl + NetworkConstants.URI_API_PREFIX_PATH_R0 + "register")
|
||||
termsAPI.register(request)
|
||||
}
|
||||
// Return empty result if it succeed, but it should never happen
|
||||
Timber.w("Request $request succeeded, it should never happen")
|
||||
TermsResponse()
|
||||
} catch (throwable: Throwable) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
TermsResponse(
|
||||
policies = (throwable.toRegistrationFlowResponse()
|
||||
?.params
|
||||
?.get(LoginFlowTypes.TERMS) as? JsonDict)
|
||||
?.get("policies") as? JsonDict
|
||||
)
|
||||
val registrationFlowResponse = throwable.toRegistrationFlowResponse()
|
||||
if (registrationFlowResponse != null) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
TermsResponse(
|
||||
policies = (registrationFlowResponse
|
||||
.params
|
||||
?.get(LoginFlowTypes.TERMS) as? JsonDict)
|
||||
?.get("policies") as? JsonDict
|
||||
)
|
||||
} else {
|
||||
// Other error
|
||||
Timber.e(throwable, "Error while getting homeserver terms")
|
||||
throw throwable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -160,7 +160,7 @@ Formatter\.formatShortFileSize===1
|
|||
# android\.text\.TextUtils
|
||||
|
||||
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt
|
||||
enum class===117
|
||||
enum class===121
|
||||
|
||||
### Do not import temporary legacy classes
|
||||
import org.matrix.android.sdk.internal.legacy.riot===3
|
||||
|
|
|
@ -140,7 +140,7 @@ android {
|
|||
buildConfigField "String", "BUILD_NUMBER", "\"${buildNumber}\""
|
||||
resValue "string", "build_number", "\"${buildNumber}\""
|
||||
|
||||
buildConfigField "im.vector.app.features.VectorFeatures.LoginVariant", "LOGIN_VARIANT", "im.vector.app.features.VectorFeatures.LoginVariant.LEGACY"
|
||||
buildConfigField "im.vector.app.features.VectorFeatures.OnboardingVariant", "ONBOARDING_VARIANT", "im.vector.app.features.VectorFeatures.OnboardingVariant.FTUE_AUTH"
|
||||
|
||||
buildConfigField "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy", "outboundSessionKeySharingStrategy", "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy.WhenTyping"
|
||||
|
||||
|
@ -392,6 +392,8 @@ dependencies {
|
|||
implementation libs.google.material
|
||||
implementation 'me.gujun.android:span:1.7'
|
||||
implementation libs.markwon.core
|
||||
implementation libs.markwon.extLatex
|
||||
implementation libs.markwon.inlineParser
|
||||
implementation libs.markwon.html
|
||||
implementation 'com.googlecode.htmlcompressor:htmlcompressor:1.5.2'
|
||||
implementation 'me.saket:better-link-movement-method:2.2.0'
|
||||
|
@ -455,7 +457,7 @@ dependencies {
|
|||
// OSS License, gplay flavor only
|
||||
gplayImplementation 'com.google.android.gms:play-services-oss-licenses:17.0.0'
|
||||
|
||||
implementation "androidx.emoji2:emoji2:1.0.0"
|
||||
implementation "androidx.emoji2:emoji2:1.0.1"
|
||||
implementation('com.github.BillCarsonFr:JsonViewer:0.7')
|
||||
|
||||
// WebRTC
|
||||
|
|
|
@ -72,6 +72,7 @@
|
|||
|
||||
<!-- Wording -->
|
||||
<issue id="Typos" severity="error" />
|
||||
<issue id="TypographyDashes" severity="error" />
|
||||
|
||||
<!-- Ignore lint issue in generated resource file from templates.
|
||||
https://github.com/LikeTheSalad/android-string-reference generates string from the default language
|
||||
|
|
|
@ -108,7 +108,6 @@ class SpanUtilsTest : InstrumentedTest {
|
|||
val string = SpannableString("Text")
|
||||
val result = spanUtils.getBindingOptions(string)
|
||||
result.canUseTextFuture shouldBeEqualTo true
|
||||
result.preventMutation shouldBeEqualTo false
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -117,7 +116,6 @@ class SpanUtilsTest : InstrumentedTest {
|
|||
string.setSpan(StrikethroughSpan(), 10, 23, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
val result = spanUtils.getBindingOptions(string)
|
||||
result.canUseTextFuture shouldBeEqualTo false
|
||||
result.preventMutation shouldBeEqualTo false
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -125,7 +123,6 @@ class SpanUtilsTest : InstrumentedTest {
|
|||
val string = SpannableString("Emoji \uD83D\uDE2E\u200D\uD83D\uDCA8")
|
||||
val result = spanUtils.getBindingOptions(string)
|
||||
result.canUseTextFuture shouldBeEqualTo false
|
||||
result.preventMutation shouldBeEqualTo true
|
||||
}
|
||||
|
||||
private fun trueIfAlwaysAllowed() = Build.VERSION.SDK_INT < Build.VERSION_CODES.P
|
||||
|
|
|
@ -134,10 +134,10 @@ class ElementRobot {
|
|||
activity.runOnUiThread { popup.performClick() }
|
||||
|
||||
waitUntilViewVisible(withId(R.id.bottomSheetFragmentContainer))
|
||||
waitUntilViewVisible(ViewMatchers.withText(R.string.skip))
|
||||
clickOn(R.string.skip)
|
||||
waitUntilViewVisible(ViewMatchers.withText(R.string.action_skip))
|
||||
clickOn(R.string.action_skip)
|
||||
assertDisplayed(R.string.are_you_sure)
|
||||
clickOn(R.string.skip)
|
||||
clickOn(R.string.action_skip)
|
||||
waitUntilViewVisible(withId(R.id.bottomSheetFragmentContainer))
|
||||
}.onFailure { Timber.w("Verification popup missing", it) }
|
||||
}
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.debug.features
|
||||
|
||||
import android.view.View
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||
|
||||
@EpoxyModelClass(layout = im.vector.app.R.layout.item_feature)
|
||||
abstract class BooleanFeatureItem : VectorEpoxyModel<BooleanFeatureItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var feature: Feature.BooleanFeature
|
||||
|
||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
||||
var listener: Listener? = null
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
holder.label.text = feature.label
|
||||
|
||||
holder.optionsSpinner.apply {
|
||||
val arrayAdapter = ArrayAdapter<String>(context, android.R.layout.simple_spinner_dropdown_item)
|
||||
val options = listOf(
|
||||
"DEFAULT - ${feature.featureDefault.toEmoji()}",
|
||||
"✅",
|
||||
"❌"
|
||||
)
|
||||
arrayAdapter.addAll(options)
|
||||
adapter = arrayAdapter
|
||||
|
||||
feature.featureOverride?.let {
|
||||
setSelection(options.indexOf(it.toEmoji()), false)
|
||||
}
|
||||
|
||||
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||
when (position) {
|
||||
0 -> listener?.onBooleanOptionSelected(option = null, feature)
|
||||
else -> listener?.onBooleanOptionSelected(options[position].fromEmoji(), feature)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
val label by bind<TextView>(im.vector.app.R.id.feature_label)
|
||||
val optionsSpinner by bind<Spinner>(im.vector.app.R.id.feature_options)
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun onBooleanOptionSelected(option: Boolean?, feature: Feature.BooleanFeature)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Boolean.toEmoji() = if (this) "✅" else "❌"
|
||||
private fun String.fromEmoji() = when (this) {
|
||||
"✅" -> true
|
||||
"❌" -> false
|
||||
else -> error("unexpected input $this")
|
||||
}
|
|
@ -35,10 +35,14 @@ class DebugFeaturesSettingsActivity : VectorBaseActivity<FragmentGenericRecycler
|
|||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
controller.listener = object : EnumFeatureItem.Listener {
|
||||
override fun <T : Enum<T>> onOptionSelected(option: T?, feature: Feature.EnumFeature<T>) {
|
||||
controller.listener = object : FeaturesController.Listener {
|
||||
override fun <T : Enum<T>> onEnumOptionSelected(option: T?, feature: Feature.EnumFeature<T>) {
|
||||
debugFeatures.overrideEnum(option, feature.type)
|
||||
}
|
||||
|
||||
override fun onBooleanOptionSelected(option: Boolean?, feature: Feature.BooleanFeature) {
|
||||
debugFeatures.override(option, feature.key)
|
||||
}
|
||||
}
|
||||
views.genericRecyclerView.configureWith(controller)
|
||||
controller.setData(debugFeaturesStateFactory.create())
|
||||
|
|
|
@ -27,18 +27,27 @@ class DebugFeaturesStateFactory @Inject constructor(
|
|||
fun create(): FeaturesState {
|
||||
return FeaturesState(listOf(
|
||||
createEnumFeature(
|
||||
label = "Login version",
|
||||
selection = debugFeatures.loginVariant(),
|
||||
default = defaultFeatures.loginVariant()
|
||||
label = "Onboarding variant",
|
||||
featureOverride = debugFeatures.onboardingVariant(),
|
||||
featureDefault = defaultFeatures.onboardingVariant()
|
||||
),
|
||||
|
||||
Feature.BooleanFeature(
|
||||
label = "FTUE Splash - I already have an account",
|
||||
featureOverride = debugFeatures.isAlreadyHaveAccountSplashEnabled().takeIf {
|
||||
debugFeatures.hasOverride(DebugFeatureKeys.alreadyHaveAnAccount)
|
||||
},
|
||||
featureDefault = defaultFeatures.isAlreadyHaveAccountSplashEnabled(),
|
||||
key = DebugFeatureKeys.alreadyHaveAnAccount
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
private inline fun <reified T : Enum<T>> createEnumFeature(label: String, selection: T, default: T): Feature {
|
||||
private inline fun <reified T : Enum<T>> createEnumFeature(label: String, featureOverride: T, featureDefault: T): Feature {
|
||||
return Feature.EnumFeature(
|
||||
label = label,
|
||||
selection = selection.takeIf { debugFeatures.hasEnumOverride(T::class) },
|
||||
default = default,
|
||||
override = featureOverride.takeIf { debugFeatures.hasEnumOverride(T::class) },
|
||||
default = featureDefault,
|
||||
options = enumValues<T>().toList(),
|
||||
type = T::class
|
||||
)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue