Merge branch 'develop' into feature/fga/timeline_chunks_rework

This commit is contained in:
ganfra 2021-12-03 12:55:57 +01:00
commit 8ca60eadbb
306 changed files with 2232 additions and 1255 deletions

View file

@ -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, '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, 'S-Critical') &&
(contains(github.event.issue.labels.*.name, 'O-Frequent') ||
contains(github.event.issue.labels.*.name, 'O-Occasional')) ||
contains(github.event.issue.labels.*.name, 'S-Major') &&
contains(github.event.issue.labels.*.name, 'O-Frequent') ||
contains(github.event.issue.labels.*.name, 'A11y') &&
contains(github.event.issue.labels.*.name, 'O-Frequent'))
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')

View file

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

View file

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

View file

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

View file

@ -1 +0,0 @@
Fixing queued voice message failing to send or retry

View file

@ -1 +0,0 @@
Keeping device screen on whilst recording and playing back voice messages

View file

@ -1 +0,0 @@
Allow voice messages to continue recording during device rotation

View file

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

View file

@ -1 +0,0 @@
Make Element Android Thread aware

1
changelog.d/4324.bugfix Normal file
View file

@ -0,0 +1 @@
Fixes message menu showing when copying message urls

View file

@ -1 +0,0 @@
Make the verification shields the same in Element Web and Element Android

View file

@ -1 +0,0 @@
Fix a display issue in the composer when the replied message is changed.

View file

@ -1 +0,0 @@
Dismissing the Fdroid variant Listening for notifications on sign out, fixes crash when tapping the notification when signed out

View file

@ -1 +0,0 @@
Upgrade Jitsi lib (and so webrtc) from Jitsi android-sdk-3.1.0 to android-sdk-3.10.0

View file

@ -1 +0,0 @@
Improve crypto logs to help debug decryption failures

View file

@ -1 +0,0 @@
Voice recording mic button refactor with small animation tweaks in preparation for voice drafts

View file

@ -1 +0,0 @@
Fix a crash when displaying the bootstrap bottom sheet

View file

@ -1 +0,0 @@
Remove duplicated settings declaration

View file

@ -1 +0,0 @@
Fixes .ogg files failing to upload to rooms

1
changelog.d/4602.misc Normal file
View file

@ -0,0 +1 @@
There is no need to call job.cancel() when we are using viewModelScope()

1
changelog.d/4604.misc Normal file
View file

@ -0,0 +1 @@
Cleanup the layout files

1
changelog.d/4617.misc Normal file
View file

@ -0,0 +1 @@
Improve issue automation workflows

View file

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

View 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

View 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

View 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

View 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

View file

@ -0,0 +1,2 @@
تغییرات اصلی در این نگارش: افزودن پشتیبانی حضور برای اتاق‌های پیام مستقیم (یادداشت: حضور روی matrix.org از کار افتاده است). افزودن دوبارهٔ پشتیبانی اندروید خودرو.
گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.3.5

View file

@ -0,0 +1,2 @@
تغییرات اصلی در این نگارش: افزودن پشتیبانی حضور برای اتاق‌های پیام مستقیم (یادداشت: حضور روی matrix.org از کار افتاده است). افزودن دوبارهٔ پشتیبانی اندروید خودرو.
گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.3.6

View file

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

View 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

View 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

View 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

View 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

View 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

View 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

View file

@ -0,0 +1 @@
Grupowy komunikator - szyfrowane wiadomosci, grupowe czaty oraz rozmowy wideo

View file

@ -0,0 +1 @@
Element - Bezpieczny Komunikator

View 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

View 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

View file

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

View file

@ -0,0 +1,2 @@
此版本的主要变化:主要关于通知的错误修复。
完整更新日志https://github.com/vector-im/element-android/releases/tag/v1.3.7

View file

@ -0,0 +1,2 @@
此版本中的主要變動:主要關於通知的臭蟲修復。
完整的變更紀錄https://github.com/vector-im/element-android/releases/tag/v1.3.7

View file

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

View file

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

View file

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

View file

@ -11,5 +11,3 @@
<changeImageTransform />
</transitionSet>

View file

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

View file

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

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="BadgeFloatingActionButton">

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -105,8 +105,6 @@
suggest="${underscoreToCamelCase(classToResource(fragmentClass))}ViewEvents"
default="MainViewEvents"
help="The name of the view events to create" />
<parameter
id="packageName"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -41,10 +41,6 @@ class ContactsBookController @Inject constructor(
var callback: Callback? = null
init {
requestModelBuild()
}
fun setData(state: ContactsBookViewState) {
this.state = state
requestModelBuild()

View file

@ -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,22 +68,27 @@ 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 ->
requireContext().showIdentityServerConsentDialog(
state.identityServerUrl,
policyLinkCallback = {
navigator.openSettings(requireContext(), SettingsActivityPayload.DiscoverySettings(expandIdentityPolicies = true))
},
consentCallBack = { contactsBookViewModel.handle(ContactsBookAction.UserConsentGranted) }
)
}
contactsBookViewModel.handle(ContactsBookAction.UserConsentRequest)
}
}
private fun showConsentDialog(event: ContactsBookViewEvents.OnPoliciesRetrieved) {
requireContext().showIdentityServerConsentDialog(
event.identityServerWithTerms,
consentCallBack = { contactsBookViewModel.handle(ContactsBookAction.UserConsentGranted) }
)
}
private fun setupOnlyBoundContactsView() {
views.phoneBookOnlyBoundContacts.checkedChanges()
.onEach {

View file

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

View file

@ -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,
private val contactsDataSource: ContactsDataSource,
private val session: Session) :
VectorViewModel<ContactsBookViewState, ContactsBookAction, EmptyViewEvents>(initialState) {
class ContactsBookViewModel @AssistedInject constructor(
@Assisted initialState: ContactsBookViewState,
private val contactsDataSource: ContactsDataSource,
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)

View file

@ -26,10 +26,6 @@ class RoomDevToolRootController @Inject constructor(
private val stringProvider: StringProvider
) : EpoxyController() {
init {
requestModelBuild()
}
var interactionListener: DevToolsInteractionListener? = null
override fun buildModels() {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,20 +119,16 @@ 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 -> {
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 {

View file

@ -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 {
amplitudeList.iterator().forEach {
update(it)
}
views.voicePlaybackWaveform.doOnLayout { waveFormView ->
amplitudeList.iterator().forEach {
(waveFormView as AudioRecordView).update(it)
}
}
}

View file

@ -141,9 +141,4 @@ class SearchViewModel @AssistedInject constructor(
)
}
}
override fun onCleared() {
currentTask?.cancel()
super.onCleared()
}
}

View file

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

View file

@ -231,9 +231,4 @@ class RoomDirectoryViewModel @AssistedInject constructor(
}
}
}
override fun onCleared() {
currentJob?.cancel()
super.onCleared()
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,15 +232,14 @@ class UserListFragment @Inject constructor(
}
override fun giveIdentityServerConsent() {
withState(viewModel) { state ->
requireContext().showIdentityServerConsentDialog(
state.configuredIdentityServer,
policyLinkCallback = {
navigator.openSettings(requireContext(), SettingsActivityPayload.DiscoverySettings(expandIdentityPolicies = true))
},
consentCallBack = { viewModel.handle(UserListAction.UpdateUserConsent(true)) }
)
}
viewModel.handle(UserListAction.UserConsentRequest)
}
private fun showConsentDialog(event: UserListViewEvents.OnPoliciesRetrieved) {
requireContext().showIdentityServerConsentDialog(
event.identityServerWithTerms,
consentCallBack = { viewModel.handle(UserListAction.UpdateUserConsent(true)) }
)
}
override fun onUseQRCode() {

View file

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

View file

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

View file

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

View file

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