mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-16 02:05:06 +08:00
Merge branch 'develop' into feature/fga/timeline_chunks_rework
This commit is contained in:
commit
8ca60eadbb
32
.github/workflows/triage-move-labelled.yml
vendored
32
.github/workflows/triage-move-labelled.yml
vendored
@ -6,7 +6,7 @@ on:
|
||||
|
||||
jobs:
|
||||
move_needs_info_issues:
|
||||
name: Move X-Needs-Info issues to Need info on triage board
|
||||
name: X-Needs-Info issues to Need info column on triage board
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: konradpabjan/move-labeled-or-milestoned-issue@219d384e03fa4b6460cd24f9f37d19eb033a4338
|
||||
@ -17,15 +17,17 @@ jobs:
|
||||
label-name: "X-Needs-Info"
|
||||
|
||||
add_priority_design_issues_to_project:
|
||||
name: Move priority X-Needs-Design issues to Design project board
|
||||
name: P1 X-Needs-Design to Design project board
|
||||
runs-on: ubuntu-latest
|
||||
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-Critical') ||
|
||||
contains(github.event.issue.labels.*.name, 'S-Major') ||
|
||||
contains(github.event.issue.labels.*.name, 'S-Minor'))
|
||||
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'))
|
||||
steps:
|
||||
- uses: octokit/graphql-action@v2.x
|
||||
id: add_to_project
|
||||
@ -45,8 +47,8 @@ jobs:
|
||||
PROJECT_ID: "PN_kwDOAM0swc0sUA"
|
||||
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
move_spaces_issues:
|
||||
name: Move Spaces issues to Delight project board
|
||||
spaces_issues_to_old_board:
|
||||
name: Spaces issues to old Delight project board
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
contains(github.event.issue.labels.*.name, 'A-Spaces') ||
|
||||
@ -59,8 +61,16 @@ jobs:
|
||||
project-url: "https://github.com/orgs/vector-im/projects/6"
|
||||
column-name: "📥 Inbox"
|
||||
label-name: "A-Spaces"
|
||||
|
||||
spaces_issues_to_new_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
|
||||
id: add_to_delight2
|
||||
with:
|
||||
headers: '{"GraphQL-Features": "projects_next_graphql"}'
|
||||
query: |
|
||||
@ -78,7 +88,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
move_voice-message_issues:
|
||||
name: Move A-Voice Messages to Voice message board
|
||||
name: A-Voice Messages to voice message board
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
contains(github.event.issue.labels.*.name, 'A-Voice Messages')
|
||||
@ -101,7 +111,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
move_threads_issues:
|
||||
name: Move A-Threads to Thread board
|
||||
name: A-Threads to Thread board
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
contains(github.event.issue.labels.*.name, 'A-Threads')
|
||||
|
2
.github/workflows/triage-priority-bugs.yml
vendored
2
.github/workflows/triage-priority-bugs.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: Move P1 issues into the P1 column for the App Team and Crypto team
|
||||
name: Move P1 bugs to boards
|
||||
|
||||
on:
|
||||
issues:
|
||||
|
32
CHANGES.md
32
CHANGES.md
@ -1,3 +1,35 @@
|
||||
Changes in Element v1.3.9 (2021-12-01)
|
||||
======================================
|
||||
|
||||
Features ✨
|
||||
----------
|
||||
- Voice messages: Persist drafts of voice messages when navigating between rooms ([#3922](https://github.com/vector-im/element-android/issues/3922))
|
||||
- Make Element Android Thread aware ([#4246](https://github.com/vector-im/element-android/issues/4246))
|
||||
- Iterate on the consent dialog of the identity server. ([#4577](https://github.com/vector-im/element-android/issues/4577))
|
||||
|
||||
Bugfixes 🐛
|
||||
----------
|
||||
- Fixes left over text when inserting emojis via the ':' menu and replaces the last typed ':' rather than the one at the end of the message ([#3449](https://github.com/vector-im/element-android/issues/3449))
|
||||
- Fixing queued voice message failing to send or retry ([#3833](https://github.com/vector-im/element-android/issues/3833))
|
||||
- Keeping device screen on whilst recording and playing back voice messages ([#4022](https://github.com/vector-im/element-android/issues/4022))
|
||||
- Allow voice messages to continue recording during device rotation ([#4067](https://github.com/vector-im/element-android/issues/4067))
|
||||
- Allowing users to hang up VOIP calls during the initialisation phase (avoids getting stuck in the call screen if something goes wrong) ([#4144](https://github.com/vector-im/element-android/issues/4144))
|
||||
- Make the verification shields the same in Element Web and Element Android ([#4338](https://github.com/vector-im/element-android/issues/4338))
|
||||
- Fix a display issue in the composer when the replied message is changed. ([#4343](https://github.com/vector-im/element-android/issues/4343))
|
||||
- Dismissing the Fdroid variant Listening for notifications on sign out, fixes crash when tapping the notification when signed out ([#4488](https://github.com/vector-im/element-android/issues/4488))
|
||||
- Fix a crash when displaying the bootstrap bottom sheet ([#4520](https://github.com/vector-im/element-android/issues/4520))
|
||||
- Remove duplicated settings declaration ([#4539](https://github.com/vector-im/element-android/issues/4539))
|
||||
- Fixes .ogg files failing to upload to rooms ([#4552](https://github.com/vector-im/element-android/issues/4552))
|
||||
- Add robustness when getting data from cursors ([#4605](https://github.com/vector-im/element-android/issues/4605))
|
||||
|
||||
Other changes
|
||||
-------------
|
||||
- Upgrade Jitsi lib (and so webrtc) from Jitsi android-sdk-3.1.0 to android-sdk-3.10.0 ([#4504](https://github.com/vector-im/element-android/issues/4504))
|
||||
- Improve crypto logs to help debug decryption failures ([#4507](https://github.com/vector-im/element-android/issues/4507))
|
||||
- Voice recording mic button refactor with small animation tweaks in preparation for voice drafts ([#4515](https://github.com/vector-im/element-android/issues/4515))
|
||||
- Remove requestModelBuild() from epoxy Controllers init{} block ([#4591](https://github.com/vector-im/element-android/issues/4591))
|
||||
|
||||
|
||||
Changes in Element v1.3.8 (2021-11-17)
|
||||
======================================
|
||||
|
||||
|
@ -1 +0,0 @@
|
||||
Fixes left over text when inserting emojis via the ':' menu and replaces the last typed ':' rather than the one at the end of the message
|
@ -1 +0,0 @@
|
||||
Fixing queued voice message failing to send or retry
|
@ -1 +0,0 @@
|
||||
Keeping device screen on whilst recording and playing back voice messages
|
@ -1 +0,0 @@
|
||||
Allow voice messages to continue recording during device rotation
|
@ -1 +0,0 @@
|
||||
Allowing users to hang up VOIP calls during the initialisation phase (avoids getting stuck in the call screen if something goes wrong)
|
@ -1 +0,0 @@
|
||||
Make Element Android Thread aware
|
1
changelog.d/4324.bugfix
Normal file
1
changelog.d/4324.bugfix
Normal file
@ -0,0 +1 @@
|
||||
Fixes message menu showing when copying message urls
|
@ -1 +0,0 @@
|
||||
Make the verification shields the same in Element Web and Element Android
|
@ -1 +0,0 @@
|
||||
Fix a display issue in the composer when the replied message is changed.
|
@ -1 +0,0 @@
|
||||
Dismissing the Fdroid variant Listening for notifications on sign out, fixes crash when tapping the notification when signed out
|
@ -1 +0,0 @@
|
||||
Upgrade Jitsi lib (and so webrtc) from Jitsi android-sdk-3.1.0 to android-sdk-3.10.0
|
@ -1 +0,0 @@
|
||||
Improve crypto logs to help debug decryption failures
|
@ -1 +0,0 @@
|
||||
Voice recording mic button refactor with small animation tweaks in preparation for voice drafts
|
@ -1 +0,0 @@
|
||||
Fix a crash when displaying the bootstrap bottom sheet
|
@ -1 +0,0 @@
|
||||
Remove duplicated settings declaration
|
@ -1 +0,0 @@
|
||||
Fixes .ogg files failing to upload to rooms
|
1
changelog.d/4602.misc
Normal file
1
changelog.d/4602.misc
Normal file
@ -0,0 +1 @@
|
||||
There is no need to call job.cancel() when we are using viewModelScope()
|
1
changelog.d/4604.misc
Normal file
1
changelog.d/4604.misc
Normal file
@ -0,0 +1 @@
|
||||
Cleanup the layout files
|
1
changelog.d/4617.misc
Normal file
1
changelog.d/4617.misc
Normal file
@ -0,0 +1 @@
|
||||
Improve issue automation workflows
|
@ -11,7 +11,7 @@ def gradle = "7.0.3"
|
||||
// Ref: https://kotlinlang.org/releases.html
|
||||
def kotlin = "1.5.31"
|
||||
def kotlinCoroutines = "1.5.2"
|
||||
def dagger = "2.40.3"
|
||||
def dagger = "2.40.4"
|
||||
def retrofit = "2.9.0"
|
||||
def arrow = "0.8.2"
|
||||
def markwon = "4.6.2"
|
||||
|
2
fastlane/metadata/android/cs-CZ/changelogs/40103070.txt
Normal file
2
fastlane/metadata/android/cs-CZ/changelogs/40103070.txt
Normal file
@ -0,0 +1,2 @@
|
||||
Hlavní změny v této verzi: Opravy chyb týkající se především oznámení.
|
||||
Úplný seznam změn: https://github.com/vector-im/element-android/releases/tag/v1.3.7
|
2
fastlane/metadata/android/de-DE/changelogs/40103070.txt
Normal file
2
fastlane/metadata/android/de-DE/changelogs/40103070.txt
Normal file
@ -0,0 +1,2 @@
|
||||
Hauptänderungen: Fehler bei Benachrichtigungen gefixt
|
||||
Ganze Änderungsliste: https://github.com/vector-im/element-android/releases/tag/v1.3.7
|
2
fastlane/metadata/android/en-US/changelogs/40103090.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40103090.txt
Normal file
@ -0,0 +1,2 @@
|
||||
Main changes in this version: Add support for voice message draft. Many bugfixes!
|
||||
Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.9
|
2
fastlane/metadata/android/et/changelogs/40103070.txt
Normal file
2
fastlane/metadata/android/et/changelogs/40103070.txt
Normal file
@ -0,0 +1,2 @@
|
||||
Põhilised muutused selles versioonis: erinevad veaparandused, neist enamus on seotud teavitustega.
|
||||
Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases/tag/v1.3.7
|
2
fastlane/metadata/android/fa/changelogs/40103050.txt
Normal file
2
fastlane/metadata/android/fa/changelogs/40103050.txt
Normal file
@ -0,0 +1,2 @@
|
||||
تغییرات اصلی در این نگارش: افزودن پشتیبانی حضور برای اتاقهای پیام مستقیم (یادداشت: حضور روی matrix.org از کار افتاده است). افزودن دوبارهٔ پشتیبانی اندروید خودرو.
|
||||
گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.3.5
|
2
fastlane/metadata/android/fa/changelogs/40103060.txt
Normal file
2
fastlane/metadata/android/fa/changelogs/40103060.txt
Normal file
@ -0,0 +1,2 @@
|
||||
تغییرات اصلی در این نگارش: افزودن پشتیبانی حضور برای اتاقهای پیام مستقیم (یادداشت: حضور روی matrix.org از کار افتاده است). افزودن دوبارهٔ پشتیبانی اندروید خودرو.
|
||||
گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.3.6
|
2
fastlane/metadata/android/fa/changelogs/40103070.txt
Normal file
2
fastlane/metadata/android/fa/changelogs/40103070.txt
Normal file
@ -0,0 +1,2 @@
|
||||
تغییرات اصلی در این نگارش: رفع اشکالهایی عمدتاً مربوط به آگاهیها.
|
||||
گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.3.7
|
2
fastlane/metadata/android/fr-FR/changelogs/40103070.txt
Normal file
2
fastlane/metadata/android/fr-FR/changelogs/40103070.txt
Normal file
@ -0,0 +1,2 @@
|
||||
Principaux changements pour cette version : corrections de problèmes, principalement sur les notifications
|
||||
Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.3.7
|
2
fastlane/metadata/android/hu-HU/changelogs/40103070.txt
Normal file
2
fastlane/metadata/android/hu-HU/changelogs/40103070.txt
Normal file
@ -0,0 +1,2 @@
|
||||
Fő változás ebben a verzióban: Értesítési hibajavítások
|
||||
Teljes változásnapló: https://github.com/vector-im/element-android/releases/tag/v1.3.7
|
2
fastlane/metadata/android/id/changelogs/40103070.txt
Normal file
2
fastlane/metadata/android/id/changelogs/40103070.txt
Normal file
@ -0,0 +1,2 @@
|
||||
Perubahan utama di versi ini: Perbaikan bug terutama untuk notifikasinya.
|
||||
Changelog lengkap: https://github.com/vector-im/element-android/releases/tag/v1.3.7
|
2
fastlane/metadata/android/it-IT/changelogs/40103070.txt
Normal file
2
fastlane/metadata/android/it-IT/changelogs/40103070.txt
Normal file
@ -0,0 +1,2 @@
|
||||
Modifiche principali in questa versione: correzioni riguardo le notifiche.
|
||||
Cronologia completa: https://github.com/vector-im/element-android/releases/tag/v1.3.7
|
2
fastlane/metadata/android/pl/changelogs/40103070.txt
Normal file
2
fastlane/metadata/android/pl/changelogs/40103070.txt
Normal file
@ -0,0 +1,2 @@
|
||||
Główne zmiany w tej wersji: Poprawki błędów dotyczące głównie powiadomień
|
||||
Pełny changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.7
|
42
fastlane/metadata/android/pl/full_description.txt
Normal file
42
fastlane/metadata/android/pl/full_description.txt
Normal file
@ -0,0 +1,42 @@
|
||||
Element jest bezpiecznym komunikatorem oraz narzędziem do komunikacji w zespole które jest idealna do pracy zdalnej. Nasza aplikacja korzysta z szyfrowania end-to-end aby rozmowy wideo, udostępnianie plików oraz rozmowy głosowe były bezpieczne.
|
||||
|
||||
<b>Funkcje Element'a:</b>
|
||||
- Zaawansowane narzędzia komunikacji online
|
||||
- W pełni szyfrowane wiadomości które umożliwiają bezpieczniejszą komunikacje dla firm, a nawet dla pracowników zdalnych.
|
||||
- Zdecentralizowany czat bazowany na otwartym protokole Matrix
|
||||
- Bezpieczne udostępnianie plików wraz z szyfrowaniem danych podczas zarządzania projektami
|
||||
- Rozmowy z Voice over IP wraz z udostępnianiem ekranu
|
||||
- Prosta konfiguracja z ulubionymi narzędziami do kolaboracji, narzędziami do zarządzania projektami usługami VoIP oraz innymi aplikacjami do komunikacji w grupie
|
||||
|
||||
Element jest całkowicie inny od innych komunikatorów i aplikacji do kolaboracji. Pracuje na protokole Matrix, otwarto źródłowej sieci stworzonej dla bezpiecznych wiadomości i zdecentralizowanej komunikacji. Pozwala ona na własny hosting serwera dla maksymalnej własności i kontroli nad danymi oraz wiadomościami.
|
||||
|
||||
<b>Prywatność i szyfrowane wiadomości</b>
|
||||
Element broni cie przez niechcianymi wiadomościami, kopaniem informacji oraz cenzurą. Zabezpiecza wszystkie twoje dane, wideo które pozostaje wiadome tylko dla rozmawiających przez szyfrowanie end-to-end i weryfikacją krzyżową urządzeń.
|
||||
|
||||
Element daje kontrole nad twoją prywatnością i umożliwia bezpieczną komunikacje z kimkolwiek w sieci Matrix, lub z innymi firmami przez narzędzia do komunikacji integrując aplikacje takie jak Slack.
|
||||
|
||||
<b>Element może być hostowany samemu</b>
|
||||
Pozwala to na kontrolę nad twoimi wrażliwymi danymi oraz rozmowami, Element może być hostowany samemu lub pozwala wybrać dowolnego hosta bazowanego na Matrix'ie - otwarto-źródłowym standardzie, dla zdecentralizowanej komunikacji. Element daje tobie prywatność, bezpieczeństwo oraz elastyczność w integracji.
|
||||
|
||||
<b>Posiadaj naprawdę swoje dane</b>
|
||||
Ty decydujesz gdzie trzymasz swoje dane i wiadomości. Bez ryzyka wycieku lub dostępu firm trzecich.
|
||||
|
||||
Element daje ci kontrolę na wiele sposobów:
|
||||
1. Utwórz darmowe konto na publicznym serwerze matrix.org hostowanym przez twórców Matrix'a lub wybierz którykolwiek z tysięcy serwerów hostowanych przez wolontariuszy
|
||||
2. Hostuj samemu swoje konto przez własny serwer na twojej infrastrukturze
|
||||
3. Zarejestruj się na specjalnym serwerze poprzez subskrybowanie hostingu na platformie Element Martix Services
|
||||
|
||||
<b>Otwarte wiadomości i kolaboracja</b>
|
||||
Możesz rozmawiać z kimkolwiek w sieci Matrix, nie ważne czy korzystają z Element'a, czy z innej aplikacji wspierającej protokół Matrix, a nawet z osobami korzystającymi z innych komunikatorów.
|
||||
|
||||
<b>Niesamowicie bezpieczny</b>
|
||||
Prawdziwe szyfrowanie end-to-end (tylko osoby w konwersacji mogą odszyfrować wiadomości), a także krzyżowa weryfikacja urządzeń.
|
||||
|
||||
<b>Pełna komunikacja i integracja</b>
|
||||
Wiadomości, rozmowy głosowe i wideo, udostępnianie plików, ekranu, a nawet integracja z botami i widżetami. Twórz pokoje, społeczności, pozostań w kontakcie i załatwiaj to co chcesz.
|
||||
|
||||
<b>Kontynuuj gdzie skończyłeś</b>
|
||||
Pozostań zawsze w kontakcie poprzez pełną synchornizację między urządzeniami oraz w sieci na https://app.element.io
|
||||
|
||||
<b>Otwarto źródłowy</b>
|
||||
Element Android jest otwarto-źródłowym projektem, hostowanym na platformie GitHub. Prosimy o zgłaszanie wszelkich błędów i/lub wsparcie w tworzeniu naszego projektu na https://github.com/vector-im/element-android
|
1
fastlane/metadata/android/pl/short_description.txt
Normal file
1
fastlane/metadata/android/pl/short_description.txt
Normal file
@ -0,0 +1 @@
|
||||
Grupowy komunikator - szyfrowane wiadomosci, grupowe czaty oraz rozmowy wideo
|
1
fastlane/metadata/android/pl/title.txt
Normal file
1
fastlane/metadata/android/pl/title.txt
Normal file
@ -0,0 +1 @@
|
||||
Element - Bezpieczny Komunikator
|
2
fastlane/metadata/android/pt-BR/changelogs/40103070.txt
Normal file
2
fastlane/metadata/android/pt-BR/changelogs/40103070.txt
Normal file
@ -0,0 +1,2 @@
|
||||
Principais mudanças nesta versão: Consertos de bugs principalmente quanto às notificações.
|
||||
Changelog completo: https://github.com/vector-im/element-android/releases/tag/v1.3.7
|
2
fastlane/metadata/android/sv-SE/changelogs/40103070.txt
Normal file
2
fastlane/metadata/android/sv-SE/changelogs/40103070.txt
Normal file
@ -0,0 +1,2 @@
|
||||
Huvudsakliga ändringar i den här versionen: Buggfixar som huvudsakligen rör aviseringar.
|
||||
Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.3.7
|
2
fastlane/metadata/android/uk/changelogs/40103070.txt
Normal file
2
fastlane/metadata/android/uk/changelogs/40103070.txt
Normal file
@ -0,0 +1,2 @@
|
||||
Основні зміни в цій версії: виправлення помилок в основному у повідомленнях.
|
||||
Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.3.7
|
2
fastlane/metadata/android/zh-CN/changelogs/40103070.txt
Normal file
2
fastlane/metadata/android/zh-CN/changelogs/40103070.txt
Normal file
@ -0,0 +1,2 @@
|
||||
此版本的主要变化:主要关于通知的错误修复。
|
||||
完整更新日志:https://github.com/vector-im/element-android/releases/tag/v1.3.7
|
2
fastlane/metadata/android/zh-TW/changelogs/40103070.txt
Normal file
2
fastlane/metadata/android/zh-TW/changelogs/40103070.txt
Normal file
@ -0,0 +1,2 @@
|
||||
此版本中的主要變動:主要關於通知的臭蟲修復。
|
||||
完整的變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.3.7
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=00b273629df4ce46e68df232161d5a7c4e495b9a029ce6e0420f071e21316867
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-all.zip
|
||||
distributionSha256Sum=b75392c5625a88bccd58a574552a5a323edca82dab5942d2d41097f809c6bcce
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.1-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
@ -34,7 +34,7 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/appBarLayout">
|
||||
app:layout_constraintTop_toBottomOf="@id/appBarLayout">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@ -334,7 +334,6 @@
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -5,7 +5,6 @@
|
||||
<item name="android:visibility">visible</item>
|
||||
</style>
|
||||
|
||||
|
||||
<style name="Theme.Debug.Light" parent="Theme.MaterialComponents.Light.NoActionBar">
|
||||
<!-- Keep all default value -->
|
||||
</style>
|
||||
|
@ -11,5 +11,3 @@
|
||||
<changeImageTransform />
|
||||
|
||||
</transitionSet>
|
||||
|
||||
|
||||
|
@ -32,7 +32,6 @@
|
||||
<dimen name="call_pip_width">88dp</dimen>
|
||||
<dimen name="call_pip_radius">8dp</dimen>
|
||||
|
||||
|
||||
<dimen name="item_form_min_height">76dp</dimen>
|
||||
|
||||
<!-- Max width for some buttons -->
|
||||
@ -40,5 +39,4 @@
|
||||
|
||||
<!-- Navigation Drawer -->
|
||||
<dimen name="navigation_drawer_max_width">320dp</dimen>
|
||||
|
||||
</resources>
|
@ -20,7 +20,6 @@
|
||||
<color name="palette_prune">#5C56F5</color>
|
||||
<color name="palette_links">#0086E6</color>
|
||||
|
||||
|
||||
<!-- For light themes -->
|
||||
<color name="palette_gray_25">#F4F6FA</color>
|
||||
<color name="palette_gray_50">#E3E8F0</color>
|
||||
|
@ -1,5 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<resources>
|
||||
|
||||
<declare-styleable name="BadgeFloatingActionButton">
|
||||
|
@ -49,7 +49,6 @@
|
||||
<item name="android:backgroundTint">@android:color/black</item>
|
||||
</style>
|
||||
|
||||
|
||||
<style name="Widget.Vector.Button.Outlined.SocialLogin.Facebook">
|
||||
<item name="icon">@drawable/ic_social_facebook</item>
|
||||
</style>
|
||||
@ -68,7 +67,6 @@
|
||||
<item name="android:backgroundTint">#3877EA</item>
|
||||
</style>
|
||||
|
||||
|
||||
<style name="Widget.Vector.Button.Outlined.SocialLogin.Twitter">
|
||||
<item name="icon">@drawable/ic_social_twitter</item>
|
||||
</style>
|
||||
@ -85,7 +83,6 @@
|
||||
<item name="android:backgroundTint">#5D9EC9</item>
|
||||
</style>
|
||||
|
||||
|
||||
<style name="Widget.Vector.Button.Outlined.SocialLogin.Apple">
|
||||
<item name="icon">@drawable/ic_social_apple</item>
|
||||
</style>
|
||||
@ -118,5 +115,4 @@
|
||||
<item name="android:backgroundTint">@android:color/black</item>
|
||||
</style>
|
||||
|
||||
|
||||
</resources>
|
@ -31,7 +31,7 @@ android {
|
||||
// that the app's state is completely cleared between tests.
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
|
||||
buildConfigField "String", "SDK_VERSION", "\"1.3.9\""
|
||||
buildConfigField "String", "SDK_VERSION", "\"1.3.10\""
|
||||
|
||||
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
|
||||
resValue "string", "git_sdk_revision", "\"${gitRevision()}\""
|
||||
@ -161,7 +161,7 @@ dependencies {
|
||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.38'
|
||||
|
||||
testImplementation libs.tests.junit
|
||||
testImplementation 'org.robolectric:robolectric:4.7.2'
|
||||
testImplementation 'org.robolectric:robolectric:4.7.3'
|
||||
//testImplementation 'org.robolectric:shadows-support-v4:3.0'
|
||||
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
|
||||
testImplementation libs.mockk.mockk
|
||||
|
@ -22,6 +22,7 @@ import androidx.exifinterface.media.ExifInterface
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.matrix.android.sdk.api.util.MimeTypes.normalizeMimeType
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
|
||||
@Parcelize
|
||||
@JsonClass(generateAdapter = true)
|
||||
@ -49,4 +50,14 @@ data class ContentAttachmentData(
|
||||
}
|
||||
|
||||
fun getSafeMimeType() = mimeType?.normalizeMimeType()
|
||||
|
||||
fun toJsonString(): String {
|
||||
return MoshiProvider.providesMoshi().adapter(ContentAttachmentData::class.java).toJson(this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromJsonString(json: String): ContentAttachmentData? {
|
||||
return MoshiProvider.providesMoshi().adapter(ContentAttachmentData::class.java).fromJson(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,14 +24,15 @@ package org.matrix.android.sdk.api.session.room.send
|
||||
* REPLY: draft of a reply of another message
|
||||
*/
|
||||
sealed interface UserDraft {
|
||||
data class Regular(val text: String) : UserDraft
|
||||
data class Quote(val linkedEventId: String, val text: String) : UserDraft
|
||||
data class Edit(val linkedEventId: String, val text: String) : UserDraft
|
||||
data class Reply(val linkedEventId: String, val text: String) : UserDraft
|
||||
data class Regular(val content: String) : UserDraft
|
||||
data class Quote(val linkedEventId: String, val content: String) : UserDraft
|
||||
data class Edit(val linkedEventId: String, val content: String) : UserDraft
|
||||
data class Reply(val linkedEventId: String, val content: String) : UserDraft
|
||||
data class Voice(val content: String) : UserDraft
|
||||
|
||||
fun isValid(): Boolean {
|
||||
return when (this) {
|
||||
is Regular -> text.isNotBlank()
|
||||
is Regular -> content.isNotBlank()
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
@ -30,16 +30,18 @@ internal object DraftMapper {
|
||||
DraftEntity.MODE_EDIT -> UserDraft.Edit(entity.linkedEventId, entity.content)
|
||||
DraftEntity.MODE_QUOTE -> UserDraft.Quote(entity.linkedEventId, entity.content)
|
||||
DraftEntity.MODE_REPLY -> UserDraft.Reply(entity.linkedEventId, entity.content)
|
||||
DraftEntity.MODE_VOICE -> UserDraft.Voice(entity.content)
|
||||
else -> null
|
||||
} ?: UserDraft.Regular("")
|
||||
}
|
||||
|
||||
fun map(domain: UserDraft): DraftEntity {
|
||||
return when (domain) {
|
||||
is UserDraft.Regular -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_REGULAR, linkedEventId = "")
|
||||
is UserDraft.Edit -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_EDIT, linkedEventId = domain.linkedEventId)
|
||||
is UserDraft.Quote -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_QUOTE, linkedEventId = domain.linkedEventId)
|
||||
is UserDraft.Reply -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_REPLY, linkedEventId = domain.linkedEventId)
|
||||
is UserDraft.Regular -> DraftEntity(content = domain.content, draftMode = DraftEntity.MODE_REGULAR, linkedEventId = "")
|
||||
is UserDraft.Edit -> DraftEntity(content = domain.content, draftMode = DraftEntity.MODE_EDIT, linkedEventId = domain.linkedEventId)
|
||||
is UserDraft.Quote -> DraftEntity(content = domain.content, draftMode = DraftEntity.MODE_QUOTE, linkedEventId = domain.linkedEventId)
|
||||
is UserDraft.Reply -> DraftEntity(content = domain.content, draftMode = DraftEntity.MODE_REPLY, linkedEventId = domain.linkedEventId)
|
||||
is UserDraft.Voice -> DraftEntity(content = domain.content, draftMode = DraftEntity.MODE_VOICE, linkedEventId = "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ import io.realm.RealmObject
|
||||
internal open class DraftEntity(var content: String = "",
|
||||
var draftMode: String = MODE_REGULAR,
|
||||
var linkedEventId: String = ""
|
||||
|
||||
) : RealmObject() {
|
||||
|
||||
companion object {
|
||||
@ -29,5 +28,6 @@ internal open class DraftEntity(var content: String = "",
|
||||
const val MODE_EDIT = "EDIT"
|
||||
const val MODE_REPLY = "REPLY"
|
||||
const val MODE_QUOTE = "QUOTE"
|
||||
const val MODE_VOICE = "VOICE"
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,8 @@ import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.provider.ContactsContract
|
||||
import androidx.core.database.getIntOrNull
|
||||
import androidx.core.database.getStringOrNull
|
||||
import im.vector.lib.multipicker.entity.MultiPickerContactType
|
||||
import im.vector.lib.multipicker.utils.getColumnIndexOrNull
|
||||
|
||||
@ -54,9 +56,9 @@ class ContactPicker : Picker<MultiPickerContactType>() {
|
||||
val nameColumn = cursor.getColumnIndexOrNull(ContactsContract.Contacts.DISPLAY_NAME) ?: return@use
|
||||
val photoUriColumn = cursor.getColumnIndexOrNull(ContactsContract.Contacts.PHOTO_URI) ?: return@use
|
||||
|
||||
val contactId = cursor.getInt(idColumn)
|
||||
var name = cursor.getString(nameColumn)
|
||||
val photoUri = cursor.getString(photoUriColumn)
|
||||
val contactId = cursor.getIntOrNull(idColumn) ?: return@use
|
||||
var name = cursor.getStringOrNull(nameColumn) ?: return@use
|
||||
val photoUri = cursor.getStringOrNull(photoUriColumn)
|
||||
val phoneNumberList = mutableListOf<String>()
|
||||
val emailList = mutableListOf<String>()
|
||||
|
||||
@ -78,8 +80,8 @@ class ContactPicker : Picker<MultiPickerContactType>() {
|
||||
val data1ColumnIndex = innerCursor.getColumnIndexOrNull(ContactsContract.Data.DATA1) ?: return@inner
|
||||
|
||||
while (innerCursor.moveToNext()) {
|
||||
val mimeType = innerCursor.getString(mimeTypeColumnIndex)
|
||||
val contactData = innerCursor.getString(data1ColumnIndex)
|
||||
val mimeType = innerCursor.getStringOrNull(mimeTypeColumnIndex)
|
||||
val contactData = innerCursor.getStringOrNull(data1ColumnIndex) ?: continue
|
||||
|
||||
if (mimeType == ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) {
|
||||
name = contactData
|
||||
@ -121,7 +123,7 @@ class ContactPicker : Picker<MultiPickerContactType>() {
|
||||
)?.use { cursor ->
|
||||
return if (cursor.moveToFirst()) {
|
||||
cursor.getColumnIndexOrNull(ContactsContract.RawContacts._ID)
|
||||
?.let { cursor.getInt(it) }
|
||||
?.let { cursor.getIntOrNull(it) }
|
||||
} else null
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,8 @@ package im.vector.lib.multipicker
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.provider.OpenableColumns
|
||||
import androidx.core.database.getLongOrNull
|
||||
import androidx.core.database.getStringOrNull
|
||||
import im.vector.lib.multipicker.entity.MultiPickerBaseType
|
||||
import im.vector.lib.multipicker.entity.MultiPickerFileType
|
||||
import im.vector.lib.multipicker.utils.getColumnIndexOrNull
|
||||
@ -53,8 +55,8 @@ class FilePicker : Picker<MultiPickerBaseType>() {
|
||||
val nameColumn = cursor.getColumnIndexOrNull(OpenableColumns.DISPLAY_NAME) ?: return@use null
|
||||
val sizeColumn = cursor.getColumnIndexOrNull(OpenableColumns.SIZE) ?: return@use null
|
||||
if (cursor.moveToFirst()) {
|
||||
val name = cursor.getString(nameColumn)
|
||||
val size = cursor.getLong(sizeColumn)
|
||||
val name = cursor.getStringOrNull(nameColumn)
|
||||
val size = cursor.getLongOrNull(sizeColumn) ?: 0
|
||||
|
||||
MultiPickerFileType(
|
||||
name,
|
||||
|
@ -20,6 +20,8 @@ import android.content.Context
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
import androidx.core.database.getLongOrNull
|
||||
import androidx.core.database.getStringOrNull
|
||||
import im.vector.lib.multipicker.entity.MultiPickerAudioType
|
||||
import im.vector.lib.multipicker.entity.MultiPickerImageType
|
||||
import im.vector.lib.multipicker.entity.MultiPickerVideoType
|
||||
@ -41,8 +43,8 @@ internal fun Uri.toMultiPickerImageType(context: Context): MultiPickerImageType?
|
||||
val sizeColumn = cursor.getColumnIndexOrNull(MediaStore.Images.Media.SIZE) ?: return@use null
|
||||
|
||||
if (cursor.moveToNext()) {
|
||||
val name = cursor.getString(nameColumn)
|
||||
val size = cursor.getLong(sizeColumn)
|
||||
val name = cursor.getStringOrNull(nameColumn)
|
||||
val size = cursor.getLongOrNull(sizeColumn) ?: 0
|
||||
|
||||
val bitmap = ImageUtils.getBitmap(context, this)
|
||||
val orientation = ImageUtils.getOrientation(context, this)
|
||||
@ -79,8 +81,8 @@ internal fun Uri.toMultiPickerVideoType(context: Context): MultiPickerVideoType?
|
||||
val sizeColumn = cursor.getColumnIndexOrNull(MediaStore.Video.Media.SIZE) ?: return@use null
|
||||
|
||||
if (cursor.moveToNext()) {
|
||||
val name = cursor.getString(nameColumn)
|
||||
val size = cursor.getLong(sizeColumn)
|
||||
val name = cursor.getStringOrNull(nameColumn)
|
||||
val size = cursor.getLongOrNull(sizeColumn) ?: 0
|
||||
var duration = 0L
|
||||
var width = 0
|
||||
var height = 0
|
||||
@ -128,8 +130,8 @@ fun Uri.toMultiPickerAudioType(context: Context): MultiPickerAudioType? {
|
||||
val sizeColumn = cursor.getColumnIndexOrNull(MediaStore.Audio.Media.SIZE) ?: return@use null
|
||||
|
||||
if (cursor.moveToNext()) {
|
||||
val name = cursor.getString(nameColumn)
|
||||
val size = cursor.getLong(sizeColumn)
|
||||
val name = cursor.getStringOrNull(nameColumn)
|
||||
val size = cursor.getLongOrNull(sizeColumn) ?: 0
|
||||
var duration = 0L
|
||||
|
||||
context.contentResolver.openFileDescriptor(this, "r")?.use { pfd ->
|
||||
|
@ -25,3 +25,8 @@
|
||||
|
||||
### Use style="@style/Widget.Vector.TextView.*" instead of textSize attribute
|
||||
android:textSize===9
|
||||
|
||||
### Use `@id` and not `@+id` when referencing ids in layouts
|
||||
layout_(.*)="@\+id
|
||||
accessibilityTraversal(.*)="@\+id
|
||||
toolbarId="@\+id
|
||||
|
@ -106,8 +106,6 @@
|
||||
default="MainViewEvents"
|
||||
help="The name of the view events to create" />
|
||||
|
||||
|
||||
|
||||
<parameter
|
||||
id="packageName"
|
||||
name="Package name"
|
||||
|
@ -15,7 +15,7 @@ kapt {
|
||||
// Note: 2 digits max for each value
|
||||
ext.versionMajor = 1
|
||||
ext.versionMinor = 3
|
||||
ext.versionPatch = 9
|
||||
ext.versionPatch = 10
|
||||
|
||||
static def getGitTimestamp() {
|
||||
def cmd = 'git show -s --format=%ct'
|
||||
|
@ -1,9 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!--
|
||||
The aim of this file is to test the different themes of Riot
|
||||
The aim of this file is to test the different themes of Element
|
||||
-->
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -1,10 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!--
|
||||
The aim of this file is to test the different themes of Riot
|
||||
The aim of this file is to test the different themes of Element
|
||||
Unfortunately, this does not work in the preview.
|
||||
-->
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
@ -20,6 +20,8 @@ import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.ContactsContract
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.database.getLongOrNull
|
||||
import androidx.core.database.getStringOrNull
|
||||
import im.vector.lib.multipicker.utils.getColumnIndexOrNull
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
@ -61,8 +63,8 @@ class ContactsDataSource @Inject constructor(
|
||||
val displayNameColumnIndex = cursor.getColumnIndexOrNull(ContactsContract.Contacts.DISPLAY_NAME) ?: return@use
|
||||
val photoUriColumnIndex = cursor.getColumnIndexOrNull(ContactsContract.Data.PHOTO_URI)
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLong(idColumnIndex)
|
||||
val displayName = cursor.getString(displayNameColumnIndex)
|
||||
val id = cursor.getLongOrNull(idColumnIndex) ?: continue
|
||||
val displayName = cursor.getStringOrNull(displayNameColumnIndex) ?: continue
|
||||
|
||||
val mappedContactBuilder = MappedContactBuilder(
|
||||
id = id,
|
||||
@ -70,7 +72,7 @@ class ContactsDataSource @Inject constructor(
|
||||
)
|
||||
|
||||
photoUriColumnIndex
|
||||
?.let { cursor.getString(it) }
|
||||
?.let { cursor.getStringOrNull(it) }
|
||||
?.let { Uri.parse(it) }
|
||||
?.let { mappedContactBuilder.photoURI = it }
|
||||
|
||||
@ -94,10 +96,10 @@ class ContactsDataSource @Inject constructor(
|
||||
val phoneNumberColumnIndex = cursor.getColumnIndexOrNull(ContactsContract.CommonDataKinds.Phone.NUMBER) ?: return@use
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val mappedContactBuilder = cursor.getLong(idColumnIndex)
|
||||
.let { map[it] }
|
||||
val mappedContactBuilder = cursor.getLongOrNull(idColumnIndex)
|
||||
?.let { map[it] }
|
||||
?: continue
|
||||
cursor.getString(phoneNumberColumnIndex)
|
||||
cursor.getStringOrNull(phoneNumberColumnIndex)
|
||||
?.let {
|
||||
mappedContactBuilder.msisdns.add(
|
||||
MappedMsisdn(
|
||||
@ -128,10 +130,10 @@ class ContactsDataSource @Inject constructor(
|
||||
while (cursor.moveToNext()) {
|
||||
// This would allow you get several email addresses
|
||||
// if the email addresses were stored in an array
|
||||
val mappedContactBuilder = cursor.getLong(idColumnIndex)
|
||||
.let { map[it] }
|
||||
val mappedContactBuilder = cursor.getLongOrNull(idColumnIndex)
|
||||
?.let { map[it] }
|
||||
?: continue
|
||||
cursor.getString(emailColumnIndex)
|
||||
cursor.getStringOrNull(emailColumnIndex)
|
||||
?.let {
|
||||
mappedContactBuilder.emails.add(
|
||||
MappedEmail(
|
||||
|
@ -17,6 +17,7 @@
|
||||
package im.vector.app.core.epoxy
|
||||
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import im.vector.app.core.utils.DebouncedClickListener
|
||||
|
||||
/**
|
||||
@ -32,6 +33,26 @@ fun View.onClick(listener: ClickListener?) {
|
||||
}
|
||||
}
|
||||
|
||||
fun TextView.onLongClickIgnoringLinks(listener: View.OnLongClickListener?) {
|
||||
if (listener == null) {
|
||||
setOnLongClickListener(null)
|
||||
} else {
|
||||
setOnLongClickListener(object : View.OnLongClickListener {
|
||||
override fun onLongClick(v: View): Boolean {
|
||||
if (hasLongPressedLink()) {
|
||||
return false
|
||||
}
|
||||
return listener.onLongClick(v)
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer that a Clickable span has been click by the presence of a selection
|
||||
*/
|
||||
private fun hasLongPressedLink() = selectionStart != -1 || selectionEnd != -1
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple Text listener lambda
|
||||
*/
|
||||
|
@ -19,6 +19,7 @@ package im.vector.app.core.intent
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import androidx.core.database.getStringOrNull
|
||||
import im.vector.lib.multipicker.utils.getColumnIndexOrNull
|
||||
|
||||
fun getFilenameFromUri(context: Context?, uri: Uri): String? {
|
||||
@ -27,7 +28,7 @@ fun getFilenameFromUri(context: Context?, uri: Uri): String? {
|
||||
?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
return cursor.getColumnIndexOrNull(OpenableColumns.DISPLAY_NAME)
|
||||
?.let { cursor.getString(it) }
|
||||
?.let { cursor.getStringOrNull(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,10 +17,15 @@
|
||||
package im.vector.app.core.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.TextView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import im.vector.app.R
|
||||
import im.vector.app.features.discovery.IdentityServerWithTerms
|
||||
import me.gujun.android.span.link
|
||||
import me.gujun.android.span.span
|
||||
|
||||
/**
|
||||
* Open a web view above the current activity.
|
||||
@ -40,16 +45,36 @@ fun Context.displayInWebView(url: String) {
|
||||
.show()
|
||||
}
|
||||
|
||||
fun Context.showIdentityServerConsentDialog(configuredIdentityServer: String?, policyLinkCallback: () -> Unit, consentCallBack: (() -> Unit)) {
|
||||
fun Context.showIdentityServerConsentDialog(identityServerWithTerms: IdentityServerWithTerms?,
|
||||
consentCallBack: (() -> Unit)) {
|
||||
// Build the message
|
||||
val content = span {
|
||||
+getString(R.string.identity_server_consent_dialog_content_3)
|
||||
+"\n\n"
|
||||
if (identityServerWithTerms?.policies?.isNullOrEmpty() == false) {
|
||||
span {
|
||||
textStyle = "bold"
|
||||
text = getString(R.string.settings_privacy_policy)
|
||||
}
|
||||
identityServerWithTerms.policies.forEach {
|
||||
+"\n • "
|
||||
// Use the url as the text too
|
||||
link(it.url, it.url)
|
||||
}
|
||||
+"\n\n"
|
||||
}
|
||||
+getString(R.string.identity_server_consent_dialog_content_question)
|
||||
}
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(getString(R.string.identity_server_consent_dialog_title_2, configuredIdentityServer ?: ""))
|
||||
.setMessage(R.string.identity_server_consent_dialog_content_2)
|
||||
.setPositiveButton(R.string.yes) { _, _ ->
|
||||
.setTitle(getString(R.string.identity_server_consent_dialog_title_2, identityServerWithTerms?.serverUrl.orEmpty()))
|
||||
.setMessage(content)
|
||||
.setPositiveButton(R.string.reactions_agree) { _, _ ->
|
||||
consentCallBack.invoke()
|
||||
}
|
||||
.setNeutralButton(R.string.identity_server_consent_dialog_neutral_policy) { _, _ ->
|
||||
policyLinkCallback.invoke()
|
||||
}
|
||||
.setNegativeButton(R.string.no, null)
|
||||
.setNegativeButton(R.string.action_not_now, null)
|
||||
.show()
|
||||
.apply {
|
||||
// Make the link(s) clickable. Must be called after show()
|
||||
(findViewById(android.R.id.message) as? TextView)?.movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
}
|
||||
|
@ -21,5 +21,6 @@ import im.vector.app.core.platform.VectorViewModelAction
|
||||
sealed class ContactsBookAction : VectorViewModelAction {
|
||||
data class FilterWith(val filter: String) : ContactsBookAction()
|
||||
data class OnlyBoundContacts(val onlyBoundContacts: Boolean) : ContactsBookAction()
|
||||
object UserConsentRequest : ContactsBookAction()
|
||||
object UserConsentGranted : ContactsBookAction()
|
||||
}
|
||||
|
@ -41,10 +41,6 @@ class ContactsBookController @Inject constructor(
|
||||
|
||||
var callback: Callback? = null
|
||||
|
||||
init {
|
||||
requestModelBuild()
|
||||
}
|
||||
|
||||
fun setData(state: ContactsBookViewState) {
|
||||
this.state = state
|
||||
requestModelBuild()
|
||||
|
@ -26,11 +26,11 @@ import com.airbnb.mvrx.activityViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.app.core.extensions.cleanup
|
||||
import im.vector.app.core.extensions.configureWith
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.extensions.hideKeyboard
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.utils.showIdentityServerConsentDialog
|
||||
import im.vector.app.databinding.FragmentContactsBookBinding
|
||||
import im.vector.app.features.navigation.SettingsActivityPayload
|
||||
import im.vector.app.features.userdirectory.PendingSelection
|
||||
import im.vector.app.features.userdirectory.UserListAction
|
||||
import im.vector.app.features.userdirectory.UserListSharedAction
|
||||
@ -68,21 +68,26 @@ class ContactsBookFragment @Inject constructor(
|
||||
setupConsentView()
|
||||
setupOnlyBoundContactsView()
|
||||
setupCloseView()
|
||||
contactsBookViewModel.observeViewEvents {
|
||||
when (it) {
|
||||
is ContactsBookViewEvents.Failure -> showFailure(it.throwable)
|
||||
is ContactsBookViewEvents.OnPoliciesRetrieved -> showConsentDialog(it)
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupConsentView() {
|
||||
views.phoneBookSearchForMatrixContacts.setOnClickListener {
|
||||
withState(contactsBookViewModel) { state ->
|
||||
contactsBookViewModel.handle(ContactsBookAction.UserConsentRequest)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showConsentDialog(event: ContactsBookViewEvents.OnPoliciesRetrieved) {
|
||||
requireContext().showIdentityServerConsentDialog(
|
||||
state.identityServerUrl,
|
||||
policyLinkCallback = {
|
||||
navigator.openSettings(requireContext(), SettingsActivityPayload.DiscoverySettings(expandIdentityPolicies = true))
|
||||
},
|
||||
event.identityServerWithTerms,
|
||||
consentCallBack = { contactsBookViewModel.handle(ContactsBookAction.UserConsentGranted) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupOnlyBoundContactsView() {
|
||||
views.phoneBookOnlyBoundContacts.checkedChanges()
|
||||
|
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.contactsbook
|
||||
|
||||
import im.vector.app.core.platform.VectorViewEvents
|
||||
import im.vector.app.features.discovery.IdentityServerWithTerms
|
||||
|
||||
sealed class ContactsBookViewEvents : VectorViewEvents {
|
||||
data class Failure(val throwable: Throwable) : ContactsBookViewEvents()
|
||||
data class OnPoliciesRetrieved(val identityServerWithTerms: IdentityServerWithTerms?) : ContactsBookViewEvents()
|
||||
}
|
@ -16,20 +16,21 @@
|
||||
|
||||
package im.vector.app.features.contactsbook
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.MavericksViewModelFactory
|
||||
import com.airbnb.mvrx.Success
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.contacts.ContactsDataSource
|
||||
import im.vector.app.core.contacts.MappedContact
|
||||
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.platform.EmptyViewEvents
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.discovery.fetchIdentityServerWithTerms
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
@ -37,11 +38,12 @@ import org.matrix.android.sdk.api.session.identity.IdentityServiceError
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
import timber.log.Timber
|
||||
|
||||
class ContactsBookViewModel @AssistedInject constructor(@Assisted
|
||||
initialState: ContactsBookViewState,
|
||||
class ContactsBookViewModel @AssistedInject constructor(
|
||||
@Assisted initialState: ContactsBookViewState,
|
||||
private val contactsDataSource: ContactsDataSource,
|
||||
private val session: Session) :
|
||||
VectorViewModel<ContactsBookViewState, ContactsBookAction, EmptyViewEvents>(initialState) {
|
||||
private val stringProvider: StringProvider,
|
||||
private val session: Session
|
||||
) : VectorViewModel<ContactsBookViewState, ContactsBookAction, ContactsBookViewEvents>(initialState) {
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : MavericksAssistedViewModelFactory<ContactsBookViewModel, ContactsBookViewState> {
|
||||
@ -162,9 +164,22 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted
|
||||
is ContactsBookAction.FilterWith -> handleFilterWith(action)
|
||||
is ContactsBookAction.OnlyBoundContacts -> handleOnlyBoundContacts(action)
|
||||
ContactsBookAction.UserConsentGranted -> handleUserConsentGranted()
|
||||
ContactsBookAction.UserConsentRequest -> handleUserConsentRequest()
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun handleUserConsentRequest() {
|
||||
viewModelScope.launch {
|
||||
val event = try {
|
||||
val result = session.fetchIdentityServerWithTerms(stringProvider.getString(R.string.resources_language))
|
||||
ContactsBookViewEvents.OnPoliciesRetrieved(result)
|
||||
} catch (throwable: Throwable) {
|
||||
ContactsBookViewEvents.Failure(throwable)
|
||||
}
|
||||
_viewEvents.post(event)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUserConsentGranted() {
|
||||
session.identityService().setUserConsent(true)
|
||||
|
||||
|
@ -26,10 +26,6 @@ class RoomDevToolRootController @Inject constructor(
|
||||
private val stringProvider: StringProvider
|
||||
) : EpoxyController() {
|
||||
|
||||
init {
|
||||
requestModelBuild()
|
||||
}
|
||||
|
||||
var interactionListener: DevToolsInteractionListener? = null
|
||||
|
||||
override fun buildModels() {
|
||||
|
@ -186,8 +186,7 @@ class DiscoverySettingsFragment @Inject constructor(
|
||||
if (newValue) {
|
||||
withState(viewModel) { state ->
|
||||
requireContext().showIdentityServerConsentDialog(
|
||||
state.identityServer.invoke()?.serverUrl,
|
||||
policyLinkCallback = { viewModel.handle(DiscoverySettingsAction.SetPoliciesExpandState(expanded = true)) },
|
||||
state.identityServer.invoke(),
|
||||
consentCallBack = { viewModel.handle(DiscoverySettingsAction.UpdateUserConsent(true)) }
|
||||
)
|
||||
}
|
||||
|
@ -29,10 +29,3 @@ data class DiscoverySettingsState(
|
||||
val userConsent: Boolean = false,
|
||||
val isIdentityPolicyUrlsExpanded: Boolean = false
|
||||
) : MavericksState
|
||||
|
||||
data class IdentityServerWithTerms(
|
||||
val serverUrl: String,
|
||||
val policies: List<IdentityServerPolicy>
|
||||
)
|
||||
|
||||
data class IdentityServerPolicy(val name: String, val url: String)
|
||||
|
@ -30,7 +30,6 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.core.utils.ensureProtocol
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
@ -39,7 +38,6 @@ import org.matrix.android.sdk.api.session.identity.IdentityServiceError
|
||||
import org.matrix.android.sdk.api.session.identity.IdentityServiceListener
|
||||
import org.matrix.android.sdk.api.session.identity.SharedState
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
import org.matrix.android.sdk.api.session.terms.TermsService
|
||||
import org.matrix.android.sdk.flow.flow
|
||||
|
||||
class DiscoverySettingsViewModel @AssistedInject constructor(
|
||||
@ -56,7 +54,6 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
|
||||
companion object : MavericksViewModelFactory<DiscoverySettingsViewModel, DiscoverySettingsState> by hiltMavericksViewModelFactory()
|
||||
|
||||
private val identityService = session.identityService()
|
||||
private val termsService: TermsService = session
|
||||
|
||||
private val identityServerManagerListener = object : IdentityServiceListener {
|
||||
override fun onIdentityServerChange() = withState { state ->
|
||||
@ -397,7 +394,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
runCatching { fetchIdentityServerWithTerms() }.fold(
|
||||
runCatching { session.fetchIdentityServerWithTerms(stringProvider.getString(R.string.resources_language)) }.fold(
|
||||
onSuccess = { setState { copy(identityServer = Success(it)) } },
|
||||
onFailure = { _viewEvents.post(DiscoverySettingsViewEvents.Failure(it)) }
|
||||
)
|
||||
@ -405,21 +402,6 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
|
||||
}
|
||||
|
||||
private suspend fun fetchIdentityServerWithTerms(): IdentityServerWithTerms? {
|
||||
val identityServerUrl = identityService.getCurrentIdentityServerUrl()
|
||||
return identityServerUrl?.let {
|
||||
val terms = termsService.getTerms(TermsService.ServiceType.IdentityService, identityServerUrl.ensureProtocol())
|
||||
.serverResponse
|
||||
.getLocalizedTerms(stringProvider.getString(R.string.resources_language))
|
||||
val policyUrls = terms.mapNotNull {
|
||||
val name = it.localizedName ?: it.policyName
|
||||
val url = it.localizedUrl
|
||||
if (name == null || url == null) {
|
||||
null
|
||||
} else {
|
||||
IdentityServerPolicy(name = name, url = url)
|
||||
}
|
||||
}
|
||||
IdentityServerWithTerms(identityServerUrl, policyUrls)
|
||||
}
|
||||
return session.fetchIdentityServerWithTerms(stringProvider.getString(R.string.resources_language))
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.discovery
|
||||
|
||||
import im.vector.app.core.utils.ensureProtocol
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.terms.TermsService
|
||||
|
||||
suspend fun Session.fetchIdentityServerWithTerms(userLanguage: String): IdentityServerWithTerms? {
|
||||
val identityServerUrl = identityService().getCurrentIdentityServerUrl()
|
||||
return identityServerUrl?.let {
|
||||
val terms = getTerms(TermsService.ServiceType.IdentityService, identityServerUrl.ensureProtocol())
|
||||
.serverResponse
|
||||
.getLocalizedTerms(userLanguage)
|
||||
val policyUrls = terms.mapNotNull {
|
||||
val name = it.localizedName ?: it.policyName
|
||||
val url = it.localizedUrl
|
||||
if (name == null || url == null) {
|
||||
null
|
||||
} else {
|
||||
IdentityServerPolicy(name = name, url = url)
|
||||
}
|
||||
}
|
||||
IdentityServerWithTerms(identityServerUrl, policyUrls)
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.discovery
|
||||
|
||||
data class IdentityServerWithTerms(
|
||||
val serverUrl: String,
|
||||
val policies: List<IdentityServerPolicy>
|
||||
)
|
||||
|
||||
data class IdentityServerPolicy(
|
||||
val name: String,
|
||||
val url: String
|
||||
)
|
@ -30,12 +30,6 @@ class BreadcrumbsController @Inject constructor(
|
||||
|
||||
private var viewState: BreadcrumbsViewState? = null
|
||||
|
||||
init {
|
||||
// We are requesting a model build directly as the first build of epoxy is on the main thread.
|
||||
// It avoids to build the whole list of breadcrumbs on the main thread.
|
||||
requestModelBuild()
|
||||
}
|
||||
|
||||
fun update(viewState: BreadcrumbsViewState) {
|
||||
this.viewState = viewState
|
||||
requestModelBuild()
|
||||
|
@ -398,6 +398,7 @@ class RoomDetailFragment @Inject constructor(
|
||||
is SendMode.Edit -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text)
|
||||
is SendMode.Quote -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.quote, mode.text)
|
||||
is SendMode.Reply -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text)
|
||||
is SendMode.Voice -> renderVoiceMessageMode(mode.text)
|
||||
}
|
||||
}
|
||||
|
||||
@ -473,6 +474,13 @@ class RoomDetailFragment @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderVoiceMessageMode(content: String) {
|
||||
ContentAttachmentData.fromJsonString(content)?.let { audioAttachmentData ->
|
||||
views.voiceMessageRecorderView.isVisible = true
|
||||
messageComposerViewModel.handle(MessageComposerAction.InitializeVoiceRecorder(audioAttachmentData))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSendButtonVisibilityChanged(event: MessageComposerViewEvents.AnimateSendButtonVisibility) {
|
||||
if (event.isVisible) {
|
||||
views.voiceMessageRecorderView.isVisible = false
|
||||
@ -509,7 +517,7 @@ class RoomDetailFragment @Inject constructor(
|
||||
|
||||
private fun onCannotRecord() {
|
||||
// Update the UI, cancel the animation
|
||||
messageComposerViewModel.handle(MessageComposerAction.OnVoiceRecordingUiStateChanged(RecordingUiState.None))
|
||||
messageComposerViewModel.handle(MessageComposerAction.OnVoiceRecordingUiStateChanged(RecordingUiState.Idle))
|
||||
}
|
||||
|
||||
private fun acceptIncomingCall(event: RoomDetailViewEvents.DisplayAndAcceptCall) {
|
||||
@ -703,7 +711,7 @@ class RoomDetailFragment @Inject constructor(
|
||||
if (checkPermissions(PERMISSIONS_FOR_VOICE_MESSAGE, requireActivity(), permissionVoiceMessageLauncher)) {
|
||||
messageComposerViewModel.handle(MessageComposerAction.StartRecordingVoiceMessage)
|
||||
vibrate(requireContext())
|
||||
updateRecordingUiState(RecordingUiState.Started(clock.epochMillis()))
|
||||
updateRecordingUiState(RecordingUiState.Recording(clock.epochMillis()))
|
||||
}
|
||||
}
|
||||
|
||||
@ -713,11 +721,12 @@ class RoomDetailFragment @Inject constructor(
|
||||
|
||||
override fun onVoiceRecordingCancelled() {
|
||||
messageComposerViewModel.handle(MessageComposerAction.EndRecordingVoiceMessage(isCancelled = true))
|
||||
updateRecordingUiState(RecordingUiState.Cancelled)
|
||||
vibrate(requireContext())
|
||||
updateRecordingUiState(RecordingUiState.Idle)
|
||||
}
|
||||
|
||||
override fun onVoiceRecordingLocked() {
|
||||
val startedState = withState(messageComposerViewModel) { it.voiceRecordingUiState as? RecordingUiState.Started }
|
||||
val startedState = withState(messageComposerViewModel) { it.voiceRecordingUiState as? RecordingUiState.Recording }
|
||||
val startTime = startedState?.recordingStartTimestamp ?: clock.epochMillis()
|
||||
updateRecordingUiState(RecordingUiState.Locked(startTime))
|
||||
}
|
||||
@ -728,22 +737,22 @@ class RoomDetailFragment @Inject constructor(
|
||||
|
||||
override fun onSendVoiceMessage() {
|
||||
messageComposerViewModel.handle(MessageComposerAction.EndRecordingVoiceMessage(isCancelled = false))
|
||||
updateRecordingUiState(RecordingUiState.None)
|
||||
updateRecordingUiState(RecordingUiState.Idle)
|
||||
}
|
||||
|
||||
override fun onDeleteVoiceMessage() {
|
||||
messageComposerViewModel.handle(MessageComposerAction.EndRecordingVoiceMessage(isCancelled = true))
|
||||
updateRecordingUiState(RecordingUiState.None)
|
||||
updateRecordingUiState(RecordingUiState.Idle)
|
||||
}
|
||||
|
||||
override fun onRecordingLimitReached() {
|
||||
messageComposerViewModel.handle(MessageComposerAction.PauseRecordingVoiceMessage)
|
||||
updateRecordingUiState(RecordingUiState.Playback)
|
||||
updateRecordingUiState(RecordingUiState.Draft)
|
||||
}
|
||||
|
||||
override fun onRecordingWaveformClicked() {
|
||||
messageComposerViewModel.handle(MessageComposerAction.PauseRecordingVoiceMessage)
|
||||
updateRecordingUiState(RecordingUiState.Playback)
|
||||
updateRecordingUiState(RecordingUiState.Draft)
|
||||
}
|
||||
|
||||
private fun updateRecordingUiState(state: RecordingUiState) {
|
||||
@ -1048,10 +1057,10 @@ class RoomDetailFragment @Inject constructor(
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun renderRegularMode(text: String) {
|
||||
private fun renderRegularMode(content: String) {
|
||||
autoCompleter.exitSpecialMode()
|
||||
views.composerLayout.collapse()
|
||||
views.composerLayout.setTextIfDifferent(text)
|
||||
views.composerLayout.setTextIfDifferent(content)
|
||||
views.composerLayout.views.sendButton.contentDescription = getString(R.string.send)
|
||||
}
|
||||
|
||||
@ -1141,10 +1150,7 @@ class RoomDetailFragment @Inject constructor(
|
||||
if (withState(messageComposerViewModel) { it.isVoiceRecording } && requireActivity().isChangingConfigurations) {
|
||||
// we're rotating, maintain any active recordings
|
||||
} else {
|
||||
messageComposerViewModel.handle(MessageComposerAction.SaveDraft(views.composerLayout.text.toString()))
|
||||
// We should improve the UX to support going into playback mode when paused and delete the media when the view is destroyed.
|
||||
messageComposerViewModel.handle(MessageComposerAction.EndAllVoiceActions(deleteRecord = false))
|
||||
views.voiceMessageRecorderView.render(RecordingUiState.None)
|
||||
messageComposerViewModel.handle(MessageComposerAction.OnEntersBackground(views.composerLayout.text.toString()))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,10 +18,10 @@ package im.vector.app.features.home.room.detail.composer
|
||||
|
||||
import im.vector.app.core.platform.VectorViewModelAction
|
||||
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView
|
||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
|
||||
|
||||
sealed class MessageComposerAction : VectorViewModelAction {
|
||||
data class SaveDraft(val draft: String) : MessageComposerAction()
|
||||
data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : MessageComposerAction()
|
||||
data class EnterEditMode(val eventId: String, val text: String) : MessageComposerAction()
|
||||
data class EnterQuoteMode(val eventId: String, val text: String) : MessageComposerAction()
|
||||
@ -29,8 +29,10 @@ sealed class MessageComposerAction : VectorViewModelAction {
|
||||
data class EnterRegularMode(val text: String, val fromSharing: Boolean) : MessageComposerAction()
|
||||
data class UserIsTyping(val isTyping: Boolean) : MessageComposerAction()
|
||||
data class OnTextChanged(val text: CharSequence) : MessageComposerAction()
|
||||
data class OnEntersBackground(val composerText: String) : MessageComposerAction()
|
||||
|
||||
// Voice Message
|
||||
data class InitializeVoiceRecorder(val attachmentData: ContentAttachmentData) : MessageComposerAction()
|
||||
data class OnVoiceRecordingUiStateChanged(val uiState: VoiceMessageRecorderView.RecordingUiState) : MessageComposerAction()
|
||||
object StartRecordingVoiceMessage : MessageComposerAction()
|
||||
data class EndRecordingVoiceMessage(val isCancelled: Boolean) : MessageComposerAction()
|
||||
|
@ -31,6 +31,7 @@ import im.vector.app.features.command.CommandParser
|
||||
import im.vector.app.features.command.ParsedCommand
|
||||
import im.vector.app.features.home.room.detail.ChatEffect
|
||||
import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator
|
||||
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView
|
||||
import im.vector.app.features.home.room.detail.toMessageType
|
||||
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
|
||||
import im.vector.app.features.session.coroutineScope
|
||||
@ -42,6 +43,7 @@ import org.commonmark.parser.Parser
|
||||
import org.commonmark.renderer.html.HtmlRenderer
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
@ -85,17 +87,18 @@ class MessageComposerViewModel @AssistedInject constructor(
|
||||
is MessageComposerAction.EnterQuoteMode -> handleEnterQuoteMode(action)
|
||||
is MessageComposerAction.EnterRegularMode -> handleEnterRegularMode(action)
|
||||
is MessageComposerAction.EnterReplyMode -> handleEnterReplyMode(action)
|
||||
is MessageComposerAction.SaveDraft -> handleSaveDraft(action)
|
||||
is MessageComposerAction.SendMessage -> handleSendMessage(action)
|
||||
is MessageComposerAction.UserIsTyping -> handleUserIsTyping(action)
|
||||
is MessageComposerAction.OnTextChanged -> handleOnTextChanged(action)
|
||||
is MessageComposerAction.OnVoiceRecordingUiStateChanged -> handleOnVoiceRecordingUiStateChanged(action)
|
||||
MessageComposerAction.StartRecordingVoiceMessage -> handleStartRecordingVoiceMessage()
|
||||
is MessageComposerAction.StartRecordingVoiceMessage -> handleStartRecordingVoiceMessage()
|
||||
is MessageComposerAction.EndRecordingVoiceMessage -> handleEndRecordingVoiceMessage(action.isCancelled)
|
||||
is MessageComposerAction.PlayOrPauseVoicePlayback -> handlePlayOrPauseVoicePlayback(action)
|
||||
MessageComposerAction.PauseRecordingVoiceMessage -> handlePauseRecordingVoiceMessage()
|
||||
MessageComposerAction.PlayOrPauseRecordingPlayback -> handlePlayOrPauseRecordingPlayback()
|
||||
is MessageComposerAction.EndAllVoiceActions -> handleEndAllVoiceActions(action.deleteRecord)
|
||||
is MessageComposerAction.InitializeVoiceRecorder -> handleInitializeVoiceRecorder(action.attachmentData)
|
||||
is MessageComposerAction.OnEntersBackground -> handleEntersBackground(action.composerText)
|
||||
}
|
||||
}
|
||||
|
||||
@ -432,6 +435,9 @@ class MessageComposerViewModel @AssistedInject constructor(
|
||||
popDraft()
|
||||
}
|
||||
}
|
||||
is SendMode.Voice -> {
|
||||
// do nothing
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
@ -455,22 +461,23 @@ class MessageComposerViewModel @AssistedInject constructor(
|
||||
copy(
|
||||
// Create a sendMode from a draft and retrieve the TimelineEvent
|
||||
sendMode = when (currentDraft) {
|
||||
is UserDraft.Regular -> SendMode.Regular(currentDraft.text, false)
|
||||
is UserDraft.Regular -> SendMode.Regular(currentDraft.content, false)
|
||||
is UserDraft.Quote -> {
|
||||
room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent ->
|
||||
SendMode.Quote(timelineEvent, currentDraft.text)
|
||||
SendMode.Quote(timelineEvent, currentDraft.content)
|
||||
}
|
||||
}
|
||||
is UserDraft.Reply -> {
|
||||
room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent ->
|
||||
SendMode.Reply(timelineEvent, currentDraft.text)
|
||||
SendMode.Reply(timelineEvent, currentDraft.content)
|
||||
}
|
||||
}
|
||||
is UserDraft.Edit -> {
|
||||
room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent ->
|
||||
SendMode.Edit(timelineEvent, currentDraft.text)
|
||||
SendMode.Edit(timelineEvent, currentDraft.content)
|
||||
}
|
||||
}
|
||||
is UserDraft.Voice -> SendMode.Voice(currentDraft.content)
|
||||
else -> null
|
||||
} ?: SendMode.Regular("", fromSharing = false)
|
||||
)
|
||||
@ -675,24 +682,24 @@ class MessageComposerViewModel @AssistedInject constructor(
|
||||
/**
|
||||
* Convert a send mode to a draft and save the draft
|
||||
*/
|
||||
private fun handleSaveDraft(action: MessageComposerAction.SaveDraft) = withState {
|
||||
private fun handleSaveTextDraft(draft: String) = withState {
|
||||
session.coroutineScope.launch {
|
||||
when {
|
||||
it.sendMode is SendMode.Regular && !it.sendMode.fromSharing -> {
|
||||
setState { copy(sendMode = it.sendMode.copy(action.draft)) }
|
||||
room.saveDraft(UserDraft.Regular(action.draft))
|
||||
setState { copy(sendMode = it.sendMode.copy(text = draft)) }
|
||||
room.saveDraft(UserDraft.Regular(draft))
|
||||
}
|
||||
it.sendMode is SendMode.Reply -> {
|
||||
setState { copy(sendMode = it.sendMode.copy(text = action.draft)) }
|
||||
room.saveDraft(UserDraft.Reply(it.sendMode.timelineEvent.root.eventId!!, action.draft))
|
||||
setState { copy(sendMode = it.sendMode.copy(text = draft)) }
|
||||
room.saveDraft(UserDraft.Reply(it.sendMode.timelineEvent.root.eventId!!, draft))
|
||||
}
|
||||
it.sendMode is SendMode.Quote -> {
|
||||
setState { copy(sendMode = it.sendMode.copy(text = action.draft)) }
|
||||
room.saveDraft(UserDraft.Quote(it.sendMode.timelineEvent.root.eventId!!, action.draft))
|
||||
setState { copy(sendMode = it.sendMode.copy(text = draft)) }
|
||||
room.saveDraft(UserDraft.Quote(it.sendMode.timelineEvent.root.eventId!!, draft))
|
||||
}
|
||||
it.sendMode is SendMode.Edit -> {
|
||||
setState { copy(sendMode = it.sendMode.copy(text = action.draft)) }
|
||||
room.saveDraft(UserDraft.Edit(it.sendMode.timelineEvent.root.eventId!!, action.draft))
|
||||
setState { copy(sendMode = it.sendMode.copy(text = draft)) }
|
||||
room.saveDraft(UserDraft.Edit(it.sendMode.timelineEvent.root.eventId!!, draft))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -700,7 +707,7 @@ class MessageComposerViewModel @AssistedInject constructor(
|
||||
|
||||
private fun handleStartRecordingVoiceMessage() {
|
||||
try {
|
||||
voiceMessageHelper.startRecording()
|
||||
voiceMessageHelper.startRecording(room.roomId)
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(MessageComposerViewEvents.VoicePlaybackOrRecordingFailure(failure))
|
||||
}
|
||||
@ -711,7 +718,7 @@ class MessageComposerViewModel @AssistedInject constructor(
|
||||
if (isCancelled) {
|
||||
voiceMessageHelper.deleteRecording()
|
||||
} else {
|
||||
voiceMessageHelper.stopRecording()?.let { audioType ->
|
||||
voiceMessageHelper.stopRecording(convertForSending = true)?.let { audioType ->
|
||||
if (audioType.duration > 1000) {
|
||||
room.sendMedia(audioType.toContentAttachmentData(isVoiceMessage = true), false, emptySet())
|
||||
} else {
|
||||
@ -719,6 +726,7 @@ class MessageComposerViewModel @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
handleEnterRegularMode(MessageComposerAction.EnterRegularMode(text = "", fromSharing = false))
|
||||
}
|
||||
|
||||
private fun handlePlayOrPauseVoicePlayback(action: MessageComposerAction.PlayOrPauseVoicePlayback) {
|
||||
@ -741,13 +749,35 @@ class MessageComposerViewModel @AssistedInject constructor(
|
||||
}
|
||||
|
||||
private fun handleEndAllVoiceActions(deleteRecord: Boolean) {
|
||||
voiceMessageHelper.clearTracker()
|
||||
voiceMessageHelper.stopAllVoiceActions(deleteRecord)
|
||||
}
|
||||
|
||||
private fun handleInitializeVoiceRecorder(attachmentData: ContentAttachmentData) {
|
||||
voiceMessageHelper.initializeRecorder(attachmentData)
|
||||
setState { copy(voiceRecordingUiState = VoiceMessageRecorderView.RecordingUiState.Draft) }
|
||||
}
|
||||
|
||||
private fun handlePauseRecordingVoiceMessage() {
|
||||
voiceMessageHelper.pauseRecording()
|
||||
}
|
||||
|
||||
private fun handleEntersBackground(composerText: String) {
|
||||
val isVoiceRecording = com.airbnb.mvrx.withState(this) { it.isVoiceRecording }
|
||||
if (isVoiceRecording) {
|
||||
voiceMessageHelper.clearTracker()
|
||||
viewModelScope.launch {
|
||||
voiceMessageHelper.stopAllVoiceActions(deleteRecord = false)?.toContentAttachmentData()?.let { voiceDraft ->
|
||||
val content = voiceDraft.toJsonString()
|
||||
room.saveDraft(UserDraft.Voice(content))
|
||||
setState { copy(sendMode = SendMode.Voice(content)) }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
handleSaveTextDraft(draft = composerText)
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchSlashCommandFlowSuspendable(block: suspend () -> Unit) {
|
||||
_viewEvents.post(MessageComposerViewEvents.SlashCommandLoading)
|
||||
viewModelScope.launch {
|
||||
|
@ -40,6 +40,7 @@ sealed interface SendMode {
|
||||
data class Quote(val timelineEvent: TimelineEvent, val text: String) : SendMode
|
||||
data class Edit(val timelineEvent: TimelineEvent, val text: String) : SendMode
|
||||
data class Reply(val timelineEvent: TimelineEvent, val text: String) : SendMode
|
||||
data class Voice(val text: String) : SendMode
|
||||
}
|
||||
|
||||
data class MessageComposerViewState(
|
||||
@ -47,15 +48,14 @@ data class MessageComposerViewState(
|
||||
val canSendMessage: Boolean = true,
|
||||
val isSendButtonVisible: Boolean = false,
|
||||
val sendMode: SendMode = SendMode.Regular("", false),
|
||||
val voiceRecordingUiState: VoiceMessageRecorderView.RecordingUiState = VoiceMessageRecorderView.RecordingUiState.None
|
||||
val voiceRecordingUiState: VoiceMessageRecorderView.RecordingUiState = VoiceMessageRecorderView.RecordingUiState.Idle
|
||||
) : MavericksState {
|
||||
|
||||
val isVoiceRecording = when (voiceRecordingUiState) {
|
||||
VoiceMessageRecorderView.RecordingUiState.None,
|
||||
VoiceMessageRecorderView.RecordingUiState.Cancelled,
|
||||
VoiceMessageRecorderView.RecordingUiState.Playback -> false
|
||||
VoiceMessageRecorderView.RecordingUiState.Idle -> false
|
||||
is VoiceMessageRecorderView.RecordingUiState.Locked,
|
||||
is VoiceMessageRecorderView.RecordingUiState.Started -> true
|
||||
VoiceMessageRecorderView.RecordingUiState.Draft,
|
||||
is VoiceMessageRecorderView.RecordingUiState.Recording -> true
|
||||
}
|
||||
|
||||
val isVoiceMessageIdle = !isVoiceRecording
|
||||
|
@ -30,6 +30,7 @@ import im.vector.lib.multipicker.entity.MultiPickerAudioType
|
||||
import im.vector.lib.multipicker.utils.toMultiPickerAudioType
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
@ -52,13 +53,22 @@ class VoiceMessageHelper @Inject constructor(
|
||||
private var amplitudeTicker: CountUpTimer? = null
|
||||
private var playbackTicker: CountUpTimer? = null
|
||||
|
||||
fun startRecording() {
|
||||
fun initializeRecorder(attachmentData: ContentAttachmentData) {
|
||||
voiceRecorder.initializeRecord(attachmentData)
|
||||
amplitudeList.clear()
|
||||
attachmentData.waveform?.let {
|
||||
amplitudeList.addAll(it)
|
||||
playbackTracker.updateCurrentRecording(VoiceMessagePlaybackTracker.RECORDING_ID, amplitudeList)
|
||||
}
|
||||
}
|
||||
|
||||
fun startRecording(roomId: String) {
|
||||
stopPlayback()
|
||||
playbackTracker.makeAllPlaybacksIdle()
|
||||
amplitudeList.clear()
|
||||
|
||||
try {
|
||||
voiceRecorder.startRecord()
|
||||
voiceRecorder.startRecord(roomId)
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure, "Unable to start recording")
|
||||
throw VoiceFailure.UnableToRecord(failure)
|
||||
@ -66,19 +76,24 @@ class VoiceMessageHelper @Inject constructor(
|
||||
startRecordingAmplitudes()
|
||||
}
|
||||
|
||||
fun stopRecording(): MultiPickerAudioType? {
|
||||
fun stopRecording(convertForSending: Boolean): MultiPickerAudioType? {
|
||||
tryOrNull("Cannot stop media recording amplitude") {
|
||||
stopRecordingAmplitudes()
|
||||
}
|
||||
val voiceMessageFile = tryOrNull("Cannot stop media recorder!") {
|
||||
voiceRecorder.stopRecord()
|
||||
if (convertForSending) {
|
||||
voiceRecorder.getVoiceMessageFile()
|
||||
} else {
|
||||
voiceRecorder.getCurrentRecord()
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
voiceMessageFile?.let {
|
||||
val outputFileUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileProvider", it, "Voice message.${it.extension}")
|
||||
return outputFileUri.toMultiPickerAudioType(context)
|
||||
return outputFileUri
|
||||
.toMultiPickerAudioType(context)
|
||||
?.apply {
|
||||
waveform = if (amplitudeList.size < 50) {
|
||||
amplitudeList
|
||||
@ -218,12 +233,16 @@ class VoiceMessageHelper @Inject constructor(
|
||||
playbackTicker = null
|
||||
}
|
||||
|
||||
fun stopAllVoiceActions(deleteRecord: Boolean = true) {
|
||||
stopRecording()
|
||||
fun clearTracker() {
|
||||
playbackTracker.clear()
|
||||
}
|
||||
|
||||
fun stopAllVoiceActions(deleteRecord: Boolean = true): MultiPickerAudioType? {
|
||||
val audioType = stopRecording(convertForSending = false)
|
||||
stopPlayback()
|
||||
if (deleteRecord) {
|
||||
deleteRecording()
|
||||
}
|
||||
playbackTracker.clear()
|
||||
return audioType
|
||||
}
|
||||
}
|
||||
|
@ -93,7 +93,14 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
|
||||
|
||||
override fun onSendVoiceMessage() = callback.onSendVoiceMessage()
|
||||
override fun onDeleteVoiceMessage() = callback.onDeleteVoiceMessage()
|
||||
override fun onWaveformClicked() = callback.onRecordingWaveformClicked()
|
||||
override fun onWaveformClicked() {
|
||||
when (lastKnownState) {
|
||||
RecordingUiState.Draft -> callback.onVoicePlaybackButtonClicked()
|
||||
is RecordingUiState.Recording,
|
||||
is RecordingUiState.Locked -> callback.onRecordingWaveformClicked()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onVoicePlaybackButtonClicked() = callback.onVoicePlaybackButtonClicked()
|
||||
override fun onMicButtonDrag(nextDragStateCreator: (DraggingState) -> DraggingState) {
|
||||
onDrag(dragState, newDragState = nextDragStateCreator(dragState))
|
||||
@ -112,19 +119,15 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
|
||||
fun render(recordingState: RecordingUiState) {
|
||||
if (lastKnownState == recordingState) return
|
||||
when (recordingState) {
|
||||
RecordingUiState.None -> {
|
||||
RecordingUiState.Idle -> {
|
||||
reset()
|
||||
}
|
||||
is RecordingUiState.Started -> {
|
||||
is RecordingUiState.Recording -> {
|
||||
startRecordingTicker(startFromLocked = false, startAt = recordingState.recordingStartTimestamp)
|
||||
voiceMessageViews.renderToast(context.getString(R.string.voice_message_release_to_send_toast))
|
||||
voiceMessageViews.showRecordingViews()
|
||||
dragState = DraggingState.Ready
|
||||
}
|
||||
RecordingUiState.Cancelled -> {
|
||||
reset()
|
||||
vibrate(context)
|
||||
}
|
||||
is RecordingUiState.Locked -> {
|
||||
if (lastKnownState == null) {
|
||||
startRecordingTicker(startFromLocked = true, startAt = recordingState.recordingStartTimestamp)
|
||||
@ -134,9 +137,9 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
|
||||
voiceMessageViews.showRecordingLockedViews(recordingState)
|
||||
}, 500)
|
||||
}
|
||||
RecordingUiState.Playback -> {
|
||||
RecordingUiState.Draft -> {
|
||||
stopRecordingTicker()
|
||||
voiceMessageViews.showPlaybackViews()
|
||||
voiceMessageViews.showDraftViews()
|
||||
}
|
||||
}
|
||||
lastKnownState = recordingState
|
||||
@ -220,11 +223,10 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
sealed interface RecordingUiState {
|
||||
object None : RecordingUiState
|
||||
data class Started(val recordingStartTimestamp: Long) : RecordingUiState
|
||||
object Cancelled : RecordingUiState
|
||||
object Idle : RecordingUiState
|
||||
data class Recording(val recordingStartTimestamp: Long) : RecordingUiState
|
||||
data class Locked(val recordingStartTimestamp: Long) : RecordingUiState
|
||||
object Playback : RecordingUiState
|
||||
object Draft : RecordingUiState
|
||||
}
|
||||
|
||||
sealed interface DraggingState {
|
||||
|
@ -23,9 +23,11 @@ import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.view.doOnLayout
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import com.visualizer.amplitude.AudioRecordView
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.setAttributeBackground
|
||||
import im.vector.app.core.extensions.setAttributeTintedBackground
|
||||
@ -195,7 +197,7 @@ class VoiceMessageViews(
|
||||
}
|
||||
|
||||
// Hide toasts if user cancelled recording before the timeout of the toast.
|
||||
if (recordingState == RecordingUiState.Cancelled || recordingState == RecordingUiState.None) {
|
||||
if (recordingState == RecordingUiState.Idle) {
|
||||
hideToast()
|
||||
}
|
||||
}
|
||||
@ -258,6 +260,16 @@ class VoiceMessageViews(
|
||||
views.voiceMessageToast.isVisible = false
|
||||
}
|
||||
|
||||
fun showDraftViews() {
|
||||
hideRecordingViews(RecordingUiState.Idle)
|
||||
views.voiceMessageMicButton.isVisible = false
|
||||
views.voiceMessageSendButton.isVisible = true
|
||||
views.voiceMessagePlaybackLayout.isVisible = true
|
||||
views.voiceMessagePlaybackTimerIndicator.isVisible = false
|
||||
views.voicePlaybackControlButton.isVisible = true
|
||||
views.voicePlaybackWaveform.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
||||
}
|
||||
|
||||
fun showRecordingLockedViews(recordingState: RecordingUiState) {
|
||||
hideRecordingViews(recordingState)
|
||||
views.voiceMessagePlaybackLayout.isVisible = true
|
||||
@ -268,14 +280,8 @@ class VoiceMessageViews(
|
||||
renderToast(resources.getString(R.string.voice_message_tap_to_stop_toast))
|
||||
}
|
||||
|
||||
fun showPlaybackViews() {
|
||||
views.voiceMessagePlaybackTimerIndicator.isVisible = false
|
||||
views.voicePlaybackControlButton.isVisible = true
|
||||
views.voicePlaybackWaveform.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
||||
}
|
||||
|
||||
fun initViews() {
|
||||
hideRecordingViews(RecordingUiState.None)
|
||||
hideRecordingViews(RecordingUiState.Idle)
|
||||
views.voiceMessageMicButton.isVisible = true
|
||||
views.voiceMessageSendButton.isVisible = false
|
||||
views.voicePlaybackWaveform.post { views.voicePlaybackWaveform.recreate() }
|
||||
@ -320,11 +326,9 @@ class VoiceMessageViews(
|
||||
}
|
||||
|
||||
fun renderRecordingWaveform(amplitudeList: Array<Int>) {
|
||||
views.voicePlaybackWaveform.post {
|
||||
views.voicePlaybackWaveform.apply {
|
||||
views.voicePlaybackWaveform.doOnLayout { waveFormView ->
|
||||
amplitudeList.iterator().forEach {
|
||||
update(it)
|
||||
}
|
||||
(waveFormView as AudioRecordView).update(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -141,9 +141,4 @@ class SearchViewModel @AssistedInject constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
currentTask?.cancel()
|
||||
super.onCleared()
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.onClick
|
||||
import im.vector.app.core.epoxy.onLongClickIgnoringLinks
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.tools.findPillsAndProcess
|
||||
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
||||
@ -94,10 +95,9 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
||||
}
|
||||
super.bind(holder)
|
||||
holder.messageView.movementMethod = movementMethod
|
||||
|
||||
renderSendState(holder.messageView, holder.messageView)
|
||||
holder.messageView.onClick(attributes.itemClickListener)
|
||||
holder.messageView.setOnLongClickListener(attributes.itemLongClickListener)
|
||||
holder.messageView.onLongClickIgnoringLinks(attributes.itemLongClickListener)
|
||||
|
||||
if (canUseTextFuture) {
|
||||
holder.messageView.setTextFuture(textFuture)
|
||||
@ -133,6 +133,7 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
||||
previewUrlView?.render(state, safeImageContentRenderer)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val STUB_ID = R.id.messageContentTextStub
|
||||
}
|
||||
|
@ -231,9 +231,4 @@ class RoomDirectoryViewModel @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
currentJob?.cancel()
|
||||
super.onCleared()
|
||||
}
|
||||
}
|
||||
|
@ -46,10 +46,6 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor
|
||||
var callback: Callback? = null
|
||||
private var viewState: DevicesViewState? = null
|
||||
|
||||
init {
|
||||
requestModelBuild()
|
||||
}
|
||||
|
||||
fun update(viewState: DevicesViewState) {
|
||||
this.viewState = viewState
|
||||
requestModelBuild()
|
||||
|
@ -31,10 +31,6 @@ class IgnoredUsersController @Inject constructor(private val stringProvider: Str
|
||||
var callback: Callback? = null
|
||||
private var viewState: IgnoredUsersViewState? = null
|
||||
|
||||
init {
|
||||
requestModelBuild()
|
||||
}
|
||||
|
||||
fun update(viewState: IgnoredUsersViewState) {
|
||||
this.viewState = viewState
|
||||
requestModelBuild()
|
||||
|
@ -45,12 +45,6 @@ class SoftLogoutController @Inject constructor(
|
||||
|
||||
private var viewState: SoftLogoutViewState? = null
|
||||
|
||||
init {
|
||||
// We are requesting a model build directly as the first build of epoxy is on the main thread.
|
||||
// It avoids to build the whole list of breadcrumbs on the main thread.
|
||||
requestModelBuild()
|
||||
}
|
||||
|
||||
fun update(viewState: SoftLogoutViewState) {
|
||||
this.viewState = viewState
|
||||
requestModelBuild()
|
||||
|
@ -47,10 +47,6 @@ class SpaceSummaryController @Inject constructor(
|
||||
|
||||
private val subSpaceComparator: Comparator<SpaceChildInfo> = compareBy<SpaceChildInfo> { it.order }.thenBy { it.childRoomId }
|
||||
|
||||
init {
|
||||
requestModelBuild()
|
||||
}
|
||||
|
||||
fun update(viewState: SpaceListViewState) {
|
||||
this.viewState = viewState
|
||||
requestModelBuild()
|
||||
|
@ -24,6 +24,7 @@ sealed class UserListAction : VectorViewModelAction {
|
||||
data class AddPendingSelection(val pendingSelection: PendingSelection) : UserListAction()
|
||||
data class RemovePendingSelection(val pendingSelection: PendingSelection) : UserListAction()
|
||||
object ComputeMatrixToLinkForSharing : UserListAction()
|
||||
object UserConsentRequest : UserListAction()
|
||||
data class UpdateUserConsent(val consent: Boolean) : UserListAction()
|
||||
object Resumed : UserListAction()
|
||||
}
|
||||
|
@ -42,7 +42,6 @@ import im.vector.app.core.utils.showIdentityServerConsentDialog
|
||||
import im.vector.app.core.utils.startSharePlainTextIntent
|
||||
import im.vector.app.databinding.FragmentUserListBinding
|
||||
import im.vector.app.features.homeserver.HomeServerCapabilitiesViewModel
|
||||
import im.vector.app.features.navigation.SettingsActivityPayload
|
||||
import im.vector.app.features.settings.VectorSettingsActivity
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@ -103,6 +102,8 @@ class UserListFragment @Inject constructor(
|
||||
extraTitle = getString(R.string.invite_friends_rich_title)
|
||||
)
|
||||
}
|
||||
is UserListViewEvents.Failure -> showFailure(it.throwable)
|
||||
is UserListViewEvents.OnPoliciesRetrieved -> showConsentDialog(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -231,16 +232,15 @@ class UserListFragment @Inject constructor(
|
||||
}
|
||||
|
||||
override fun giveIdentityServerConsent() {
|
||||
withState(viewModel) { state ->
|
||||
viewModel.handle(UserListAction.UserConsentRequest)
|
||||
}
|
||||
|
||||
private fun showConsentDialog(event: UserListViewEvents.OnPoliciesRetrieved) {
|
||||
requireContext().showIdentityServerConsentDialog(
|
||||
state.configuredIdentityServer,
|
||||
policyLinkCallback = {
|
||||
navigator.openSettings(requireContext(), SettingsActivityPayload.DiscoverySettings(expandIdentityPolicies = true))
|
||||
},
|
||||
event.identityServerWithTerms,
|
||||
consentCallBack = { viewModel.handle(UserListAction.UpdateUserConsent(true)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUseQRCode() {
|
||||
view?.hideKeyboard()
|
||||
|
@ -17,10 +17,13 @@
|
||||
package im.vector.app.features.userdirectory
|
||||
|
||||
import im.vector.app.core.platform.VectorViewEvents
|
||||
import im.vector.app.features.discovery.IdentityServerWithTerms
|
||||
|
||||
/**
|
||||
* Transient events for invite users to room screen
|
||||
*/
|
||||
sealed class UserListViewEvents : VectorViewEvents {
|
||||
data class Failure(val throwable: Throwable) : UserListViewEvents()
|
||||
data class OnPoliciesRetrieved(val identityServerWithTerms: IdentityServerWithTerms?) : UserListViewEvents()
|
||||
data class OpenShareMatrixToLink(val link: String) : UserListViewEvents()
|
||||
}
|
||||
|
@ -23,12 +23,15 @@ import com.airbnb.mvrx.Uninitialized
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.extensions.isEmail
|
||||
import im.vector.app.core.extensions.toggle
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.discovery.fetchIdentityServerWithTerms
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.filter
|
||||
@ -36,6 +39,7 @@ import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.sample
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.MatrixPatterns
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
@ -51,9 +55,11 @@ data class ThreePidUser(
|
||||
val user: User?
|
||||
)
|
||||
|
||||
class UserListViewModel @AssistedInject constructor(@Assisted initialState: UserListViewState,
|
||||
private val session: Session) :
|
||||
VectorViewModel<UserListViewState, UserListAction, UserListViewEvents>(initialState) {
|
||||
class UserListViewModel @AssistedInject constructor(
|
||||
@Assisted initialState: UserListViewState,
|
||||
private val stringProvider: StringProvider,
|
||||
private val session: Session
|
||||
) : VectorViewModel<UserListViewState, UserListAction, UserListViewEvents>(initialState) {
|
||||
|
||||
private val knownUsersSearch = MutableStateFlow("")
|
||||
private val directoryUsersSearch = MutableStateFlow("")
|
||||
@ -104,11 +110,24 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User
|
||||
is UserListAction.AddPendingSelection -> handleSelectUser(action)
|
||||
is UserListAction.RemovePendingSelection -> handleRemoveSelectedUser(action)
|
||||
UserListAction.ComputeMatrixToLinkForSharing -> handleShareMyMatrixToLink()
|
||||
UserListAction.UserConsentRequest -> handleUserConsentRequest()
|
||||
is UserListAction.UpdateUserConsent -> handleISUpdateConsent(action)
|
||||
UserListAction.Resumed -> handleResumed()
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun handleUserConsentRequest() {
|
||||
viewModelScope.launch {
|
||||
val event = try {
|
||||
val result = session.fetchIdentityServerWithTerms(stringProvider.getString(R.string.resources_language))
|
||||
UserListViewEvents.OnPoliciesRetrieved(result)
|
||||
} catch (throwable: Throwable) {
|
||||
UserListViewEvents.Failure(throwable)
|
||||
}
|
||||
_viewEvents.post(event)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleISUpdateConsent(action: UserListAction.UpdateUserConsent) {
|
||||
session.identityService().setUserConsent(action.consent)
|
||||
withState {
|
||||
|
@ -21,6 +21,7 @@ import android.media.MediaRecorder
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||
import org.matrix.android.sdk.internal.util.md5
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.util.UUID
|
||||
@ -60,9 +61,17 @@ abstract class AbstractVoiceRecorder(
|
||||
}
|
||||
}
|
||||
|
||||
override fun startRecord() {
|
||||
override fun initializeRecord(attachmentData: ContentAttachmentData) {
|
||||
outputFile = attachmentData.findVoiceFile(outputDirectory)
|
||||
}
|
||||
|
||||
override fun startRecord(roomId: String) {
|
||||
init()
|
||||
outputFile = File(outputDirectory, "${UUID.randomUUID()}.$filenameExt")
|
||||
val fileName = "${UUID.randomUUID()}.$filenameExt"
|
||||
val outputDirectoryForRoom = File(outputDirectory, roomId.md5()).apply {
|
||||
mkdirs()
|
||||
}
|
||||
outputFile = File(outputDirectoryForRoom, fileName)
|
||||
|
||||
val mr = mediaRecorder ?: return
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
@ -104,7 +113,6 @@ abstract class AbstractVoiceRecorder(
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNUSED") // preemptively added for https://github.com/vector-im/element-android/pull/4527
|
||||
private fun ContentAttachmentData.findVoiceFile(baseDirectory: File): File {
|
||||
return File(baseDirectory, queryUri.takePathAfter(baseDirectory.name))
|
||||
}
|
||||
|
@ -16,13 +16,21 @@
|
||||
|
||||
package im.vector.app.features.voice
|
||||
|
||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||
import java.io.File
|
||||
|
||||
interface VoiceRecorder {
|
||||
/**
|
||||
* Start the recording
|
||||
* Initialize recording with a pre-recorded file.
|
||||
* @param attachmentData data of the recorded file
|
||||
*/
|
||||
fun startRecord()
|
||||
fun initializeRecord(attachmentData: ContentAttachmentData)
|
||||
|
||||
/**
|
||||
* Start the recording
|
||||
* @param roomId id of the room to start record
|
||||
*/
|
||||
fun startRecord(roomId: String)
|
||||
|
||||
/**
|
||||
* Stop the recording
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user