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:
issues:
types: [ opened ]
types: [opened]
jobs:
automate-project-columns:
runs-on: ubuntu-latest
if: |
github.repository == 'vector-im/element-android' # Skip in forks
steps:
- uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488
with:

View file

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

View file

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

View file

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

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
2. Umožňuje komunikovať s kýmkoľvek v sieti Matrix a vďaka integráciám aj s rôznymi inými aplikáciami ako napríklad Slack
3. Chráni vás pred reklamami, zhromažďovaním údajov a uzavretými platformami
4. Posilňuje vašu bezpečnosť vďaka E2E šifrovaniu a krížovému podpisovaniu určenému na overovanie ostatných
<b>Funkcie aplikácie Element zahŕňajú:</b>
- Pokročilé nástroje na online komunikáciu
- Plne šifrované správy umožňujúce bezpečnejšiu firemnú komunikáciu aj pre pracovníkov na diaľku
- Decentralizované konverzácie založené na open source frameworku Matrix
- Bezpečné zdieľanie súborov so šifrovanými údajmi pri správe projektov
- Videochaty s funkciou Voice over IP a zdieľaním obrazovky
- Jednoduchá integrácia s obľúbenými nástrojmi na online spoluprácu, nástrojmi na riadenie projektov, službami VoIP a inými aplikáciami na tímovú komunikáciu
Element sa od ostatných komunikačných a kolaboračných aplikácií odlišuje tým, že je decentralizovaný a open-source.
Element sa úplne líši od ostatných aplikácií na zasielanie správ a spoluprácu. Funguje na Matrixe, otvorenej sieti na bezpečné posielanie správ a decentralizovanú komunikáciu. Umožňuje vlastný hosting, aby používatelia získali maximálne vlastníctvo a kontrolu nad svojimi údajmi a správami.
S Elementom sa môžete pripojiť k vlastnému serveru alebo si môžete vybrať server s dôveryhodným poskytovateľom, čím si zachováte súkromie, vlastníctvo a kontrolu nad vašimi konverzáciami a údajmi. Získate tak prístup do otvorenej siete a teda nie ste limitovaní na komunikáciu len s ostatnými Element používateľmi. A samozrejme je vaša komunikácia dobre zabezpečná.
<b>Súkromie a šifrovanie správ</b>
Element vás chráni pred nežiaducimi reklamami, ťažbou údajov a tzv. walled gardens. Zabezpečuje tiež všetky vaše údaje, video a hlasovú komunikáciu jeden na jedného prostredníctvom end-to-end šifrovania a overovania zariadení krížovým podpisovaním
Element vám poskytuje kontrolu nad vaším súkromím a zároveň vám umožňuje bezpečne komunikovať s kýmkoľvek v sieti Matrix alebo s inými nástrojmi na podnikovú spoluprácu vďaka integrácii s aplikáciami, ako je napríklad Slack.
Element všetko toto dokáže vďaka tomu, že pracuje podľa protokolu Matrix - štandardu na otvorenú, decentralizovanú komunikáciu.
<b>Element môže byť na vašom vlastnom serveri</b>.
Aby ste mali väčšiu kontrolu nad svojimi citlivými údajmi a konverzáciami, Element môže byť na vašom vlastnom serveri alebo si môžete vybrať ľubovoľný hosting založený na systéme Matrix - štandarde pre decentralizovanú komunikáciu s otvoreným zdrojovým kódom. Element vám poskytuje súkromie, súlad s bezpečnostnými predpismi a flexibilitu integrácie.
Element vám dáva kontrolu tým, že si samy vyberiete, ako budete spravovať (ang. host) vaše konverzácie. Priamo v aplikácii Element si môžete vybrať z rôznych spôsobov hostovania:
<b>Vlastnite svoje údaje</b>
Vy rozhodujete o tom, kde budú vaše údaje a správy uložené. Bez rizika ťažby údajov alebo prístupu tretích strán.
1. Získajte účet zdarma na verejnom servery matrix.org od vývojárov protokolu Matrix alebo si vyberte z tísíce iných serverov hostovaných dobrovoľníkmi
2. Hostujte si účet spustením vlastného servera použitím vlastného hardvéru
3. Prihláste sa k účtu na vlastnom servery objednaním služieb na platforme Element Matrix Services
Element vám dáva kontrolu rôznymi spôsobmi:
1. Získajte bezplatné konto na verejnom serveri matrix.org, ktorý hostia vývojári Matrixu, alebo si vyberte z tisícov verejných serverov, ktoré hostia dobrovoľníci.
2. Vlastný hosting účtu spustením servera na vlastnej IT infraštruktúre.
3. Zaregistrujte si účet na vlastnom serveri tak, že si jednoducho predplatíte hostingovú platformu Element Matrix Services.
<b>Prečo si vybrať Element?</b>
<b>Otvorené zasielanie správ a spolupráca</b>
Môžete komunikovať s kýmkoľvek v sieti Matrix, či už používa aplikáciu Element, inú aplikáciu Matrix alebo dokonca ak používa inú aplikáciu na zasielanie správ.
<b>PONECHAJTE SI VAŠE ÚDAJE</b>: Len vy rozhodujete o tom, kde si budete uchovávať vaše správy a ostatné údaje. Len vy vlastníte vaše údaje a riadite zaobchádzanie s nimi, nie nejaká megakorporácia, ktorá z nich ťaží alebo ich poskytuje tretím stranám.
<b>Vynikajúce zabezpečenie</b>
Skutočné end-to-end šifrovanie (správy môžu dešifrovať len účastníci konverzácie) a krížové overovanie zariadení.
<b>OTVORENÁ KOMUNIKÁCIA a KOLABORÁCIA</b>: Konverzovať môžete s kýmkoľvek v otvorenej sieti Matrix nezávisle na tom, či používa Element, inú kompatibilnú aplikáciu, ba dokkonca aj s tými, ktorí používajú úplne inú platformu určenú na okamžitú komunikáciu ako sú Slack, IRC alebo XMPP.
<b>Kompletná komunikácia a integrácia</b>
Správy, hlasové a video hovory, zdieľanie súborov, zdieľanie obrazovky a celý rad integrácií, botov a widgetov. Vytvárajte miestnosti, komunity, zostaňte v kontakte a vybavujte veci.
<b>VEĽMI VYSOKÉ ZABEZPEČENIE</b>: Skutočné šifrovanie od zariadenia k zariadeniu (len diskutujúci môžu dešifrovať správy) a krížové podpisovanie určené na overovanie jednotlivých zariadení členov konverzácií.
<b>Nadviažte tam, kde ste skončili</b>
Buďte v kontakte, nech ste kdekoľvek, vďaka plne synchronizovanej histórii správ vo všetkých zariadeniach a na webe na adrese https://app.element.io.
<b>KOMPLETNÁ KOMUNIKÁCIA</b>: Okamžité správy, telefonáty a video hovory, zdieľanie súborov, zdieľanie obrazovky a veľké množstvo integrácií, botov a widgetov. Vytvorte si vlastné miestnosti, založte komunity, ostante v kontakte a vyriešte problémy.
<b>KDEKOĽVEK SA NACHÁDZATE</b>: Ostante v kontakte kdekoľvek ste s plne synchronizovanou históriou konverzácií naprieč všetkými vašimi zariadeniami a aj cez web na adrese https://app.element.io.
<b>Otvorený zdroj</b>
Element Android je projekt s otvoreným zdrojovým kódom, ktorého hostiteľom je GitHub. Nahlasujte chyby a/alebo prispievajte k jeho vývoju na adrese https://github.com/vector-im/element-android.

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
*/
fun sendTextMessage(room: Room, message: String, nbOfMessages: Int, timeout: Long = TestConstants.timeOutMillis): List<TimelineEvent> {
val sentEvents = ArrayList<TimelineEvent>(nbOfMessages)
val timeline = room.createTimeline(null, TimelineSettings(10))
timeline.start()
waitWithLatch(timeout + 1_000L * nbOfMessages) { latch ->
val timelineListener = object : Timeline.Listener {
override fun onTimelineFailure(throwable: Throwable) {
}
override fun onNewTimelineEvents(eventIds: List<String>) {
// noop
}
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
val newMessages = snapshot
.filter { it.root.sendState == SendState.SYNCED }
.filter { it.root.getClearType() == EventType.MESSAGE }
.filter { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(message) == true }
Timber.v("New synced message size: ${newMessages.size}")
if (newMessages.size == nbOfMessages) {
sentEvents.addAll(newMessages)
// Remove listener now, if not at the next update sendEvents could change
timeline.removeListener(this)
latch.countDown()
}
}
}
timeline.addListener(timelineListener)
sendTextMessagesBatched(room, message, nbOfMessages)
}
val sentEvents = sendTextMessagesBatched(timeline, room, message, nbOfMessages, timeout)
timeline.dispose()
// Check that all events has been created
assertEquals("Message number do not match $sentEvents", nbOfMessages.toLong(), sentEvents.size.toLong())
@ -182,9 +155,10 @@ class CommonTestHelper(context: Context) {
}
/**
* Will send nb of messages provided by count parameter but waits a bit every 10 messages to avoid gap in sync
* Will send nb of messages provided by count parameter but waits every 10 messages to avoid gap in sync
*/
private fun sendTextMessagesBatched(room: Room, message: String, count: Int) {
private fun sendTextMessagesBatched(timeline: Timeline, room: Room, message: String, count: Int, timeout: Long): List<TimelineEvent> {
val sentEvents = ArrayList<TimelineEvent>(count)
(1 until count + 1)
.map { "$message #$it" }
.chunked(10)
@ -192,8 +166,34 @@ class CommonTestHelper(context: Context) {
batchedMessages.forEach { formattedMessage ->
room.sendTextMessage(formattedMessage)
}
Thread.sleep(1_000L)
waitWithLatch(timeout) { latch ->
val timelineListener = object : Timeline.Listener {
override fun onTimelineUpdated(snapshot: List<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 *****************************************************************************
@ -332,13 +332,6 @@ class CommonTestHelper(context: Context) {
fun createEventListener(latch: CountDownLatch, predicate: (List<TimelineEvent>) -> Boolean): Timeline.Listener {
return object : Timeline.Listener {
override fun onTimelineFailure(throwable: Throwable) {
// noop
}
override fun onNewTimelineEvents(eventIds: List<String>) {
// noop
}
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
if (predicate(snapshot)) {

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
import kotlinx.coroutines.runBlocking
import org.amshove.kluent.internal.assertEquals
import org.amshove.kluent.shouldBeFalse
import org.amshove.kluent.shouldBeTrue
import org.junit.FixMethodOrder
@ -123,54 +125,29 @@ class TimelineForwardPaginationTest : InstrumentedTest {
// Alice paginates BACKWARD and FORWARD of 50 events each
// Then she can only navigate FORWARD
run {
val lock = CountDownLatch(1)
val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
Timber.e("Alice timeline updated: with ${snapshot.size} events:")
snapshot.forEach {
Timber.w(" event ${it.root.content}")
}
// Alice can see the first event of the room (so Back pagination has worked)
snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE &&
// 6 for room creation item (backward pagination), 1 for the context, and 50 for the forward pagination
snapshot.size == 57 // 6 + 1 + 50
val snapshot = runBlocking {
aliceTimeline.awaitPaginate(Timeline.Direction.BACKWARDS, 50)
aliceTimeline.awaitPaginate(Timeline.Direction.FORWARDS, 50)
}
aliceTimeline.addListener(aliceEventsListener)
// Restart the timeline to the first sent event
// We ask to load event backward and forward
aliceTimeline.paginate(Timeline.Direction.BACKWARDS, 50)
aliceTimeline.paginate(Timeline.Direction.FORWARDS, 50)
commonTestHelper.await(lock)
aliceTimeline.removeAllListeners()
aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue()
aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
assertEquals(EventType.STATE_ROOM_CREATE, snapshot.lastOrNull()?.root?.getClearType())
// 6 for room creation item (backward pagination), 1 for the context, and 50 for the forward pagination
// 6 + 1 + 50
assertEquals(57, snapshot.size)
}
// Alice paginates once again FORWARD for 50 events
// All the timeline is retrieved, she cannot paginate anymore in both direction
run {
val lock = CountDownLatch(1)
val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
Timber.e("Alice timeline updated: with ${snapshot.size} events:")
snapshot.forEach {
Timber.w(" event ${it.root.content}")
}
// 6 for room creation item (backward pagination),and numberOfMessagesToSend (all the message of the room)
snapshot.size == 6 + numberOfMessagesToSend &&
snapshot.checkSendOrder(message, numberOfMessagesToSend, 0)
}
aliceTimeline.addListener(aliceEventsListener)
// Ask for a forward pagination
aliceTimeline.paginate(Timeline.Direction.FORWARDS, 50)
commonTestHelper.await(lock)
aliceTimeline.removeAllListeners()
val snapshot = runBlocking {
aliceTimeline.awaitPaginate(Timeline.Direction.FORWARDS, 50)
}
// 6 for room creation item (backward pagination),and numberOfMessagesToSend (all the message of the room)
snapshot.size == 6 + numberOfMessagesToSend &&
snapshot.checkSendOrder(message, numberOfMessagesToSend, 0)
// The timeline is fully loaded
aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()

View file

@ -168,10 +168,8 @@ class TimelinePreviousLastForwardTest : InstrumentedTest {
bobTimeline.addListener(eventsListener)
// Restart the timeline to the first sent event, and paginate in both direction
// Restart the timeline to the first sent event
bobTimeline.restartWithEventId(firstMessageFromAliceId)
bobTimeline.paginate(Timeline.Direction.BACKWARDS, 50)
bobTimeline.paginate(Timeline.Direction.FORWARDS, 50)
commonTestHelper.await(lock)
bobTimeline.removeAllListeners()

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)
/**
* Returns the number of sending events
* This is the same than the regular paginate method but waits for the results instead
* of relying on the timeline listener.
*/
fun pendingEventCount(): Int
/**
* Returns the number of failed sending events.
*/
fun failedToDeliverEventCount(): Int
suspend fun awaitPaginate(direction: Direction, count: Int): List<TimelineEvent>
/**
* Returns the index of a built event or null.
@ -86,14 +82,14 @@ interface Timeline {
fun getIndexOfEvent(eventId: String?): Int?
/**
* Returns the built [TimelineEvent] at index or null
* Returns the current pagination state for the direction.
*/
fun getTimelineEventAtIndex(index: Int): TimelineEvent?
fun getPaginationState(direction: Direction): PaginationState
/**
* Returns the built [TimelineEvent] with eventId or null
* Returns a snapshot of the timeline in his current state.
*/
fun getTimelineEventWithId(eventId: String?): TimelineEvent?
fun getSnapshot(): List<TimelineEvent>
interface Listener {
/**
@ -101,19 +97,33 @@ interface Timeline {
* The latest event is the first in the list
* @param snapshot the most up to date snapshot
*/
fun onTimelineUpdated(snapshot: List<TimelineEvent>)
fun onTimelineUpdated(snapshot: List<TimelineEvent>) = Unit
/**
* Called whenever an error we can't recover from occurred
*/
fun onTimelineFailure(throwable: Throwable)
fun onTimelineFailure(throwable: Throwable) = Unit
/**
* Called when new events come through the sync
*/
fun onNewTimelineEvents(eventIds: List<String>)
fun onNewTimelineEvents(eventIds: List<String>) = Unit
/**
* Called when the pagination state has changed in one direction
*/
fun onStateUpdated(direction: Direction, state: PaginationState) = Unit
}
/**
* Pagination state
*/
data class PaginationState(
val hasMoreToLoad: Boolean = true,
val loading: Boolean = false,
val inError: Boolean = false
)
/**
* This is used to paginate in one or another direction.
*/

View file

@ -47,6 +47,10 @@ data class TimelineEvent(
*/
val localId: Long,
val eventId: String,
/**
* This display index is the position in the current chunk.
* It's not unique on the timeline as it's reset on each chunk.
*/
val displayIndex: Int,
val senderInfo: SenderInfo,
val annotations: EventAnnotationsSummary? = null,

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.create.RoomCreateContent
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.EditAggregatedSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.EditionOfEventFields
@ -54,7 +55,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
) : RealmMigration {
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 <= 17) migrateTo18(realm)
if (oldVersion <= 18) migrateTo19(realm)
if (oldVersion <= 19) migrateTo20(realm)
}
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
}
}
numberOfTimelineEvents++
// numberOfTimelineEvents++
timelineEvents.add(timelineEventEntity)
}
@ -191,3 +191,29 @@ internal fun ChunkEntity.nextDisplayIndex(direction: PaginationDirection): Int {
}
}
}
internal fun ChunkEntity.doesNextChunksVerifyCondition(linkCondition: (ChunkEntity) -> Boolean): Boolean {
var nextChunkToCheck = this.nextChunk
while (nextChunkToCheck != null) {
if (linkCondition(nextChunkToCheck)) {
return true
}
nextChunkToCheck = nextChunkToCheck.nextChunk
}
return false
}
internal fun ChunkEntity.isMoreRecentThan(chunkToCheck: ChunkEntity): Boolean {
if (this.isLastForward) return true
if (chunkToCheck.isLastForward) return false
// Check if the chunk to check is linked to this one
if (chunkToCheck.doesNextChunksVerifyCondition { it == this }) {
return true
}
// Otherwise check if this chunk is linked to last forward
if (this.doesNextChunksVerifyCondition { it.isLastForward }) {
return true
}
// We don't know, so we assume it's false
return false
}

View file

@ -28,3 +28,13 @@ internal fun TimelineEventEntity.Companion.nextId(realm: Realm): Long {
currentIdNum.toLong() + 1
}
}
internal fun TimelineEventEntity.isMoreRecentThan(eventToCheck: TimelineEventEntity): Boolean {
val currentChunk = this.chunk?.first() ?: return false
val chunkToCheck = eventToCheck.chunk?.firstOrNull() ?: return false
return if (currentChunk == chunkToCheck) {
this.displayIndex >= eventToCheck.displayIndex
} else {
currentChunk.isMoreRecentThan(chunkToCheck)
}
}

View file

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

View file

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

View file

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

View file

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

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

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

View file

@ -136,7 +136,7 @@ internal class RoomSummaryUpdater @Inject constructor(
roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 ||
// avoid this call if we are sure there are unread events
!isEventRead(realm.configuration, userId, roomId, latestPreviewableEvent?.eventId)
latestPreviewableEvent?.let { !isEventRead(realm.configuration, userId, roomId, it.eventId) } ?: false
roomSummaryEntity.setDisplayName(roomDisplayNameResolver.resolve(realm, roomId))
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(realm, roomId)

View file

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

View file

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

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
internal class TimelineInput @Inject constructor() {
val listeners = mutableSetOf<Listener>()
fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) {
listeners.toSet().forEach { it.onLocalEchoCreated(roomId, timelineEvent) }
}
@ -35,8 +38,6 @@ internal class TimelineInput @Inject constructor() {
listeners.toSet().forEach { it.onNewTimelineEvents(roomId, eventIds) }
}
val listeners = mutableSetOf<Listener>()
internal interface Listener {
fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) = Unit
fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState) = Unit

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.addStateEvent
import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
import org.matrix.android.sdk.internal.database.helper.merge
import org.matrix.android.sdk.internal.database.mapper.toEntity
import org.matrix.android.sdk.internal.database.model.ChunkEntity
import org.matrix.android.sdk.internal.database.model.EventInsertType
import org.matrix.android.sdk.internal.database.model.RoomEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.deleteOnCascade
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
import org.matrix.android.sdk.internal.database.query.create
import org.matrix.android.sdk.internal.database.query.find
import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents
import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryEventsHelper
import org.matrix.android.sdk.internal.util.awaitTransaction
import timber.log.Timber
import javax.inject.Inject
/**
* Insert Chunk in DB, and eventually merge with existing chunk event
* Insert Chunk in DB, and eventually link next and previous chunk in db.
*/
internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase private val monarchy: Monarchy) {
/**
* <pre>
* ========================================================================================================
* | Backward case |
* ========================================================================================================
*
* *--------------------------* *--------------------------*
* | startToken1 | | startToken1 |
* *--------------------------* *--------------------------*
* | | | |
* | | | |
* | receivedChunk backward | | |
* | Events | | |
* | | | |
* | | | |
* | | | |
* *--------------------------* *--------------------------* | |
* | startToken0 | | endToken1 | => | Merged chunk |
* *--------------------------* *--------------------------* | Events |
* | | | |
* | | | |
* | Current Chunk | | |
* | Events | | |
* | | | |
* | | | |
* | | | |
* *--------------------------* *--------------------------*
* | endToken0 | | endToken0 |
* *--------------------------* *--------------------------*
*
*
* ========================================================================================================
* | Forward case |
* ========================================================================================================
*
* *--------------------------* *--------------------------*
* | startToken0 | | startToken0 |
* *--------------------------* *--------------------------*
* | | | |
* | | | |
* | Current Chunk | | |
* | Events | | |
* | | | |
* | | | |
* | | | |
* *--------------------------* *--------------------------* | |
* | endToken0 | | startToken1 | => | Merged chunk |
* *--------------------------* *--------------------------* | Events |
* | | | |
* | | | |
* | receivedChunk forward | | |
* | Events | | |
* | | | |
* | | | |
* | | | |
* *--------------------------* *--------------------------*
* | endToken1 | | endToken1 |
* *--------------------------* *--------------------------*
*
* ========================================================================================================
* </pre>
*/
enum class Result {
SHOULD_FETCH_MORE,
REACHED_END,
@ -136,21 +67,21 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
prevToken = receivedChunk.end
}
val existingChunk = ChunkEntity.find(realm, roomId, prevToken = prevToken, nextToken = nextToken)
if (existingChunk != null) {
Timber.v("This chunk is already in the db, returns")
return@awaitTransaction
}
val prevChunk = ChunkEntity.find(realm, roomId, nextToken = prevToken)
val nextChunk = ChunkEntity.find(realm, roomId, prevToken = nextToken)
// The current chunk is the one we will keep all along the merge processChanges.
// We try to look for a chunk next to the token,
// otherwise we create a whole new one which is unlinked (not live)
val currentChunk = if (direction == PaginationDirection.FORWARDS) {
prevChunk?.apply { this.nextToken = nextToken }
} else {
nextChunk?.apply { this.prevToken = prevToken }
val currentChunk = ChunkEntity.create(realm, prevToken = prevToken, nextToken = nextToken).apply {
this.nextChunk = nextChunk
this.prevChunk = prevChunk
}
?: ChunkEntity.create(realm, prevToken, nextToken)
if (receivedChunk.events.isNullOrEmpty() && !receivedChunk.hasMore()) {
handleReachEnd(realm, roomId, direction, currentChunk)
nextChunk?.prevChunk = currentChunk
prevChunk?.nextChunk = currentChunk
if (receivedChunk.events.isEmpty() && !receivedChunk.hasMore()) {
handleReachEnd(roomId, direction, currentChunk)
} else {
handlePagination(realm, roomId, direction, receivedChunk, currentChunk)
}
@ -166,17 +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")
if (direction == PaginationDirection.FORWARDS) {
val currentLastForwardChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)
if (currentChunk != currentLastForwardChunk) {
currentChunk.isLastForward = true
currentLastForwardChunk?.deleteOnCascade(deleteStateEvents = false, canDeleteRoot = false)
RoomSummaryEntity.where(realm, roomId).findFirst()?.apply {
latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
}
}
Timber.v("We should keep the lastForward chunk unique, the one from sync")
} else {
currentChunk.isLastBackward = true
}
@ -204,44 +128,50 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
roomMemberContentsByUser[stateEvent.stateKey] = stateEvent.content.toModel<RoomMemberContent>()
}
}
val eventIds = ArrayList<String>(eventList.size)
eventList.forEach { event ->
if (event.eventId == null || event.senderId == null) {
return@forEach
}
val ageLocalTs = event.unsignedData?.age?.let { now - it }
eventIds.add(event.eventId)
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null) {
val contentToUse = if (direction == PaginationDirection.BACKWARDS) {
event.prevContent
} else {
event.content
run processTimelineEvents@{
eventList.forEach { event ->
if (event.eventId == null || event.senderId == null) {
return@forEach
}
roomMemberContentsByUser[event.stateKey] = contentToUse.toModel<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) {
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.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import timber.log.Timber
import java.util.Collections
internal class UIEchoManager(
private val settings: TimelineSettings,
private val listener: Listener
) {
internal class UIEchoManager(private val listener: Listener) {
interface Listener {
fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?): Boolean
@ -70,13 +66,12 @@ internal class UIEchoManager(
return existingState != sendState
}
fun onLocalEchoCreated(timelineEvent: TimelineEvent) {
// Manage some ui echos (do it before filter because actual event could be filtered out)
fun onLocalEchoCreated(timelineEvent: TimelineEvent): Boolean {
when (timelineEvent.root.getClearType()) {
EventType.REDACTION -> {
}
EventType.REACTION -> {
val content = timelineEvent.root.content?.toModel<ReactionContent>()
val content: ReactionContent? = timelineEvent.root.content?.toModel<ReactionContent>()
if (RelationType.ANNOTATION == content?.relatesTo?.type) {
val reaction = content.relatesTo.key
val relatedEventID = content.relatesTo.eventId
@ -96,11 +91,12 @@ internal class UIEchoManager(
}
Timber.v("On local echo created: ${timelineEvent.eventId}")
inMemorySendingEvents.add(0, timelineEvent)
return true
}
fun decorateEventWithReactionUiEcho(timelineEvent: TimelineEvent): TimelineEvent? {
fun decorateEventWithReactionUiEcho(timelineEvent: TimelineEvent): TimelineEvent {
val relatedEventID = timelineEvent.eventId
val contents = inMemoryReactions[relatedEventID] ?: return null
val contents = inMemoryReactions[relatedEventID] ?: return timelineEvent
var existingAnnotationSummary = timelineEvent.annotations ?: EventAnnotationsSummary(
relatedEventID

View file

@ -345,15 +345,17 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
syncLocalTimestampMillis: Long,
aggregator: SyncResponsePostTreatmentAggregator): ChunkEntity {
val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId)
if (isLimited && lastChunk != null) {
lastChunk.deleteOnCascade(deleteStateEvents = true, canDeleteRoot = true)
}
val chunkEntity = if (!isLimited && lastChunk != null) {
lastChunk
} else {
realm.createObject<ChunkEntity>().apply { this.prevToken = prevToken }
realm.createObject<ChunkEntity>().apply {
this.prevToken = prevToken
this.isLastForward = true
}
}
// Only one chunk has isLastForward set to true
lastChunk?.isLastForward = false
chunkEntity.isLastForward = true
val eventIds = ArrayList<String>(eventList.size)
val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()

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

View file

@ -32,8 +32,6 @@
import static
### Rubbish from merge. Please delete those lines (sometimes in comment)
<<<<<<<
>>>>>>>
### carry return before "}". Please remove empty lines.
\n\s*\n\s*\}
@ -160,7 +158,7 @@ Formatter\.formatShortFileSize===1
# android\.text\.TextUtils
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt
enum class===116
enum class===117
### Do not import temporary legacy classes
import org.matrix.android.sdk.internal.legacy.riot===3

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,9 +17,6 @@
package im.vector.app.core.epoxy
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.VisibilityState
import kotlinx.coroutines.CoroutineScope
@ -30,24 +27,19 @@ import kotlinx.coroutines.cancelChildren
/**
* 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)
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
override fun getLifecycle() = lifecycleRegistry
private var onModelVisibilityStateChangedListener: OnVisibilityStateChangedListener? = null
@CallSuper
override fun bind(holder: H) {
super.bind(holder)
lifecycleRegistry.currentState = Lifecycle.State.STARTED
}
@CallSuper
override fun unbind(holder: H) {
lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
coroutineScope.coroutineContext.cancelChildren()
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.VectorEpoxyHolder
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.util.preventMutation
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.features.displayname.getBestName
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.tools.findPillsAndProcess
import im.vector.app.features.media.ImageContentRenderer
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.util.MatrixItem
/**
@ -50,13 +49,13 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
lateinit var matrixItem: MatrixItem
@EpoxyAttribute
lateinit var body: CharSequence
lateinit var body: EpoxyCharSequence
@EpoxyAttribute
var bindingOptions: BindingOptions? = null
@EpoxyAttribute
var bodyDetails: CharSequence? = null
var bodyDetails: EpoxyCharSequence? = null
@EpoxyAttribute
var imageContentRenderer: ImageContentRenderer? = null
@ -65,7 +64,7 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
var data: ImageContentRenderer.Data? = null
@EpoxyAttribute
var time: CharSequence? = null
var time: String? = null
@EpoxyAttribute
var movementMethod: MovementMethod? = null
@ -84,13 +83,9 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
}
holder.imagePreview.isVisible = data != null
holder.body.movementMethod = movementMethod
holder.body.text = if (bindingOptions?.preventMutation.orFalse()) {
body.preventMutation()
} else {
body
}
holder.bodyDetails.setTextOrHide(bodyDetails)
body.findPillsAndProcess(coroutineScope) { it.bind(holder.body) }
holder.body.text = body.charSequence
holder.bodyDetails.setTextOrHide(bodyDetails?.charSequence)
body.charSequence.findPillsAndProcess(coroutineScope) { it.bind(holder.body) }
holder.timestamp.setTextOrHide(time)
}

View file

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

View file

@ -36,7 +36,7 @@ abstract class BottomSheetSendStateItem : VectorEpoxyModel<BottomSheetSendStateI
var showProgress: Boolean = false
@EpoxyAttribute
lateinit var text: CharSequence
lateinit var text: String
@EpoxyAttribute
@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");
* you may not use this file except in compliance with the License.
@ -14,8 +14,9 @@
* limitations under the License.
*/
package im.vector.app.core.epoxy.util
package im.vector.app.core.epoxy.charsequence
import android.text.SpannableString
fun CharSequence?.preventMutation(): CharSequence? = this?.let { SpannableString(it) }
/**
* Extensions to wrap CharSequence to EpoxyCharSequence
*/
fun CharSequence.toEpoxyCharSequence() = EpoxyCharSequence(this)

View file

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

View file

@ -100,6 +100,10 @@ class DefaultErrorFormatter @Inject constructor(
throwable.error.code == MatrixError.M_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 -> {
throwable.error.message.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 connectedCallIds = mutableSetOf<String>()
lateinit var notificationManager: NotificationManagerCompat
private lateinit var notificationManager: NotificationManagerCompat
@Inject lateinit var notificationUtils: NotificationUtils
@Inject lateinit var callManager: WebRtcCallManager
@Inject lateinit var avatarRenderer: AvatarRenderer
@ -125,13 +125,6 @@ class CallService : VectorService() {
callRingPlayerOutgoing?.stop()
displayCallInProgressNotification(intent)
}
ACTION_CALL_CONNECTING -> {
// lower notification priority
displayCallInProgressNotification(intent)
// stop ringing
callRingPlayerIncoming?.stop()
callRingPlayerOutgoing?.stop()
}
ACTION_CALL_TERMINATED -> {
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_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_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_IS_IN_BG = "EXTRA_IS_IN_BG"
@ -351,7 +340,6 @@ class CallService : VectorService() {
action = ACTION_OUTGOING_RINGING_CALL
putExtra(EXTRA_CALL_ID, callId)
}
ContextCompat.startForegroundService(context, intent)
}
@ -362,11 +350,13 @@ class CallService : VectorService() {
action = ACTION_ONGOING_CALL
putExtra(EXTRA_CALL_ID, callId)
}
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)
.apply {
action = ACTION_CALL_TERMINATED

View file

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

View file

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

View file

@ -38,7 +38,7 @@ import im.vector.app.features.themes.ThemeUtils
abstract class GenericFooterItem : VectorEpoxyModel<GenericFooterItem.Holder>() {
@EpoxyAttribute
var text: CharSequence? = null
var text: String? = null
@EpoxyAttribute
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.VectorEpoxyHolder
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.extensions.setTextOrHide
@ -41,10 +42,10 @@ import im.vector.app.core.extensions.setTextOrHide
abstract class GenericItem : VectorEpoxyModel<GenericItem.Holder>() {
@EpoxyAttribute
var title: CharSequence? = null
var title: EpoxyCharSequence? = null
@EpoxyAttribute
var description: CharSequence? = null
var description: EpoxyCharSequence? = null
@EpoxyAttribute
var style: ItemStyle = ItemStyle.NORMAL_TEXT
@ -71,7 +72,7 @@ abstract class GenericItem : VectorEpoxyModel<GenericItem.Holder>() {
override fun bind(holder: Holder) {
super.bind(holder)
holder.titleText.setTextOrHide(title)
holder.titleText.setTextOrHide(title?.charSequence)
if (titleIconResourceId != -1) {
holder.titleIcon.setImageResource(titleIconResourceId)
@ -82,7 +83,7 @@ abstract class GenericItem : VectorEpoxyModel<GenericItem.Holder>() {
holder.titleText.textSize = style.toTextSize()
holder.descriptionText.setTextOrHide(description)
holder.descriptionText.setTextOrHide(description?.charSequence)
if (hasIndeterminateProcess) {
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.VectorEpoxyHolder
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.extensions.setTextOrHide
import im.vector.app.features.themes.ThemeUtils
@ -39,7 +40,7 @@ import im.vector.app.features.themes.ThemeUtils
abstract class GenericPillItem : VectorEpoxyModel<GenericPillItem.Holder>() {
@EpoxyAttribute
var text: CharSequence? = null
var text: EpoxyCharSequence? = null
@EpoxyAttribute
var style: ItemStyle = ItemStyle.NORMAL_TEXT
@ -60,7 +61,7 @@ abstract class GenericPillItem : VectorEpoxyModel<GenericPillItem.Holder>() {
override fun bind(holder: Holder) {
super.bind(holder)
holder.textView.setTextOrHide(text)
holder.textView.setTextOrHide(text?.charSequence)
holder.textView.typeface = style.toTypeFace()
holder.textView.textSize = style.toTextSize()
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.VectorEpoxyHolder
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.extensions.setTextOrHide
import im.vector.app.features.themes.ThemeUtils
@ -41,10 +42,10 @@ import im.vector.app.features.themes.ThemeUtils
abstract class GenericWithValueItem : VectorEpoxyModel<GenericWithValueItem.Holder>() {
@EpoxyAttribute
var title: CharSequence? = null
var title: EpoxyCharSequence? = null
@EpoxyAttribute
var value: CharSequence? = null
var value: String? = null
@EpoxyAttribute
@ColorInt
@ -62,7 +63,7 @@ abstract class GenericWithValueItem : VectorEpoxyModel<GenericWithValueItem.Hold
override fun bind(holder: Holder) {
super.bind(holder)
holder.titleText.setTextOrHide(title)
holder.titleText.setTextOrHide(title?.charSequence)
if (titleIconResourceId != -1) {
holder.titleIcon.setImageResource(titleIconResourceId)

View file

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

View file

@ -305,26 +305,28 @@ fun shareMedia(context: Context, file: File, mediaMimeType: String?) {
return
}
val sendIntent = ShareCompat.IntentBuilder(context)
val chooserIntent = ShareCompat.IntentBuilder(context)
.setType(mediaMimeType)
.setStream(mediaUri)
.getIntent()
.setChooserTitle(R.string.action_share)
.createChooserIntent()
sendShareIntent(context, sendIntent)
createChooser(context, chooserIntent)
}
fun shareText(context: Context, text: String) {
val sendIntent = Intent()
sendIntent.action = Intent.ACTION_SEND
sendIntent.type = "text/plain"
sendIntent.putExtra(Intent.EXTRA_TEXT, text)
val chooserIntent = ShareCompat.IntentBuilder(context)
.setType("text/plain")
.setText(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 {
context.startActivity(Intent.createChooser(intent, context.getString(R.string.share)))
context.startActivity(intent)
} catch (activityNotFoundException: ActivityNotFoundException) {
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) { _, _ ->
openAppSettingsPage(this)
}
.setNegativeButton(R.string.cancel, null)
.setNegativeButton(R.string.action_cancel, null)
.show()
}

View file

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

View file

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

View file

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

View file

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

View file

@ -22,6 +22,7 @@ import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
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.loadingItem
import im.vector.app.core.resources.StringProvider
@ -73,11 +74,11 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(
KeysBackupState.Disabled -> {
genericItem {
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)
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 -> {
genericItem {
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)
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 {
description(keyBackupState.toString())
description(keyBackupState.toString().toEpoxyCharSequence())
}
endIconResourceId(R.drawable.unit_test_ko)
}
@ -103,12 +104,12 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(
KeysBackupState.ReadyToBackUp -> {
genericItem {
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)
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 {
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)
}
@ -119,7 +120,7 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(
KeysBackupState.BackingUp -> {
genericItem {
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)
hasIndeterminateProcess(true)
@ -129,10 +130,11 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(
val remainingKeysToBackup = totalKeys - backedUpKeys
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 {
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
genericItem {
id("version")
title(host.stringProvider.getString(R.string.keys_backup_info_title_version))
description(keyVersionResult?.version ?: "")
title(host.stringProvider.getString(R.string.keys_backup_info_title_version).toEpoxyCharSequence())
description(keyVersionResult?.version.orEmpty().toEpoxyCharSequence())
}
genericItem {
id("algorithm")
title(host.stringProvider.getString(R.string.keys_backup_info_title_algorithm))
description(keyVersionResult?.algorithm ?: "")
title(host.stringProvider.getString(R.string.keys_backup_info_title_algorithm).toEpoxyCharSequence())
description(keyVersionResult?.algorithm.orEmpty().toEpoxyCharSequence())
}
if (vectorPreferences.developerMode()) {
@ -189,7 +191,7 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(
keysVersionTrust().signatures.forEach {
genericItem {
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 isDeviceVerified = it.device?.isVerified ?: false
@ -197,21 +199,27 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(
val deviceId: String = it.deviceId ?: ""
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)
} else {
if (isSignatureValid) {
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)
} else {
if (isDeviceVerified) {
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)
} else {
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)
}
}
@ -220,10 +228,12 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(
endIconResourceId(R.drawable.e2e_warning)
if (isDeviceVerified) {
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 {
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)
.setTitle(R.string.keys_backup_setup_skip_title)
.setMessage(R.string.keys_backup_setup_skip_msg)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.leave) { _, _ ->
.setNegativeButton(R.string.action_cancel, null)
.setPositiveButton(R.string.action_leave) { _, _ ->
finish()
}
.show()

View file

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

View file

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

View file

@ -20,6 +20,8 @@ import androidx.core.text.toSpannable
import com.airbnb.epoxy.EpoxyController
import im.vector.app.R
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.StringProvider
import im.vector.app.core.utils.colorizeMatchingText
@ -49,12 +51,12 @@ class VerificationCancelController @Inject constructor(
if (state.currentDeviceCanCrossSign) {
bottomSheetVerificationNoticeItem {
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 {
bottomSheetVerificationNoticeItem {
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 {
@ -63,9 +65,11 @@ class VerificationCancelController @Inject constructor(
bottomSheetVerificationNoticeItem {
id("notice")
notice(
host.stringProvider.getString(R.string.verify_cancel_other, otherDisplayName, otherUserID)
.toSpannable()
.colorizeMatchingText(otherUserID, host.colorProvider.getColorFromAttribute(R.attr.vctr_notice_text_color))
EpoxyCharSequence(
host.stringProvider.getString(R.string.verify_cancel_other, otherDisplayName, otherUserID)
.toSpannable()
.colorizeMatchingText(otherUserID, host.colorProvider.getColorFromAttribute(R.attr.vctr_notice_text_color))
)
)
}
}
@ -76,7 +80,7 @@ class VerificationCancelController @Inject constructor(
bottomSheetVerificationActionItem {
id("cancel")
title(host.stringProvider.getString(R.string.skip))
title(host.stringProvider.getString(R.string.action_skip))
titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorError))
iconRes(R.drawable.ic_arrow_right)
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 im.vector.app.R
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.StringProvider
import im.vector.app.features.crypto.verification.VerificationBottomSheetViewState
@ -46,7 +47,7 @@ class VerificationNotMeController @Inject constructor(
val host = this
bottomSheetVerificationNoticeItem {
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 {
@ -55,7 +56,7 @@ class VerificationNotMeController @Inject constructor(
bottomSheetVerificationActionItem {
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))
iconRes(R.drawable.ic_arrow_right)
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