Merge branch 'develop' into feature/bma/math_final

This commit is contained in:
Benoit Marty 2022-01-04 16:13:23 +01:00 committed by GitHub
commit 7bbea52e66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
314 changed files with 6789 additions and 5786 deletions

View File

@ -2,13 +2,11 @@ name: Move new issues onto Issue triage board
on: on:
issues: issues:
types: [ opened ] types: [opened]
jobs: jobs:
automate-project-columns: automate-project-columns:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: |
github.repository == 'vector-im/element-android' # Skip in forks
steps: steps:
- uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488 - uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488
with: with:

View File

@ -2,14 +2,12 @@ name: Move labelled issues to correct boards and columns
on: on:
issues: issues:
types: [ labeled ] types: [labeled]
jobs: jobs:
move_needs_info_issues: move_needs_info_issues:
name: X-Needs-Info issues to Need info column on triage board name: X-Needs-Info issues to Need info column on triage board
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: |
github.repository == 'vector-im/element-android' # Skip in forks
steps: steps:
- uses: konradpabjan/move-labeled-or-milestoned-issue@219d384e03fa4b6460cd24f9f37d19eb033a4338 - uses: konradpabjan/move-labeled-or-milestoned-issue@219d384e03fa4b6460cd24f9f37d19eb033a4338
with: with:
@ -21,16 +19,15 @@ jobs:
add_priority_design_issues_to_project: add_priority_design_issues_to_project:
name: P1 X-Needs-Design to Design project board name: P1 X-Needs-Design to Design project board
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: | if: >
github.repository == 'vector-im/element-android' && # Skip in forks contains(github.event.issue.labels.*.name, 'X-Needs-Design') &&
contains(github.event.issue.labels.*.name, 'X-Needs-Design') && (contains(github.event.issue.labels.*.name, 'S-Critical') &&
(contains(github.event.issue.labels.*.name, 'S-Critical') && (contains(github.event.issue.labels.*.name, 'O-Frequent') ||
(contains(github.event.issue.labels.*.name, 'O-Frequent') ||
contains(github.event.issue.labels.*.name, 'O-Occasional')) || contains(github.event.issue.labels.*.name, 'O-Occasional')) ||
contains(github.event.issue.labels.*.name, 'S-Major') && contains(github.event.issue.labels.*.name, 'S-Major') &&
contains(github.event.issue.labels.*.name, 'O-Frequent') || contains(github.event.issue.labels.*.name, 'O-Frequent') ||
contains(github.event.issue.labels.*.name, 'A11y') && contains(github.event.issue.labels.*.name, 'A11y') &&
contains(github.event.issue.labels.*.name, 'O-Frequent')) contains(github.event.issue.labels.*.name, 'O-Frequent'))
steps: steps:
- uses: octokit/graphql-action@v2.x - uses: octokit/graphql-action@v2.x
id: add_to_project id: add_to_project
@ -50,38 +47,36 @@ jobs:
PROJECT_ID: "PN_kwDOAM0swc0sUA" PROJECT_ID: "PN_kwDOAM0swc0sUA"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
# delight_issues_to_board: # delight_issues_to_board:
# name: Spaces issues to new Delight project board # name: Spaces issues to new Delight project board
# runs-on: ubuntu-latest # runs-on: ubuntu-latest
# if: | # if: >
# github.repository == 'vector-im/element-android' && # Skip in forks # contains(github.event.issue.labels.*.name, 'A-Spaces') ||
# contains(github.event.issue.labels.*.name, 'A-Spaces') || # contains(github.event.issue.labels.*.name, 'A-Space-Settings') ||
# contains(github.event.issue.labels.*.name, 'A-Space-Settings') || # contains(github.event.issue.labels.*.name, 'A-Subspaces')
# contains(github.event.issue.labels.*.name, 'A-Subspaces') # steps:
# steps: # - uses: octokit/graphql-action@v2.x
# - uses: octokit/graphql-action@v2.x # with:
# with: # headers: '{"GraphQL-Features": "projects_next_graphql"}'
# headers: '{"GraphQL-Features": "projects_next_graphql"}' # query: |
# query: | # mutation add_to_project($projectid:ID!,$contentid:ID!) {
# mutation add_to_project($projectid:ID!,$contentid:ID!) { # addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) {
# addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { # projectNextItem {
# projectNextItem { # id
# id # }
# } # }
# } # }
# } # projectid: ${{ env.PROJECT_ID }}
# projectid: ${{ env.PROJECT_ID }} # contentid: ${{ github.event.issue.node_id }}
# contentid: ${{ github.event.issue.node_id }} # env:
# env: # PROJECT_ID: "PN_kwDOAM0swc1HvQ"
# PROJECT_ID: "PN_kwDOAM0swc1HvQ" # GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
# GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
move_voice-message_issues: move_voice-message_issues:
name: A-Voice Messages to voice message board name: A-Voice Messages to voice message board
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: | if: >
github.repository == 'vector-im/element-android' && # Skip in forks contains(github.event.issue.labels.*.name, 'A-Voice Messages')
contains(github.event.issue.labels.*.name, 'A-Voice Messages')
steps: steps:
- uses: octokit/graphql-action@v2.x - uses: octokit/graphql-action@v2.x
with: with:
@ -103,9 +98,8 @@ jobs:
move_threads_issues: move_threads_issues:
name: A-Threads to Thread board name: A-Threads to Thread board
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: | if: >
github.repository == 'vector-im/element-android' && # Skip in forks contains(github.event.issue.labels.*.name, 'A-Threads')
contains(github.event.issue.labels.*.name, 'A-Threads')
steps: steps:
- uses: octokit/graphql-action@v2.x - uses: octokit/graphql-action@v2.x
with: with:
@ -127,9 +121,8 @@ jobs:
move_message_bubbles_issues: move_message_bubbles_issues:
name: A-Message-Bubbles to Message bubbles board name: A-Message-Bubbles to Message bubbles board
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: | if: >
github.repository == 'vector-im/element-android' && # Skip in forks contains(github.event.issue.labels.*.name, 'A-Message-Bubbles')
contains(github.event.issue.labels.*.name, 'A-Message-Bubbles')
steps: steps:
- uses: octokit/graphql-action@v2.x - uses: octokit/graphql-action@v2.x
with: with:

View File

@ -2,15 +2,15 @@ name: Move unlabelled from needs info columns to triaged
on: on:
issues: issues:
types: [ unlabeled ] types: [unlabeled]
jobs: jobs:
Move_Unabeled_Issue_On_Project_Board: Move_Unabeled_Issue_On_Project_Board:
name: Move no longer X-Needs-Info issues to Triaged name: Move no longer X-Needs-Info issues to Triaged
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: | if: >
github.repository == 'vector-im/element-android' && # Skip in forks ${{
!contains(github.event.issue.labels.*.name, 'X-Needs-Info') !contains(github.event.issue.labels.*.name, 'X-Needs-Info') }}
env: env:
BOARD_NAME: "Issue triage" BOARD_NAME: "Issue triage"
OWNER: ${{ github.repository_owner }} OWNER: ${{ github.repository_owner }}

View File

@ -2,29 +2,28 @@ name: Move P1 bugs to boards
on: on:
issues: issues:
types: [ labeled, unlabeled ] types: [labeled, unlabeled]
jobs: jobs:
p1_issues_to_team_workboard: p1_issues_to_team_workboard:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: | if: >
github.repository == 'vector-im/element-android' && # Skip in forks (!contains(github.event.issue.labels.*.name, 'A-E2EE') &&
(!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-Cross-Signing') && !contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') &&
!contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') && !contains(github.event.issue.labels.*.name, 'A-E2EE-Key-Backup') &&
!contains(github.event.issue.labels.*.name, 'A-E2EE-Key-Backup') && !contains(github.event.issue.labels.*.name, 'A-E2EE-SAS-Verification') &&
!contains(github.event.issue.labels.*.name, 'A-E2EE-SAS-Verification') && !contains(github.event.issue.labels.*.name, 'A-Spaces') &&
!contains(github.event.issue.labels.*.name, 'A-Spaces') && !contains(github.event.issue.labels.*.name, 'A-Spaces-Settings') &&
!contains(github.event.issue.labels.*.name, 'A-Spaces-Settings') && !contains(github.event.issue.labels.*.name, 'A-Subspaces')) &&
!contains(github.event.issue.labels.*.name, 'A-Subspaces')) && (contains(github.event.issue.labels.*.name, 'T-Defect') &&
(contains(github.event.issue.labels.*.name, 'T-Defect') && contains(github.event.issue.labels.*.name, 'S-Critical') &&
contains(github.event.issue.labels.*.name, 'S-Critical') && (contains(github.event.issue.labels.*.name, 'O-Frequent') ||
(contains(github.event.issue.labels.*.name, 'O-Frequent') ||
contains(github.event.issue.labels.*.name, 'O-Occasional')) || contains(github.event.issue.labels.*.name, 'O-Occasional')) ||
contains(github.event.issue.labels.*.name, 'S-Major') && contains(github.event.issue.labels.*.name, 'S-Major') &&
contains(github.event.issue.labels.*.name, 'O-Frequent') || contains(github.event.issue.labels.*.name, 'O-Frequent') ||
contains(github.event.issue.labels.*.name, 'A11y') && contains(github.event.issue.labels.*.name, 'A11y') &&
contains(github.event.issue.labels.*.name, 'O-Frequent')) contains(github.event.issue.labels.*.name, 'O-Frequent'))
steps: steps:
- uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488 - uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488
with: with:
@ -34,21 +33,20 @@ jobs:
P1_issues_to_crypto_team_workboard: P1_issues_to_crypto_team_workboard:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: | if: >
github.repository == 'vector-im/element-android' && # Skip in forks (contains(github.event.issue.labels.*.name, 'A-E2EE') ||
(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-Cross-Signing') || contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') ||
contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') || contains(github.event.issue.labels.*.name, 'A-E2EE-Key-Backup') ||
contains(github.event.issue.labels.*.name, 'A-E2EE-Key-Backup') || contains(github.event.issue.labels.*.name, 'A-E2EE-SAS-Verification')) &&
contains(github.event.issue.labels.*.name, 'A-E2EE-SAS-Verification')) && (contains(github.event.issue.labels.*.name, 'T-Defect') &&
(contains(github.event.issue.labels.*.name, 'T-Defect') && contains(github.event.issue.labels.*.name, 'S-Critical') &&
contains(github.event.issue.labels.*.name, 'S-Critical') && (contains(github.event.issue.labels.*.name, 'O-Frequent') ||
(contains(github.event.issue.labels.*.name, 'O-Frequent') ||
contains(github.event.issue.labels.*.name, 'O-Occasional')) || contains(github.event.issue.labels.*.name, 'O-Occasional')) ||
contains(github.event.issue.labels.*.name, 'S-Major') && contains(github.event.issue.labels.*.name, 'S-Major') &&
contains(github.event.issue.labels.*.name, 'O-Frequent') || contains(github.event.issue.labels.*.name, 'O-Frequent') ||
contains(github.event.issue.labels.*.name, 'A11y') && contains(github.event.issue.labels.*.name, 'A11y') &&
contains(github.event.issue.labels.*.name, 'O-Frequent')) contains(github.event.issue.labels.*.name, 'O-Frequent'))
steps: steps:
- uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488 - uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488
with: with:

1
changelog.d/4405.feature Normal file
View File

@ -0,0 +1 @@
Change internal timeline management.

1
changelog.d/4405.removal Normal file
View File

@ -0,0 +1 @@
Introduce method onStateUpdated on Timeline.Callback

1
changelog.d/4745.misc Normal file
View File

@ -0,0 +1 @@
Open share UI provides by the system when sharing media or text.

1
changelog.d/4749.bugfix Normal file
View 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/4837.bugfix Normal file
View 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
View File

@ -0,0 +1 @@
Translate the error observed when the user is not allowed to join a room

View 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

View 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

View 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

View File

@ -0,0 +1,2 @@
Hauptänderungen: Verbesserungen bei Sprachnachrichten, Bugfixes.
Änderungsliste: https://github.com/vector-im/element-android/releases/tag/v1.3.9

View 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

View File

@ -0,0 +1,2 @@
تغییرات عمده در این نگارش: افزودن پشتیبان از چرک‌نویس‌های صوتی. رفع چندین مشکل!
گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.3.9

View 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

View 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

View 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

View File

@ -1 +1 @@
Perpesanan grup - perpesanan, panggilan suara dan video grup terenkripsi Perpesanan grup perpesanan, panggilan suara dan video grup terenkripsi

View File

@ -1 +1 @@
Element - Perpesanan Aman Element Perpesanan Aman

View 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

View 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

View 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

View 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

View File

@ -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 <b>Funkcie aplikácie Element zahŕňajú:</b>
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 - Pokročilé nástroje na online komunikáciu
3. Chráni vás pred reklamami, zhromažďovaním údajov a uzavretými platformami - Plne šifrované správy umožňujúce bezpečnejšiu firemnú komunikáciu aj pre pracovníkov na diaľku
4. Posilňuje vašu bezpečnosť vďaka E2E šifrovaniu a krížovému podpisovaniu určenému na overovanie ostatných - 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 Element vám dáva kontrolu rôznymi spôsobmi:
2. Hostujte si účet spustením vlastného servera použitím vlastného hardvéru 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.
3. Prihláste sa k účtu na vlastnom servery objednaním služieb na platforme Element Matrix Services 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>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.
<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.

View File

@ -1 +1 @@
Element (kedysi Riot.im) Element - Bezpečný messenger

View 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

View 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

View File

@ -0,0 +1,2 @@
Основні зміни в цій версії: підтримка чернеток голосових повідомлень. Багато виправлень помилок!
Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.3.9

View File

@ -0,0 +1,2 @@
版本的主要变化:增加了对语音信息草稿的支持。许多修正!
完整更新日志https://github.com/vector-im/element-android/releases/tag/v1.3.9

View File

@ -0,0 +1,2 @@
此版本中的主要變動:新增對語音訊息草稿的支援。許多臭蟲修復!
完整的變更紀錄https://github.com/vector-im/element-android/releases/tag/v1.3.9

View File

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

View File

@ -145,36 +145,9 @@ class CommonTestHelper(context: Context) {
* @param nbOfMessages the number of time the message will be sent * @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> { 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)) val timeline = room.createTimeline(null, TimelineSettings(10))
timeline.start() timeline.start()
waitWithLatch(timeout + 1_000L * nbOfMessages) { latch -> val sentEvents = sendTextMessagesBatched(timeline, room, message, nbOfMessages, timeout)
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)
}
timeline.dispose() timeline.dispose()
// Check that all events has been created // Check that all events has been created
assertEquals("Message number do not match $sentEvents", nbOfMessages.toLong(), sentEvents.size.toLong()) assertEquals("Message number do not match $sentEvents", nbOfMessages.toLong(), sentEvents.size.toLong())
@ -182,9 +155,10 @@ class CommonTestHelper(context: Context) {
} }
/** /**
* Will send nb of messages provided by count parameter but waits a bit every 10 messages to avoid gap in sync * Will send nb of messages provided by count parameter but waits every 10 messages to avoid gap in sync
*/ */
private fun sendTextMessagesBatched(room: Room, message: String, count: Int) { private fun sendTextMessagesBatched(timeline: Timeline, room: Room, message: String, count: Int, timeout: Long): List<TimelineEvent> {
val sentEvents = ArrayList<TimelineEvent>(count)
(1 until count + 1) (1 until count + 1)
.map { "$message #$it" } .map { "$message #$it" }
.chunked(10) .chunked(10)
@ -192,8 +166,34 @@ class CommonTestHelper(context: Context) {
batchedMessages.forEach { formattedMessage -> batchedMessages.forEach { formattedMessage ->
room.sendTextMessage(formattedMessage) 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
} }
// PRIVATE METHODS ***************************************************************************** // PRIVATE METHODS *****************************************************************************
@ -332,13 +332,6 @@ class CommonTestHelper(context: Context) {
fun createEventListener(latch: CountDownLatch, predicate: (List<TimelineEvent>) -> Boolean): Timeline.Listener { fun createEventListener(latch: CountDownLatch, predicate: (List<TimelineEvent>) -> Boolean): Timeline.Listener {
return object : 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>) { override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
if (predicate(snapshot)) { if (predicate(snapshot)) {

View File

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

View File

@ -16,6 +16,8 @@
package org.matrix.android.sdk.session.room.timeline 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.shouldBeFalse
import org.amshove.kluent.shouldBeTrue import org.amshove.kluent.shouldBeTrue
import org.junit.FixMethodOrder import org.junit.FixMethodOrder
@ -123,54 +125,29 @@ class TimelineForwardPaginationTest : InstrumentedTest {
// Alice paginates BACKWARD and FORWARD of 50 events each // Alice paginates BACKWARD and FORWARD of 50 events each
// Then she can only navigate FORWARD // Then she can only navigate FORWARD
run { run {
val lock = CountDownLatch(1) val snapshot = runBlocking {
val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot -> aliceTimeline.awaitPaginate(Timeline.Direction.BACKWARDS, 50)
Timber.e("Alice timeline updated: with ${snapshot.size} events:") aliceTimeline.awaitPaginate(Timeline.Direction.FORWARDS, 50)
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
} }
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.FORWARDS).shouldBeTrue()
aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() 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 // Alice paginates once again FORWARD for 50 events
// All the timeline is retrieved, she cannot paginate anymore in both direction // All the timeline is retrieved, she cannot paginate anymore in both direction
run { 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 // Ask for a forward pagination
aliceTimeline.paginate(Timeline.Direction.FORWARDS, 50) val snapshot = runBlocking {
aliceTimeline.awaitPaginate(Timeline.Direction.FORWARDS, 50)
commonTestHelper.await(lock) }
aliceTimeline.removeAllListeners() // 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 // The timeline is fully loaded
aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()

View File

@ -168,10 +168,8 @@ class TimelinePreviousLastForwardTest : InstrumentedTest {
bobTimeline.addListener(eventsListener) 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.restartWithEventId(firstMessageFromAliceId)
bobTimeline.paginate(Timeline.Direction.BACKWARDS, 50)
bobTimeline.paginate(Timeline.Direction.FORWARDS, 50)
commonTestHelper.await(lock) commonTestHelper.await(lock)
bobTimeline.removeAllListeners() bobTimeline.removeAllListeners()

View File

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

View File

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

View File

@ -71,14 +71,10 @@ interface Timeline {
fun paginate(direction: Direction, count: Int) 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 suspend fun awaitPaginate(direction: Direction, count: Int): List<TimelineEvent>
/**
* Returns the number of failed sending events.
*/
fun failedToDeliverEventCount(): Int
/** /**
* Returns the index of a built event or null. * Returns the index of a built event or null.
@ -86,14 +82,14 @@ interface Timeline {
fun getIndexOfEvent(eventId: String?): Int? 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 { interface Listener {
/** /**
@ -101,19 +97,33 @@ interface Timeline {
* The latest event is the first in the list * The latest event is the first in the list
* @param snapshot the most up to date snapshot * @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 * 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 * 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. * This is used to paginate in one or another direction.
*/ */

View File

@ -47,6 +47,10 @@ data class TimelineEvent(
*/ */
val localId: Long, val localId: Long,
val eventId: String, 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 displayIndex: Int,
val senderInfo: SenderInfo, val senderInfo: SenderInfo,
val annotations: EventAnnotationsSummary? = null, val annotations: EventAnnotationsSummary? = null,

View File

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

View File

@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
import org.matrix.android.sdk.api.session.room.model.VersioningState import org.matrix.android.sdk.api.session.room.model.VersioningState
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
import org.matrix.android.sdk.internal.database.model.ChunkEntityFields
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields 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.EditAggregatedSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.EditionOfEventFields import org.matrix.android.sdk.internal.database.model.EditionOfEventFields
@ -54,7 +55,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
) : RealmMigration { ) : RealmMigration {
companion object { companion object {
const val SESSION_STORE_SCHEMA_VERSION = 19L const val SESSION_STORE_SCHEMA_VERSION = 20L
} }
/** /**
@ -86,6 +87,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion <= 16) migrateTo17(realm) if (oldVersion <= 16) migrateTo17(realm)
if (oldVersion <= 17) migrateTo18(realm) if (oldVersion <= 17) migrateTo18(realm)
if (oldVersion <= 18) migrateTo19(realm) if (oldVersion <= 18) migrateTo19(realm)
if (oldVersion <= 19) migrateTo20(realm)
} }
private fun migrateTo1(realm: DynamicRealm) { private fun migrateTo1(realm: DynamicRealm) {
@ -390,4 +392,26 @@ 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()
}
}
}
} }

View File

@ -110,7 +110,7 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
true true
} }
} }
numberOfTimelineEvents++ // numberOfTimelineEvents++
timelineEvents.add(timelineEventEntity) timelineEvents.add(timelineEventEntity)
} }
@ -191,3 +191,29 @@ internal fun ChunkEntity.nextDisplayIndex(direction: PaginationDirection): Int {
} }
} }
} }
internal fun ChunkEntity.doesNextChunksVerifyCondition(linkCondition: (ChunkEntity) -> Boolean): Boolean {
var nextChunkToCheck = this.nextChunk
while (nextChunkToCheck != null) {
if (linkCondition(nextChunkToCheck)) {
return true
}
nextChunkToCheck = nextChunkToCheck.nextChunk
}
return false
}
internal fun ChunkEntity.isMoreRecentThan(chunkToCheck: ChunkEntity): Boolean {
if (this.isLastForward) return true
if (chunkToCheck.isLastForward) return false
// Check if the chunk to check is linked to this one
if (chunkToCheck.doesNextChunksVerifyCondition { it == this }) {
return true
}
// Otherwise check if this chunk is linked to last forward
if (this.doesNextChunksVerifyCondition { it.isLastForward }) {
return true
}
// We don't know, so we assume it's false
return false
}

View File

@ -28,3 +28,13 @@ internal fun TimelineEventEntity.Companion.nextId(realm: Realm): Long {
currentIdNum.toLong() + 1 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)
}
}

View File

@ -27,9 +27,10 @@ import org.matrix.android.sdk.internal.extensions.clearWith
internal open class ChunkEntity(@Index var prevToken: String? = null, internal open class ChunkEntity(@Index var prevToken: String? = null,
// Because of gaps we can have several chunks with nextToken == null // Because of gaps we can have several chunks with nextToken == null
@Index var nextToken: String? = null, @Index var nextToken: String? = null,
var prevChunk: ChunkEntity? = null,
var nextChunk: ChunkEntity? = null,
var stateEvents: RealmList<EventEntity> = RealmList(), var stateEvents: RealmList<EventEntity> = RealmList(),
var timelineEvents: RealmList<TimelineEventEntity> = RealmList(), var timelineEvents: RealmList<TimelineEventEntity> = RealmList(),
var numberOfTimelineEvents: Long = 0,
// Only one chunk will have isLastForward == true // Only one chunk will have isLastForward == true
@Index var isLastForward: Boolean = false, @Index var isLastForward: Boolean = false,
@Index var isLastBackward: Boolean = false @Index var isLastBackward: Boolean = false

View File

@ -40,8 +40,6 @@ internal open class EventEntity(@Index var eventId: String = "",
var unsignedData: String? = null, var unsignedData: String? = null,
var redacts: String? = null, var redacts: String? = null,
var decryptionResultJson: String? = null, var decryptionResultJson: String? = null,
var decryptionErrorCode: String? = null,
var decryptionErrorReason: String? = null,
var ageLocalTs: Long? = null var ageLocalTs: Long? = null
) : RealmObject() { ) : RealmObject() {
@ -55,6 +53,16 @@ internal open class EventEntity(@Index var eventId: String = "",
sendStateStr = value.name sendStateStr = value.name
} }
var decryptionErrorCode: String? = null
set(value) {
if (value != field) field = value
}
var decryptionErrorReason: String? = null
set(value) {
if (value != field) field = value
}
companion object companion object
fun setDecryptionResult(result: MXEventDecryptionResult, clearEvent: JsonDict? = null) { fun setDecryptionResult(result: MXEventDecryptionResult, clearEvent: JsonDict? = null) {

View File

@ -46,7 +46,5 @@ internal fun TimelineEventEntity.deleteOnCascade(canDeleteRoot: Boolean) {
if (canDeleteRoot) { if (canDeleteRoot) {
root?.deleteFromRealm() root?.deleteFromRealm()
} }
annotations?.deleteOnCascade()
readReceipts?.deleteOnCascade()
deleteFromRealm() deleteFromRealm()
} }

View File

@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.database.query
import io.realm.Realm import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import org.matrix.android.sdk.api.session.events.model.LocalEcho 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.ChunkEntity
import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity
import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity
@ -33,28 +34,26 @@ internal fun isEventRead(realmConfiguration: RealmConfiguration,
if (LocalEcho.isLocalEchoId(eventId)) { if (LocalEcho.isLocalEchoId(eventId)) {
return true return true
} }
// If we don't know if the event has been read, we assume it's not
var isEventRead = false var isEventRead = false
Realm.getInstance(realmConfiguration).use { realm -> Realm.getInstance(realmConfiguration).use { realm ->
val liveChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) ?: return@use val latestEvent = TimelineEventEntity.latestEvent(realm, roomId, true)
val eventToCheck = liveChunk.timelineEvents.find(eventId) // 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 { isEventRead = when {
eventToCheck == null -> hasReadMissingEvent( eventToCheck == null -> false
realm = realm,
latestChunkEntity = liveChunk,
roomId = roomId,
userId = userId,
eventId = eventId
)
eventToCheck.root?.sender == userId -> true eventToCheck.root?.sender == userId -> true
else -> { else -> {
val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst() ?: return@use val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst() ?: return@use
val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.displayIndex ?: Int.MIN_VALUE val readReceiptEvent = TimelineEventEntity.where(realm, roomId, readReceipt.eventId).findFirst() ?: return@use
eventToCheck.displayIndex <= readReceiptIndex readReceiptEvent.isMoreRecentThan(eventToCheck)
} }
} }
} }
return isEventRead return isEventRead
} }

View File

@ -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.DefaultRedactEventTask
import org.matrix.android.sdk.internal.crypto.tasks.RedactEventTask import org.matrix.android.sdk.internal.crypto.tasks.RedactEventTask
import org.matrix.android.sdk.internal.crypto.verification.VerificationMessageProcessor 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.EventInsertLiveObserver
import org.matrix.android.sdk.internal.database.RealmSessionProvider import org.matrix.android.sdk.internal.database.RealmSessionProvider
import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory
@ -339,10 +338,6 @@ internal abstract class SessionModule {
@IntoSet @IntoSet
abstract fun bindIdentityService(service: DefaultIdentityService): SessionLifecycleObserver abstract fun bindIdentityService(service: DefaultIdentityService): SessionLifecycleObserver
@Binds
@IntoSet
abstract fun bindDatabaseCleaner(cleaner: DatabaseCleaner): SessionLifecycleObserver
@Binds @Binds
@IntoSet @IntoSet
abstract fun bindRealmSessionProvider(provider: RealmSessionProvider): SessionLifecycleObserver abstract fun bindRealmSessionProvider(provider: RealmSessionProvider): SessionLifecycleObserver

View File

@ -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.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.task.TaskExecutor
internal class DefaultReadService @AssistedInject constructor( internal class DefaultReadService @AssistedInject constructor(
@Assisted private val roomId: String, @Assisted private val roomId: String,
@SessionDatabase private val monarchy: Monarchy, @SessionDatabase private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor,
private val setReadMarkersTask: SetReadMarkersTask, private val setReadMarkersTask: SetReadMarkersTask,
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
@UserId private val userId: String @UserId private val userId: String

View File

@ -136,7 +136,7 @@ internal class RoomSummaryUpdater @Inject constructor(
roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 || roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 ||
// avoid this call if we are sure there are unread events // 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.setDisplayName(roomDisplayNameResolver.resolve(realm, roomId))
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(realm, roomId) roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(realm, roomId)

View File

@ -23,6 +23,7 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.realm.Sort import io.realm.Sort
import io.realm.kotlin.where 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.isImageMessage
import org.matrix.android.sdk.api.session.events.model.isVideoMessage import org.matrix.android.sdk.api.session.events.model.isVideoMessage
import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.Timeline
@ -54,7 +55,8 @@ internal class DefaultTimelineService @AssistedInject constructor(
private val timelineEventMapper: TimelineEventMapper, private val timelineEventMapper: TimelineEventMapper,
private val loadRoomMembersTask: LoadRoomMembersTask, private val loadRoomMembersTask: LoadRoomMembersTask,
private val threadsAwarenessHandler: ThreadsAwarenessHandler, private val threadsAwarenessHandler: ThreadsAwarenessHandler,
private val readReceiptHandler: ReadReceiptHandler private val readReceiptHandler: ReadReceiptHandler,
private val coroutineDispatchers: MatrixCoroutineDispatchers
) : TimelineService { ) : TimelineService {
@AssistedFactory @AssistedFactory
@ -66,19 +68,18 @@ internal class DefaultTimelineService @AssistedInject constructor(
return DefaultTimeline( return DefaultTimeline(
roomId = roomId, roomId = roomId,
initialEventId = eventId, initialEventId = eventId,
settings = settings,
realmConfiguration = monarchy.realmConfiguration, realmConfiguration = monarchy.realmConfiguration,
taskExecutor = taskExecutor, coroutineDispatchers = coroutineDispatchers,
contextOfEventTask = contextOfEventTask,
paginationTask = paginationTask, paginationTask = paginationTask,
timelineEventMapper = timelineEventMapper, timelineEventMapper = timelineEventMapper,
settings = settings,
timelineInput = timelineInput, timelineInput = timelineInput,
eventDecryptor = eventDecryptor, eventDecryptor = eventDecryptor,
fetchTokenAndPaginateTask = fetchTokenAndPaginateTask, fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
realmSessionProvider = realmSessionProvider,
loadRoomMembersTask = loadRoomMembersTask, loadRoomMembersTask = loadRoomMembersTask,
threadsAwarenessHandler = threadsAwarenessHandler, readReceiptHandler = readReceiptHandler,
readReceiptHandler = readReceiptHandler getEventTask = contextOfEventTask,
threadsAwarenessHandler = threadsAwarenessHandler
) )
} }

View File

@ -16,9 +16,8 @@
package org.matrix.android.sdk.internal.session.room.timeline package org.matrix.android.sdk.internal.session.room.timeline
internal data class TimelineState( internal enum class LoadMoreResult {
val hasReachedEnd: Boolean = false, REACHED_END,
val hasMoreInCache: Boolean = true, SUCCESS,
val isPaginating: Boolean = false, FAILURE
val requestedPaginationCount: Int = 0 }
)

View File

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

View File

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

View File

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

View File

@ -23,6 +23,9 @@ import javax.inject.Inject
@SessionScope @SessionScope
internal class TimelineInput @Inject constructor() { internal class TimelineInput @Inject constructor() {
val listeners = mutableSetOf<Listener>()
fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) { fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) {
listeners.toSet().forEach { it.onLocalEchoCreated(roomId, timelineEvent) } listeners.toSet().forEach { it.onLocalEchoCreated(roomId, timelineEvent) }
} }
@ -35,8 +38,6 @@ internal class TimelineInput @Inject constructor() {
listeners.toSet().forEach { it.onNewTimelineEvents(roomId, eventIds) } listeners.toSet().forEach { it.onNewTimelineEvents(roomId, eventIds) }
} }
val listeners = mutableSetOf<Listener>()
internal interface Listener { internal interface Listener {
fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) = Unit fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) = Unit
fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState) = Unit fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState) = Unit

View File

@ -25,94 +25,25 @@ import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.database.helper.addIfNecessary import org.matrix.android.sdk.internal.database.helper.addIfNecessary
import org.matrix.android.sdk.internal.database.helper.addStateEvent import org.matrix.android.sdk.internal.database.helper.addStateEvent
import org.matrix.android.sdk.internal.database.helper.addTimelineEvent import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
import org.matrix.android.sdk.internal.database.helper.merge
import org.matrix.android.sdk.internal.database.mapper.toEntity import org.matrix.android.sdk.internal.database.mapper.toEntity
import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.ChunkEntity
import org.matrix.android.sdk.internal.database.model.EventInsertType import org.matrix.android.sdk.internal.database.model.EventInsertType
import org.matrix.android.sdk.internal.database.model.RoomEntity 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.TimelineEventEntity
import org.matrix.android.sdk.internal.database.model.deleteOnCascade
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore 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.create
import org.matrix.android.sdk.internal.database.query.find import org.matrix.android.sdk.internal.database.query.find
import org.matrix.android.sdk.internal.database.query.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.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryEventsHelper
import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.util.awaitTransaction
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject 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) { internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase private val monarchy: Monarchy) {
/**
* <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 { enum class Result {
SHOULD_FETCH_MORE, SHOULD_FETCH_MORE,
REACHED_END, REACHED_END,
@ -136,21 +67,21 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
prevToken = receivedChunk.end 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 prevChunk = ChunkEntity.find(realm, roomId, nextToken = prevToken)
val nextChunk = ChunkEntity.find(realm, roomId, prevToken = nextToken) val nextChunk = ChunkEntity.find(realm, roomId, prevToken = nextToken)
val currentChunk = ChunkEntity.create(realm, prevToken = prevToken, nextToken = nextToken).apply {
// The current chunk is the one we will keep all along the merge processChanges. this.nextChunk = nextChunk
// We try to look for a chunk next to the token, this.prevChunk = prevChunk
// 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 }
} }
?: ChunkEntity.create(realm, prevToken, nextToken) nextChunk?.prevChunk = currentChunk
prevChunk?.nextChunk = currentChunk
if (receivedChunk.events.isNullOrEmpty() && !receivedChunk.hasMore()) { if (receivedChunk.events.isEmpty() && !receivedChunk.hasMore()) {
handleReachEnd(realm, roomId, direction, currentChunk) handleReachEnd(roomId, direction, currentChunk)
} else { } else {
handlePagination(realm, roomId, direction, receivedChunk, currentChunk) handlePagination(realm, roomId, direction, receivedChunk, currentChunk)
} }
@ -166,17 +97,10 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
} }
} }
private fun handleReachEnd(realm: Realm, roomId: String, direction: PaginationDirection, currentChunk: ChunkEntity) { private fun handleReachEnd(roomId: String, direction: PaginationDirection, currentChunk: ChunkEntity) {
Timber.v("Reach end of $roomId") Timber.v("Reach end of $roomId")
if (direction == PaginationDirection.FORWARDS) { if (direction == PaginationDirection.FORWARDS) {
val currentLastForwardChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) Timber.v("We should keep the lastForward chunk unique, the one from sync")
if (currentChunk != currentLastForwardChunk) {
currentChunk.isLastForward = true
currentLastForwardChunk?.deleteOnCascade(deleteStateEvents = false, canDeleteRoot = false)
RoomSummaryEntity.where(realm, roomId).findFirst()?.apply {
latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
}
}
} else { } else {
currentChunk.isLastBackward = true currentChunk.isLastBackward = true
} }
@ -204,44 +128,50 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
roomMemberContentsByUser[stateEvent.stateKey] = stateEvent.content.toModel<RoomMemberContent>() roomMemberContentsByUser[stateEvent.stateKey] = stateEvent.content.toModel<RoomMemberContent>()
} }
} }
val eventIds = ArrayList<String>(eventList.size) run processTimelineEvents@{
eventList.forEach { event -> eventList.forEach { event ->
if (event.eventId == null || event.senderId == null) { if (event.eventId == null || event.senderId == null) {
return@forEach 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
} }
roomMemberContentsByUser[event.stateKey] = contentToUse.toModel<RoomMemberContent>() // 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)
} }
currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser)
}
// Find all the chunks which contain at least one event from the list of eventIds
val chunks = ChunkEntity.findAllIncludingEvents(realm, eventIds)
Timber.d("Found ${chunks.size} chunks containing at least one of the eventIds")
val chunksToDelete = ArrayList<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) { if (currentChunk.isValid) {
RoomEntity.where(realm, roomId).findFirst()?.addIfNecessary(currentChunk) RoomEntity.where(realm, roomId).findFirst()?.addIfNecessary(currentChunk)

View File

@ -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.model.relation.ReactionContent
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent 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 timber.log.Timber
import java.util.Collections import java.util.Collections
internal class UIEchoManager( internal class UIEchoManager(private val listener: Listener) {
private val settings: TimelineSettings,
private val listener: Listener
) {
interface Listener { interface Listener {
fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?): Boolean fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?): Boolean
@ -70,13 +66,12 @@ internal class UIEchoManager(
return existingState != sendState return existingState != sendState
} }
fun onLocalEchoCreated(timelineEvent: TimelineEvent) { fun onLocalEchoCreated(timelineEvent: TimelineEvent): Boolean {
// Manage some ui echos (do it before filter because actual event could be filtered out)
when (timelineEvent.root.getClearType()) { when (timelineEvent.root.getClearType()) {
EventType.REDACTION -> { EventType.REDACTION -> {
} }
EventType.REACTION -> { EventType.REACTION -> {
val content = timelineEvent.root.content?.toModel<ReactionContent>() val content: ReactionContent? = timelineEvent.root.content?.toModel<ReactionContent>()
if (RelationType.ANNOTATION == content?.relatesTo?.type) { if (RelationType.ANNOTATION == content?.relatesTo?.type) {
val reaction = content.relatesTo.key val reaction = content.relatesTo.key
val relatedEventID = content.relatesTo.eventId val relatedEventID = content.relatesTo.eventId
@ -96,11 +91,12 @@ internal class UIEchoManager(
} }
Timber.v("On local echo created: ${timelineEvent.eventId}") Timber.v("On local echo created: ${timelineEvent.eventId}")
inMemorySendingEvents.add(0, timelineEvent) inMemorySendingEvents.add(0, timelineEvent)
return true
} }
fun decorateEventWithReactionUiEcho(timelineEvent: TimelineEvent): TimelineEvent? { fun decorateEventWithReactionUiEcho(timelineEvent: TimelineEvent): TimelineEvent {
val relatedEventID = timelineEvent.eventId val relatedEventID = timelineEvent.eventId
val contents = inMemoryReactions[relatedEventID] ?: return null val contents = inMemoryReactions[relatedEventID] ?: return timelineEvent
var existingAnnotationSummary = timelineEvent.annotations ?: EventAnnotationsSummary( var existingAnnotationSummary = timelineEvent.annotations ?: EventAnnotationsSummary(
relatedEventID relatedEventID

View File

@ -345,15 +345,17 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
syncLocalTimestampMillis: Long, syncLocalTimestampMillis: Long,
aggregator: SyncResponsePostTreatmentAggregator): ChunkEntity { aggregator: SyncResponsePostTreatmentAggregator): ChunkEntity {
val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId) val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId)
if (isLimited && lastChunk != null) {
lastChunk.deleteOnCascade(deleteStateEvents = true, canDeleteRoot = true)
}
val chunkEntity = if (!isLimited && lastChunk != null) { val chunkEntity = if (!isLimited && lastChunk != null) {
lastChunk lastChunk
} else { } 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 eventIds = ArrayList<String>(eventList.size)
val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>() val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()

View File

@ -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.UpdateUserAccountDataTask
import org.matrix.android.sdk.internal.session.user.accountdata.UserAccountDataDataSource import org.matrix.android.sdk.internal.session.user.accountdata.UserAccountDataDataSource
import org.matrix.android.sdk.internal.util.ensureTrailingSlash import org.matrix.android.sdk.internal.util.ensureTrailingSlash
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal class DefaultTermsService @Inject constructor( internal class DefaultTermsService @Inject constructor(
@ -63,19 +64,28 @@ internal class DefaultTermsService @Inject constructor(
*/ */
override suspend fun getHomeserverTerms(baseUrl: String): TermsResponse { override suspend fun getHomeserverTerms(baseUrl: String): TermsResponse {
return try { return try {
val request = baseUrl + NetworkConstants.URI_API_PREFIX_PATH_R0 + "register"
executeRequest(null) { 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 // Return empty result if it succeed, but it should never happen
Timber.w("Request $request succeeded, it should never happen")
TermsResponse() TermsResponse()
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
@Suppress("UNCHECKED_CAST") val registrationFlowResponse = throwable.toRegistrationFlowResponse()
TermsResponse( if (registrationFlowResponse != null) {
policies = (throwable.toRegistrationFlowResponse() @Suppress("UNCHECKED_CAST")
?.params TermsResponse(
?.get(LoginFlowTypes.TERMS) as? JsonDict) policies = (registrationFlowResponse
?.get("policies") as? JsonDict .params
) ?.get(LoginFlowTypes.TERMS) as? JsonDict)
?.get("policies") as? JsonDict
)
} else {
// Other error
Timber.e(throwable, "Error while getting homeserver terms")
throw throwable
}
} }
} }

View File

@ -32,8 +32,6 @@
import static import static
### Rubbish from merge. Please delete those lines (sometimes in comment) ### Rubbish from merge. Please delete those lines (sometimes in comment)
<<<<<<<
>>>>>>>
### carry return before "}". Please remove empty lines. ### carry return before "}". Please remove empty lines.
\n\s*\n\s*\} \n\s*\n\s*\}
@ -160,7 +158,7 @@ Formatter\.formatShortFileSize===1
# android\.text\.TextUtils # 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 ### 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===116 enum class===117
### Do not import temporary legacy classes ### Do not import temporary legacy classes
import org.matrix.android.sdk.internal.legacy.riot===3 import org.matrix.android.sdk.internal.legacy.riot===3

View File

@ -72,6 +72,7 @@
<!-- Wording --> <!-- Wording -->
<issue id="Typos" severity="error" /> <issue id="Typos" severity="error" />
<issue id="TypographyDashes" severity="error" />
<!-- Ignore lint issue in generated resource file from templates. <!-- Ignore lint issue in generated resource file from templates.
https://github.com/LikeTheSalad/android-string-reference generates string from the default language https://github.com/LikeTheSalad/android-string-reference generates string from the default language

View File

@ -108,7 +108,6 @@ class SpanUtilsTest : InstrumentedTest {
val string = SpannableString("Text") val string = SpannableString("Text")
val result = spanUtils.getBindingOptions(string) val result = spanUtils.getBindingOptions(string)
result.canUseTextFuture shouldBeEqualTo true result.canUseTextFuture shouldBeEqualTo true
result.preventMutation shouldBeEqualTo false
} }
@Test @Test
@ -117,7 +116,6 @@ class SpanUtilsTest : InstrumentedTest {
string.setSpan(StrikethroughSpan(), 10, 23, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) string.setSpan(StrikethroughSpan(), 10, 23, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
val result = spanUtils.getBindingOptions(string) val result = spanUtils.getBindingOptions(string)
result.canUseTextFuture shouldBeEqualTo false result.canUseTextFuture shouldBeEqualTo false
result.preventMutation shouldBeEqualTo false
} }
@Test @Test
@ -125,7 +123,6 @@ class SpanUtilsTest : InstrumentedTest {
val string = SpannableString("Emoji \uD83D\uDE2E\u200D\uD83D\uDCA8") val string = SpannableString("Emoji \uD83D\uDE2E\u200D\uD83D\uDCA8")
val result = spanUtils.getBindingOptions(string) val result = spanUtils.getBindingOptions(string)
result.canUseTextFuture shouldBeEqualTo false result.canUseTextFuture shouldBeEqualTo false
result.preventMutation shouldBeEqualTo true
} }
private fun trueIfAlwaysAllowed() = Build.VERSION.SDK_INT < Build.VERSION_CODES.P private fun trueIfAlwaysAllowed() = Build.VERSION.SDK_INT < Build.VERSION_CODES.P

View File

@ -134,10 +134,10 @@ class ElementRobot {
activity.runOnUiThread { popup.performClick() } activity.runOnUiThread { popup.performClick() }
waitUntilViewVisible(withId(R.id.bottomSheetFragmentContainer)) waitUntilViewVisible(withId(R.id.bottomSheetFragmentContainer))
waitUntilViewVisible(ViewMatchers.withText(R.string.skip)) waitUntilViewVisible(ViewMatchers.withText(R.string.action_skip))
clickOn(R.string.skip) clickOn(R.string.action_skip)
assertDisplayed(R.string.are_you_sure) assertDisplayed(R.string.are_you_sure)
clickOn(R.string.skip) clickOn(R.string.action_skip)
waitUntilViewVisible(withId(R.id.bottomSheetFragmentContainer)) waitUntilViewVisible(withId(R.id.bottomSheetFragmentContainer))
}.onFailure { Timber.w("Verification popup missing", it) } }.onFailure { Timber.w("Verification popup missing", it) }
} }

View File

@ -56,7 +56,7 @@ object ConfirmationDialogBuilder {
?.takeIf { it.isNotBlank() } ?.takeIf { it.isNotBlank() }
confirmation(reason) confirmation(reason)
} }
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.action_cancel, null)
.show() .show()
} }
} }

View File

@ -113,7 +113,7 @@ class GalleryOrCameraDialogHelper(
)) { _, which -> )) { _, which ->
onAvatarTypeSelected(if (which == 0) Type.Camera else Type.Gallery) onAvatarTypeSelected(if (which == 0) Type.Camera else Type.Gallery)
} }
.setPositiveButton(R.string.cancel, null) .setPositiveButton(R.string.action_cancel, null)
.show() .show()
} }

View File

@ -34,7 +34,7 @@ object ManuallyVerifyDialog {
.setPositiveButton(R.string.encryption_information_verify) { _, _ -> .setPositiveButton(R.string.encryption_information_verify) { _, _ ->
onVerified() onVerified()
} }
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.action_cancel, null)
views.encryptedDeviceInfoDeviceName.text = cryptoDeviceInfo.displayName() views.encryptedDeviceInfoDeviceName.text = cryptoDeviceInfo.displayName()
views.encryptedDeviceInfoDeviceId.text = cryptoDeviceInfo.deviceId views.encryptedDeviceInfoDeviceId.text = cryptoDeviceInfo.deviceId

View File

@ -57,7 +57,7 @@ class PhotoOrVideoDialog(
.setPositiveButton(R.string._continue) { _, _ -> .setPositiveButton(R.string._continue) { _, _ ->
submit(views, vectorPreferences, listener) submit(views, vectorPreferences, listener)
} }
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.action_cancel, null)
.show() .show()
} }
} }
@ -98,11 +98,11 @@ class PhotoOrVideoDialog(
MaterialAlertDialogBuilder(activity) MaterialAlertDialogBuilder(activity)
.setTitle(R.string.option_take_photo_video) .setTitle(R.string.option_take_photo_video)
.setView(dialogLayout) .setView(dialogLayout)
.setPositiveButton(R.string.save) { _, _ -> .setPositiveButton(R.string.action_save) { _, _ ->
submitSettings(views) submitSettings(views)
listener.onUpdated() listener.onUpdated()
} }
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.action_cancel, null)
.show() .show()
} }

View File

@ -151,7 +151,7 @@ class UnrecognizedCertificateDialog @Inject constructor(
} }
builder.setNeutralButton(R.string.ssl_logout_account) { _, _ -> callback.onReject() } builder.setNeutralButton(R.string.ssl_logout_account) { _, _ -> callback.onReject() }
} else { } else {
builder.setNegativeButton(R.string.cancel) { _, _ -> callback.onReject() } builder.setNegativeButton(R.string.action_cancel) { _, _ -> callback.onReject() }
} }
builder.setOnDismissListener { builder.setOnDismissListener {

View File

@ -17,9 +17,6 @@
package im.vector.app.core.epoxy package im.vector.app.core.epoxy
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import com.airbnb.epoxy.EpoxyModelWithHolder import com.airbnb.epoxy.EpoxyModelWithHolder
import com.airbnb.epoxy.VisibilityState import com.airbnb.epoxy.VisibilityState
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -30,24 +27,19 @@ import kotlinx.coroutines.cancelChildren
/** /**
* EpoxyModelWithHolder which can listen to visibility state change * EpoxyModelWithHolder which can listen to visibility state change
*/ */
abstract class VectorEpoxyModel<H : VectorEpoxyHolder> : EpoxyModelWithHolder<H>(), LifecycleOwner { abstract class VectorEpoxyModel<H : VectorEpoxyHolder> : EpoxyModelWithHolder<H>() {
protected val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) protected val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
override fun getLifecycle() = lifecycleRegistry
private var onModelVisibilityStateChangedListener: OnVisibilityStateChangedListener? = null private var onModelVisibilityStateChangedListener: OnVisibilityStateChangedListener? = null
@CallSuper @CallSuper
override fun bind(holder: H) { override fun bind(holder: H) {
super.bind(holder) super.bind(holder)
lifecycleRegistry.currentState = Lifecycle.State.STARTED
} }
@CallSuper @CallSuper
override fun unbind(holder: H) { override fun unbind(holder: H) {
lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
coroutineScope.coroutineContext.cancelChildren() coroutineScope.coroutineContext.cancelChildren()
super.unbind(holder) super.unbind(holder)
} }

View File

@ -26,15 +26,14 @@ import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence
import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.onClick
import im.vector.app.core.epoxy.util.preventMutation
import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.features.displayname.getBestName import im.vector.app.features.displayname.getBestName
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.item.BindingOptions import im.vector.app.features.home.room.detail.timeline.item.BindingOptions
import im.vector.app.features.home.room.detail.timeline.tools.findPillsAndProcess import im.vector.app.features.home.room.detail.timeline.tools.findPillsAndProcess
import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.ImageContentRenderer
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
/** /**
@ -50,13 +49,13 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
lateinit var matrixItem: MatrixItem lateinit var matrixItem: MatrixItem
@EpoxyAttribute @EpoxyAttribute
lateinit var body: CharSequence lateinit var body: EpoxyCharSequence
@EpoxyAttribute @EpoxyAttribute
var bindingOptions: BindingOptions? = null var bindingOptions: BindingOptions? = null
@EpoxyAttribute @EpoxyAttribute
var bodyDetails: CharSequence? = null var bodyDetails: EpoxyCharSequence? = null
@EpoxyAttribute @EpoxyAttribute
var imageContentRenderer: ImageContentRenderer? = null var imageContentRenderer: ImageContentRenderer? = null
@ -65,7 +64,7 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
var data: ImageContentRenderer.Data? = null var data: ImageContentRenderer.Data? = null
@EpoxyAttribute @EpoxyAttribute
var time: CharSequence? = null var time: String? = null
@EpoxyAttribute @EpoxyAttribute
var movementMethod: MovementMethod? = null var movementMethod: MovementMethod? = null
@ -84,13 +83,9 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
} }
holder.imagePreview.isVisible = data != null holder.imagePreview.isVisible = data != null
holder.body.movementMethod = movementMethod holder.body.movementMethod = movementMethod
holder.body.text = if (bindingOptions?.preventMutation.orFalse()) { holder.body.text = body.charSequence
body.preventMutation() holder.bodyDetails.setTextOrHide(bodyDetails?.charSequence)
} else { body.charSequence.findPillsAndProcess(coroutineScope) { it.bind(holder.body) }
body
}
holder.bodyDetails.setTextOrHide(bodyDetails)
body.findPillsAndProcess(coroutineScope) { it.bind(holder.body) }
holder.timestamp.setTextOrHide(time) holder.timestamp.setTextOrHide(time)
} }

View File

@ -37,7 +37,7 @@ import im.vector.app.core.extensions.setTextOrHide
abstract class BottomSheetRadioActionItem : VectorEpoxyModel<BottomSheetRadioActionItem.Holder>() { abstract class BottomSheetRadioActionItem : VectorEpoxyModel<BottomSheetRadioActionItem.Holder>() {
@EpoxyAttribute @EpoxyAttribute
var title: CharSequence? = null var title: String? = null
@StringRes @StringRes
@EpoxyAttribute @EpoxyAttribute
@ -47,7 +47,7 @@ abstract class BottomSheetRadioActionItem : VectorEpoxyModel<BottomSheetRadioAct
var selected = false var selected = false
@EpoxyAttribute @EpoxyAttribute
var description: CharSequence? = null var description: String? = null
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
lateinit var listener: ClickListener lateinit var listener: ClickListener

View File

@ -36,7 +36,7 @@ abstract class BottomSheetSendStateItem : VectorEpoxyModel<BottomSheetSendStateI
var showProgress: Boolean = false var showProgress: Boolean = false
@EpoxyAttribute @EpoxyAttribute
lateinit var text: CharSequence lateinit var text: String
@EpoxyAttribute @EpoxyAttribute
@DrawableRes @DrawableRes

View File

@ -0,0 +1,27 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.epoxy.charsequence
/**
* Wrapper for a CharSequence, which support mutation of the CharSequence, which can happen during rendering
*/
class EpoxyCharSequence(val charSequence: CharSequence) {
private val hash = charSequence.toString().hashCode()
override fun hashCode() = hash
override fun equals(other: Any?) = other is EpoxyCharSequence && other.hash == hash
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2021 New Vector Ltd * Copyright (c) 2022 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,8 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.core.epoxy.util package im.vector.app.core.epoxy.charsequence
import android.text.SpannableString /**
* Extensions to wrap CharSequence to EpoxyCharSequence
fun CharSequence?.preventMutation(): CharSequence? = this?.let { SpannableString(it) } */
fun CharSequence.toEpoxyCharSequence() = EpoxyCharSequence(this)

View File

@ -33,7 +33,7 @@ import im.vector.app.core.extensions.setAttributeTintedImageResource
abstract class RadioButtonItem : VectorEpoxyModel<RadioButtonItem.Holder>() { abstract class RadioButtonItem : VectorEpoxyModel<RadioButtonItem.Holder>() {
@EpoxyAttribute @EpoxyAttribute
var title: CharSequence? = null var title: String? = null
@StringRes @StringRes
@EpoxyAttribute @EpoxyAttribute

View File

@ -100,6 +100,10 @@ class DefaultErrorFormatter @Inject constructor(
throwable.error.code == MatrixError.M_THREEPID_AUTH_FAILED -> { throwable.error.code == MatrixError.M_THREEPID_AUTH_FAILED -> {
stringProvider.getString(R.string.error_threepid_auth_failed) stringProvider.getString(R.string.error_threepid_auth_failed)
} }
throwable.error.code == MatrixError.M_UNKNOWN &&
throwable.error.message == "Not allowed to join this room" -> {
stringProvider.getString(R.string.room_error_access_unauthorized)
}
else -> { else -> {
throwable.error.message.takeIf { it.isNotEmpty() } throwable.error.message.takeIf { it.isNotEmpty() }
?: throwable.error.code.takeIf { it.isNotEmpty() } ?: throwable.error.code.takeIf { it.isNotEmpty() }

View File

@ -57,7 +57,7 @@ class CallService : VectorService() {
private val knownCalls = mutableMapOf<String, CallInformation>() private val knownCalls = mutableMapOf<String, CallInformation>()
private val connectedCallIds = mutableSetOf<String>() private val connectedCallIds = mutableSetOf<String>()
lateinit var notificationManager: NotificationManagerCompat private lateinit var notificationManager: NotificationManagerCompat
@Inject lateinit var notificationUtils: NotificationUtils @Inject lateinit var notificationUtils: NotificationUtils
@Inject lateinit var callManager: WebRtcCallManager @Inject lateinit var callManager: WebRtcCallManager
@Inject lateinit var avatarRenderer: AvatarRenderer @Inject lateinit var avatarRenderer: AvatarRenderer
@ -125,13 +125,6 @@ class CallService : VectorService() {
callRingPlayerOutgoing?.stop() callRingPlayerOutgoing?.stop()
displayCallInProgressNotification(intent) displayCallInProgressNotification(intent)
} }
ACTION_CALL_CONNECTING -> {
// lower notification priority
displayCallInProgressNotification(intent)
// stop ringing
callRingPlayerIncoming?.stop()
callRingPlayerOutgoing?.stop()
}
ACTION_CALL_TERMINATED -> { ACTION_CALL_TERMINATED -> {
handleCallTerminated(intent) handleCallTerminated(intent)
} }
@ -320,12 +313,8 @@ class CallService : VectorService() {
private const val ACTION_INCOMING_RINGING_CALL = "im.vector.app.core.services.CallService.ACTION_INCOMING_RINGING_CALL" private const val ACTION_INCOMING_RINGING_CALL = "im.vector.app.core.services.CallService.ACTION_INCOMING_RINGING_CALL"
private const val ACTION_OUTGOING_RINGING_CALL = "im.vector.app.core.services.CallService.ACTION_OUTGOING_RINGING_CALL" private const val ACTION_OUTGOING_RINGING_CALL = "im.vector.app.core.services.CallService.ACTION_OUTGOING_RINGING_CALL"
private const val ACTION_CALL_CONNECTING = "im.vector.app.core.services.CallService.ACTION_CALL_CONNECTING"
private const val ACTION_ONGOING_CALL = "im.vector.app.core.services.CallService.ACTION_ONGOING_CALL" private const val ACTION_ONGOING_CALL = "im.vector.app.core.services.CallService.ACTION_ONGOING_CALL"
private const val ACTION_CALL_TERMINATED = "im.vector.app.core.services.CallService.ACTION_CALL_TERMINATED" private const val ACTION_CALL_TERMINATED = "im.vector.app.core.services.CallService.ACTION_CALL_TERMINATED"
private const val ACTION_NO_ACTIVE_CALL = "im.vector.app.core.services.CallService.NO_ACTIVE_CALL"
// private const val ACTION_ACTIVITY_VISIBLE = "im.vector.app.core.services.CallService.ACTION_ACTIVITY_VISIBLE"
// private const val ACTION_STOP_RINGING = "im.vector.app.core.services.CallService.ACTION_STOP_RINGING"
private const val EXTRA_CALL_ID = "EXTRA_CALL_ID" private const val EXTRA_CALL_ID = "EXTRA_CALL_ID"
private const val EXTRA_IS_IN_BG = "EXTRA_IS_IN_BG" private const val EXTRA_IS_IN_BG = "EXTRA_IS_IN_BG"
@ -351,7 +340,6 @@ class CallService : VectorService() {
action = ACTION_OUTGOING_RINGING_CALL action = ACTION_OUTGOING_RINGING_CALL
putExtra(EXTRA_CALL_ID, callId) putExtra(EXTRA_CALL_ID, callId)
} }
ContextCompat.startForegroundService(context, intent) ContextCompat.startForegroundService(context, intent)
} }
@ -362,11 +350,13 @@ class CallService : VectorService() {
action = ACTION_ONGOING_CALL action = ACTION_ONGOING_CALL
putExtra(EXTRA_CALL_ID, callId) putExtra(EXTRA_CALL_ID, callId)
} }
ContextCompat.startForegroundService(context, intent) ContextCompat.startForegroundService(context, intent)
} }
fun onCallTerminated(context: Context, callId: String, endCallReason: EndCallReason, rejected: Boolean) { fun onCallTerminated(context: Context,
callId: String,
endCallReason: EndCallReason,
rejected: Boolean) {
val intent = Intent(context, CallService::class.java) val intent = Intent(context, CallService::class.java)
.apply { .apply {
action = ACTION_CALL_TERMINATED action = ACTION_CALL_TERMINATED

View File

@ -23,7 +23,7 @@ import im.vector.app.core.platform.VectorSharedAction
* Parent class for a bottom sheet action * Parent class for a bottom sheet action
*/ */
open class BottomSheetGenericRadioAction( open class BottomSheetGenericRadioAction(
open val title: CharSequence?, open val title: String?,
open val description: String? = null, open val description: String? = null,
open val isSelected: Boolean open val isSelected: Boolean
) : VectorSharedAction { ) : VectorSharedAction {

View File

@ -39,10 +39,10 @@ import im.vector.app.core.extensions.setTextOrHide
abstract class GenericEmptyWithActionItem : VectorEpoxyModel<GenericEmptyWithActionItem.Holder>() { abstract class GenericEmptyWithActionItem : VectorEpoxyModel<GenericEmptyWithActionItem.Holder>() {
@EpoxyAttribute @EpoxyAttribute
var title: CharSequence? = null var title: String? = null
@EpoxyAttribute @EpoxyAttribute
var description: CharSequence? = null var description: String? = null
@EpoxyAttribute @EpoxyAttribute
@DrawableRes @DrawableRes

View File

@ -38,7 +38,7 @@ import im.vector.app.features.themes.ThemeUtils
abstract class GenericFooterItem : VectorEpoxyModel<GenericFooterItem.Holder>() { abstract class GenericFooterItem : VectorEpoxyModel<GenericFooterItem.Holder>() {
@EpoxyAttribute @EpoxyAttribute
var text: CharSequence? = null var text: String? = null
@EpoxyAttribute @EpoxyAttribute
var style: ItemStyle = ItemStyle.NORMAL_TEXT var style: ItemStyle = ItemStyle.NORMAL_TEXT

View File

@ -28,6 +28,7 @@ import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence
import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.onClick
import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.extensions.setTextOrHide
@ -41,10 +42,10 @@ import im.vector.app.core.extensions.setTextOrHide
abstract class GenericItem : VectorEpoxyModel<GenericItem.Holder>() { abstract class GenericItem : VectorEpoxyModel<GenericItem.Holder>() {
@EpoxyAttribute @EpoxyAttribute
var title: CharSequence? = null var title: EpoxyCharSequence? = null
@EpoxyAttribute @EpoxyAttribute
var description: CharSequence? = null var description: EpoxyCharSequence? = null
@EpoxyAttribute @EpoxyAttribute
var style: ItemStyle = ItemStyle.NORMAL_TEXT var style: ItemStyle = ItemStyle.NORMAL_TEXT
@ -71,7 +72,7 @@ abstract class GenericItem : VectorEpoxyModel<GenericItem.Holder>() {
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
holder.titleText.setTextOrHide(title) holder.titleText.setTextOrHide(title?.charSequence)
if (titleIconResourceId != -1) { if (titleIconResourceId != -1) {
holder.titleIcon.setImageResource(titleIconResourceId) holder.titleIcon.setImageResource(titleIconResourceId)
@ -82,7 +83,7 @@ abstract class GenericItem : VectorEpoxyModel<GenericItem.Holder>() {
holder.titleText.textSize = style.toTextSize() holder.titleText.textSize = style.toTextSize()
holder.descriptionText.setTextOrHide(description) holder.descriptionText.setTextOrHide(description?.charSequence)
if (hasIndeterminateProcess) { if (hasIndeterminateProcess) {
holder.progressBar.isVisible = true holder.progressBar.isVisible = true

View File

@ -28,6 +28,7 @@ import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence
import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.onClick
import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
@ -39,7 +40,7 @@ import im.vector.app.features.themes.ThemeUtils
abstract class GenericPillItem : VectorEpoxyModel<GenericPillItem.Holder>() { abstract class GenericPillItem : VectorEpoxyModel<GenericPillItem.Holder>() {
@EpoxyAttribute @EpoxyAttribute
var text: CharSequence? = null var text: EpoxyCharSequence? = null
@EpoxyAttribute @EpoxyAttribute
var style: ItemStyle = ItemStyle.NORMAL_TEXT var style: ItemStyle = ItemStyle.NORMAL_TEXT
@ -60,7 +61,7 @@ abstract class GenericPillItem : VectorEpoxyModel<GenericPillItem.Holder>() {
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
holder.textView.setTextOrHide(text) holder.textView.setTextOrHide(text?.charSequence)
holder.textView.typeface = style.toTypeFace() holder.textView.typeface = style.toTypeFace()
holder.textView.textSize = style.toTextSize() holder.textView.textSize = style.toTextSize()
holder.textView.gravity = if (centered) Gravity.CENTER_HORIZONTAL else Gravity.START holder.textView.gravity = if (centered) Gravity.CENTER_HORIZONTAL else Gravity.START

View File

@ -27,6 +27,7 @@ import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence
import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.onClick
import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
@ -41,10 +42,10 @@ import im.vector.app.features.themes.ThemeUtils
abstract class GenericWithValueItem : VectorEpoxyModel<GenericWithValueItem.Holder>() { abstract class GenericWithValueItem : VectorEpoxyModel<GenericWithValueItem.Holder>() {
@EpoxyAttribute @EpoxyAttribute
var title: CharSequence? = null var title: EpoxyCharSequence? = null
@EpoxyAttribute @EpoxyAttribute
var value: CharSequence? = null var value: String? = null
@EpoxyAttribute @EpoxyAttribute
@ColorInt @ColorInt
@ -62,7 +63,7 @@ abstract class GenericWithValueItem : VectorEpoxyModel<GenericWithValueItem.Hold
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
holder.titleText.setTextOrHide(title) holder.titleText.setTextOrHide(title?.charSequence)
if (titleIconResourceId != -1) { if (titleIconResourceId != -1) {
holder.titleIcon.setImageResource(titleIconResourceId) holder.titleIcon.setImageResource(titleIconResourceId)

View File

@ -68,7 +68,7 @@ fun Context.showIdentityServerConsentDialog(identityServerWithTerms: ServerAndPo
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this)
.setTitle(getString(R.string.identity_server_consent_dialog_title_2, identityServerWithTerms?.serverUrl.orEmpty())) .setTitle(getString(R.string.identity_server_consent_dialog_title_2, identityServerWithTerms?.serverUrl.orEmpty()))
.setMessage(content) .setMessage(content)
.setPositiveButton(R.string.reactions_agree) { _, _ -> .setPositiveButton(R.string.action_agree) { _, _ ->
consentCallBack.invoke() consentCallBack.invoke()
} }
.setNegativeButton(R.string.action_not_now, null) .setNegativeButton(R.string.action_not_now, null)

View File

@ -305,26 +305,28 @@ fun shareMedia(context: Context, file: File, mediaMimeType: String?) {
return return
} }
val sendIntent = ShareCompat.IntentBuilder(context) val chooserIntent = ShareCompat.IntentBuilder(context)
.setType(mediaMimeType) .setType(mediaMimeType)
.setStream(mediaUri) .setStream(mediaUri)
.getIntent() .setChooserTitle(R.string.action_share)
.createChooserIntent()
sendShareIntent(context, sendIntent) createChooser(context, chooserIntent)
} }
fun shareText(context: Context, text: String) { fun shareText(context: Context, text: String) {
val sendIntent = Intent() val chooserIntent = ShareCompat.IntentBuilder(context)
sendIntent.action = Intent.ACTION_SEND .setType("text/plain")
sendIntent.type = "text/plain" .setText(text)
sendIntent.putExtra(Intent.EXTRA_TEXT, text) .setChooserTitle(R.string.action_share)
.createChooserIntent()
sendShareIntent(context, sendIntent) createChooser(context, chooserIntent)
} }
private fun sendShareIntent(context: Context, intent: Intent) { private fun createChooser(context: Context, intent: Intent) {
try { try {
context.startActivity(Intent.createChooser(intent, context.getString(R.string.share))) context.startActivity(intent)
} catch (activityNotFoundException: ActivityNotFoundException) { } catch (activityNotFoundException: ActivityNotFoundException) {
context.toast(R.string.error_no_external_application_found) context.toast(R.string.error_no_external_application_found)
} }

View File

@ -168,6 +168,6 @@ fun FragmentActivity.onPermissionDeniedDialog(@StringRes rationaleMessage: Int)
.setPositiveButton(R.string.open_settings) { _, _ -> .setPositiveButton(R.string.open_settings) { _, _ ->
openAppSettingsPage(this) openAppSettingsPage(this)
} }
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.action_cancel, null)
.show() .show()
} }

View File

@ -209,7 +209,7 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
.setTitle(R.string.dialog_title_error) .setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(failure)) .setMessage(errorFormatter.toHumanReadable(failure))
.setPositiveButton(R.string.global_retry) { _, _ -> doCleanUp() } .setPositiveButton(R.string.global_retry) { _, _ -> doCleanUp() }
.setNegativeButton(R.string.cancel) { _, _ -> startNextActivityAndFinish(ignoreClearCredentials = true) } .setNegativeButton(R.string.action_cancel) { _, _ -> startNextActivityAndFinish(ignoreClearCredentials = true) }
.setCancelable(false) .setCancelable(false)
.show() .show()
} }

View File

@ -29,13 +29,13 @@ import im.vector.app.core.epoxy.onClick
abstract class AutocompleteCommandItem : VectorEpoxyModel<AutocompleteCommandItem.Holder>() { abstract class AutocompleteCommandItem : VectorEpoxyModel<AutocompleteCommandItem.Holder>() {
@EpoxyAttribute @EpoxyAttribute
var name: CharSequence? = null var name: String? = null
@EpoxyAttribute @EpoxyAttribute
var parameters: CharSequence? = null var parameters: String? = null
@EpoxyAttribute @EpoxyAttribute
var description: CharSequence? = null var description: String? = null
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
var clickListener: ClickListener? = null var clickListener: ClickListener? = null

View File

@ -135,7 +135,7 @@ class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMee
.setPositiveButton(R.string.action_switch) { _, _ -> .setPositiveButton(R.string.action_switch) { _, _ ->
jitsiViewModel.handle(JitsiCallViewActions.SwitchTo(action.args, false)) jitsiViewModel.handle(JitsiCallViewActions.SwitchTo(action.args, false))
} }
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.action_cancel, null)
.show() .show()
} }

View File

@ -78,7 +78,7 @@ class KeysBackupSettingsFragment @Inject constructor(private val keysBackupSetti
.setPositiveButton(R.string.keys_backup_settings_delete_confirm_title) { _, _ -> .setPositiveButton(R.string.keys_backup_settings_delete_confirm_title) { _, _ ->
viewModel.handle(KeyBackupSettingsAction.DeleteKeyBackup) viewModel.handle(KeyBackupSettingsAction.DeleteKeyBackup)
} }
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.action_cancel, null)
.setCancelable(true) .setCancelable(true)
.show() .show()
} }

View File

@ -22,6 +22,7 @@ import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.epoxy.errorWithRetryItem import im.vector.app.core.epoxy.errorWithRetryItem
import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
@ -73,11 +74,11 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(
KeysBackupState.Disabled -> { KeysBackupState.Disabled -> {
genericItem { genericItem {
id("summary") id("summary")
title(host.stringProvider.getString(R.string.keys_backup_settings_status_not_setup)) title(host.stringProvider.getString(R.string.keys_backup_settings_status_not_setup).toEpoxyCharSequence())
style(ItemStyle.BIG_TEXT) style(ItemStyle.BIG_TEXT)
if (data.keysBackupVersionTrust()?.usable == false) { if (data.keysBackupVersionTrust()?.usable == false) {
description(host.stringProvider.getString(R.string.keys_backup_settings_untrusted_backup)) description(host.stringProvider.getString(R.string.keys_backup_settings_untrusted_backup).toEpoxyCharSequence())
} }
} }
@ -88,12 +89,12 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(
KeysBackupState.Enabling -> { KeysBackupState.Enabling -> {
genericItem { genericItem {
id("summary") id("summary")
title(host.stringProvider.getString(R.string.keys_backup_settings_status_ko)) title(host.stringProvider.getString(R.string.keys_backup_settings_status_ko).toEpoxyCharSequence())
style(ItemStyle.BIG_TEXT) style(ItemStyle.BIG_TEXT)
if (data.keysBackupVersionTrust()?.usable == false) { if (data.keysBackupVersionTrust()?.usable == false) {
description(host.stringProvider.getString(R.string.keys_backup_settings_untrusted_backup)) description(host.stringProvider.getString(R.string.keys_backup_settings_untrusted_backup).toEpoxyCharSequence())
} else { } else {
description(keyBackupState.toString()) description(keyBackupState.toString().toEpoxyCharSequence())
} }
endIconResourceId(R.drawable.unit_test_ko) endIconResourceId(R.drawable.unit_test_ko)
} }
@ -103,12 +104,12 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(
KeysBackupState.ReadyToBackUp -> { KeysBackupState.ReadyToBackUp -> {
genericItem { genericItem {
id("summary") id("summary")
title(host.stringProvider.getString(R.string.keys_backup_settings_status_ok)) title(host.stringProvider.getString(R.string.keys_backup_settings_status_ok).toEpoxyCharSequence())
style(ItemStyle.BIG_TEXT) style(ItemStyle.BIG_TEXT)
if (data.keysBackupVersionTrust()?.usable == false) { if (data.keysBackupVersionTrust()?.usable == false) {
description(host.stringProvider.getString(R.string.keys_backup_settings_untrusted_backup)) description(host.stringProvider.getString(R.string.keys_backup_settings_untrusted_backup).toEpoxyCharSequence())
} else { } else {
description(host.stringProvider.getString(R.string.keys_backup_info_keys_all_backup_up)) description(host.stringProvider.getString(R.string.keys_backup_info_keys_all_backup_up).toEpoxyCharSequence())
} }
endIconResourceId(R.drawable.unit_test_ok) endIconResourceId(R.drawable.unit_test_ok)
} }
@ -119,7 +120,7 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(
KeysBackupState.BackingUp -> { KeysBackupState.BackingUp -> {
genericItem { genericItem {
id("summary") id("summary")
title(host.stringProvider.getString(R.string.keys_backup_settings_status_ok)) title(host.stringProvider.getString(R.string.keys_backup_settings_status_ok).toEpoxyCharSequence())
style(ItemStyle.BIG_TEXT) style(ItemStyle.BIG_TEXT)
hasIndeterminateProcess(true) hasIndeterminateProcess(true)
@ -129,10 +130,11 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(
val remainingKeysToBackup = totalKeys - backedUpKeys val remainingKeysToBackup = totalKeys - backedUpKeys
if (data.keysBackupVersionTrust()?.usable == false) { if (data.keysBackupVersionTrust()?.usable == false) {
description(host.stringProvider.getString(R.string.keys_backup_settings_untrusted_backup)) description(host.stringProvider.getString(R.string.keys_backup_settings_untrusted_backup).toEpoxyCharSequence())
} else { } else {
description(host.stringProvider description(host.stringProvider
.getQuantityString(R.plurals.keys_backup_info_keys_backing_up, remainingKeysToBackup, remainingKeysToBackup)) .getQuantityString(R.plurals.keys_backup_info_keys_backing_up, remainingKeysToBackup, remainingKeysToBackup)
.toEpoxyCharSequence())
} }
} }
@ -144,14 +146,14 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(
// Add infos // Add infos
genericItem { genericItem {
id("version") id("version")
title(host.stringProvider.getString(R.string.keys_backup_info_title_version)) title(host.stringProvider.getString(R.string.keys_backup_info_title_version).toEpoxyCharSequence())
description(keyVersionResult?.version ?: "") description(keyVersionResult?.version.orEmpty().toEpoxyCharSequence())
} }
genericItem { genericItem {
id("algorithm") id("algorithm")
title(host.stringProvider.getString(R.string.keys_backup_info_title_algorithm)) title(host.stringProvider.getString(R.string.keys_backup_info_title_algorithm).toEpoxyCharSequence())
description(keyVersionResult?.algorithm ?: "") description(keyVersionResult?.algorithm.orEmpty().toEpoxyCharSequence())
} }
if (vectorPreferences.developerMode()) { if (vectorPreferences.developerMode()) {
@ -189,7 +191,7 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(
keysVersionTrust().signatures.forEach { keysVersionTrust().signatures.forEach {
genericItem { genericItem {
id(UUID.randomUUID().toString()) id(UUID.randomUUID().toString())
title(host.stringProvider.getString(R.string.keys_backup_info_title_signature)) title(host.stringProvider.getString(R.string.keys_backup_info_title_signature).toEpoxyCharSequence())
val isDeviceKnown = it.device != null val isDeviceKnown = it.device != null
val isDeviceVerified = it.device?.isVerified ?: false val isDeviceVerified = it.device?.isVerified ?: false
@ -197,21 +199,27 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(
val deviceId: String = it.deviceId ?: "" val deviceId: String = it.deviceId ?: ""
if (!isDeviceKnown) { if (!isDeviceKnown) {
description(host.stringProvider.getString(R.string.keys_backup_settings_signature_from_unknown_device, deviceId)) description(host.stringProvider
.getString(R.string.keys_backup_settings_signature_from_unknown_device, deviceId)
.toEpoxyCharSequence())
endIconResourceId(R.drawable.e2e_warning) endIconResourceId(R.drawable.e2e_warning)
} else { } else {
if (isSignatureValid) { if (isSignatureValid) {
if (host.session.sessionParams.deviceId == it.deviceId) { if (host.session.sessionParams.deviceId == it.deviceId) {
description(host.stringProvider.getString(R.string.keys_backup_settings_valid_signature_from_this_device)) description(host.stringProvider
.getString(R.string.keys_backup_settings_valid_signature_from_this_device)
.toEpoxyCharSequence())
endIconResourceId(R.drawable.e2e_verified) endIconResourceId(R.drawable.e2e_verified)
} else { } else {
if (isDeviceVerified) { if (isDeviceVerified) {
description(host.stringProvider description(host.stringProvider
.getString(R.string.keys_backup_settings_valid_signature_from_verified_device, deviceId)) .getString(R.string.keys_backup_settings_valid_signature_from_verified_device, deviceId)
.toEpoxyCharSequence())
endIconResourceId(R.drawable.e2e_verified) endIconResourceId(R.drawable.e2e_verified)
} else { } else {
description(host.stringProvider description(host.stringProvider
.getString(R.string.keys_backup_settings_valid_signature_from_unverified_device, deviceId)) .getString(R.string.keys_backup_settings_valid_signature_from_unverified_device, deviceId)
.toEpoxyCharSequence())
endIconResourceId(R.drawable.e2e_warning) endIconResourceId(R.drawable.e2e_warning)
} }
} }
@ -220,10 +228,12 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(
endIconResourceId(R.drawable.e2e_warning) endIconResourceId(R.drawable.e2e_warning)
if (isDeviceVerified) { if (isDeviceVerified) {
description(host.stringProvider description(host.stringProvider
.getString(R.string.keys_backup_settings_invalid_signature_from_verified_device, deviceId)) .getString(R.string.keys_backup_settings_invalid_signature_from_verified_device, deviceId)
.toEpoxyCharSequence())
} else { } else {
description(host.stringProvider description(host.stringProvider
.getString(R.string.keys_backup_settings_invalid_signature_from_unverified_device, deviceId)) .getString(R.string.keys_backup_settings_invalid_signature_from_unverified_device, deviceId)
.toEpoxyCharSequence())
} }
} }
} }

View File

@ -177,8 +177,8 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this)
.setTitle(R.string.keys_backup_setup_skip_title) .setTitle(R.string.keys_backup_setup_skip_title)
.setMessage(R.string.keys_backup_setup_skip_msg) .setMessage(R.string.keys_backup_setup_skip_msg)
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.action_cancel, null)
.setPositiveButton(R.string.leave) { _, _ -> .setPositiveButton(R.string.action_leave) { _, _ ->
finish() finish()
} }
.show() .show()

View File

@ -119,7 +119,7 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetBoot
.setTitle(R.string.are_you_sure) .setTitle(R.string.are_you_sure)
.setMessage(R.string.bootstrap_cancel_text) .setMessage(R.string.bootstrap_cancel_text)
.setPositiveButton(R.string._continue, null) .setPositiveButton(R.string._continue, null)
.setNegativeButton(R.string.skip) { _, _ -> .setNegativeButton(R.string.action_skip) { _, _ ->
bottomSheetResult = ResultListener.RESULT_CANCEL bottomSheetResult = ResultListener.RESULT_CANCEL
dismiss() dismiss()
} }

View File

@ -92,7 +92,7 @@ class IncomingVerificationRequestHandler @Inject constructor(
tx.cancel() tx.cancel()
} }
addButton( addButton(
context.getString(R.string.ignore), context.getString(R.string.action_ignore),
{ tx.cancel() } { tx.cancel() }
) )
addButton( addButton(

View File

@ -20,6 +20,8 @@ import androidx.core.text.toSpannable
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.bottomSheetDividerItem import im.vector.app.core.epoxy.bottomSheetDividerItem
import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.colorizeMatchingText import im.vector.app.core.utils.colorizeMatchingText
@ -49,12 +51,12 @@ class VerificationCancelController @Inject constructor(
if (state.currentDeviceCanCrossSign) { if (state.currentDeviceCanCrossSign) {
bottomSheetVerificationNoticeItem { bottomSheetVerificationNoticeItem {
id("notice") id("notice")
notice(host.stringProvider.getString(R.string.verify_cancel_self_verification_from_trusted)) notice(host.stringProvider.getString(R.string.verify_cancel_self_verification_from_trusted).toEpoxyCharSequence())
} }
} else { } else {
bottomSheetVerificationNoticeItem { bottomSheetVerificationNoticeItem {
id("notice") id("notice")
notice(host.stringProvider.getString(R.string.verify_cancel_self_verification_from_untrusted)) notice(host.stringProvider.getString(R.string.verify_cancel_self_verification_from_untrusted).toEpoxyCharSequence())
} }
} }
} else { } else {
@ -63,9 +65,11 @@ class VerificationCancelController @Inject constructor(
bottomSheetVerificationNoticeItem { bottomSheetVerificationNoticeItem {
id("notice") id("notice")
notice( notice(
host.stringProvider.getString(R.string.verify_cancel_other, otherDisplayName, otherUserID) EpoxyCharSequence(
.toSpannable() host.stringProvider.getString(R.string.verify_cancel_other, otherDisplayName, otherUserID)
.colorizeMatchingText(otherUserID, host.colorProvider.getColorFromAttribute(R.attr.vctr_notice_text_color)) .toSpannable()
.colorizeMatchingText(otherUserID, host.colorProvider.getColorFromAttribute(R.attr.vctr_notice_text_color))
)
) )
} }
} }
@ -76,7 +80,7 @@ class VerificationCancelController @Inject constructor(
bottomSheetVerificationActionItem { bottomSheetVerificationActionItem {
id("cancel") id("cancel")
title(host.stringProvider.getString(R.string.skip)) title(host.stringProvider.getString(R.string.action_skip))
titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorError)) titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorError))
iconRes(R.drawable.ic_arrow_right) iconRes(R.drawable.ic_arrow_right)
iconColor(host.colorProvider.getColorFromAttribute(R.attr.colorError)) iconColor(host.colorProvider.getColorFromAttribute(R.attr.colorError))

View File

@ -19,6 +19,7 @@ package im.vector.app.features.crypto.verification.cancel
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.bottomSheetDividerItem import im.vector.app.core.epoxy.bottomSheetDividerItem
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.crypto.verification.VerificationBottomSheetViewState import im.vector.app.features.crypto.verification.VerificationBottomSheetViewState
@ -46,7 +47,7 @@ class VerificationNotMeController @Inject constructor(
val host = this val host = this
bottomSheetVerificationNoticeItem { bottomSheetVerificationNoticeItem {
id("notice") id("notice")
notice(host.eventHtmlRenderer.render(host.stringProvider.getString(R.string.verify_not_me_self_verification))) notice(host.eventHtmlRenderer.render(host.stringProvider.getString(R.string.verify_not_me_self_verification)).toEpoxyCharSequence())
} }
bottomSheetDividerItem { bottomSheetDividerItem {
@ -55,7 +56,7 @@ class VerificationNotMeController @Inject constructor(
bottomSheetVerificationActionItem { bottomSheetVerificationActionItem {
id("skip") id("skip")
title(host.stringProvider.getString(R.string.skip)) title(host.stringProvider.getString(R.string.action_skip))
titleColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) titleColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
iconRes(R.drawable.ic_arrow_right) iconRes(R.drawable.ic_arrow_right)
iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))

Some files were not shown because too many files have changed in this diff Show More