diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000000..3126b47a07
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,5 @@
+# Reporting a Vulnerability
+
+**If you've found a security vulnerability, please report it to security@matrix.org**
+
+For more information on our security disclosure policy, visit https://www.matrix.org/security-disclosure-policy/
diff --git a/build.gradle b/build.gradle
index 53b7a983ec..850a4143c9 100644
--- a/build.gradle
+++ b/build.gradle
@@ -45,7 +45,7 @@ plugins {
// Detekt
id "io.gitlab.arturbosch.detekt" version "1.22.0"
// Ksp
- id "com.google.devtools.ksp" version "1.7.22-1.0.8"
+ id "com.google.devtools.ksp" version "1.8.0-1.0.8"
// Dependency Analysis
id 'com.autonomousapps.dependency-analysis' version "1.18.0"
diff --git a/changelog.d/4025.bugfix b/changelog.d/4025.bugfix
new file mode 100644
index 0000000000..109da1c830
--- /dev/null
+++ b/changelog.d/4025.bugfix
@@ -0,0 +1 @@
+Fix can't get out of a verification dialog
diff --git a/changelog.d/7824.feature b/changelog.d/7824.feature
new file mode 100644
index 0000000000..3c8b416571
--- /dev/null
+++ b/changelog.d/7824.feature
@@ -0,0 +1 @@
+[Poll] Warning message on decryption failure of some events
diff --git a/changelog.d/7829.bugfix b/changelog.d/7829.bugfix
new file mode 100644
index 0000000000..705f7310f0
--- /dev/null
+++ b/changelog.d/7829.bugfix
@@ -0,0 +1 @@
+Handle exceptions when listening a voice broadcast
diff --git a/changelog.d/7832.bugfix b/changelog.d/7832.bugfix
new file mode 100644
index 0000000000..871f9aabb9
--- /dev/null
+++ b/changelog.d/7832.bugfix
@@ -0,0 +1 @@
+[Voice Broadcast] Fix unexpected "live broadcast" in the room list
diff --git a/changelog.d/7845.wip b/changelog.d/7845.wip
new file mode 100644
index 0000000000..8bce21499a
--- /dev/null
+++ b/changelog.d/7845.wip
@@ -0,0 +1 @@
+[Voice Broadcast] Only display a notification on the first voice chunk
diff --git a/changelog.d/7864.wip b/changelog.d/7864.wip
new file mode 100644
index 0000000000..9d719d92ff
--- /dev/null
+++ b/changelog.d/7864.wip
@@ -0,0 +1 @@
+[Poll] History list: Load more UI mechanism
diff --git a/changelog.d/7936.misc b/changelog.d/7936.misc
new file mode 100644
index 0000000000..8480d9a6bf
--- /dev/null
+++ b/changelog.d/7936.misc
@@ -0,0 +1 @@
+Upgrade to Kotlin 1.8
diff --git a/changelog.d/7938.bugfix b/changelog.d/7938.bugfix
new file mode 100644
index 0000000000..70218edf8a
--- /dev/null
+++ b/changelog.d/7938.bugfix
@@ -0,0 +1 @@
+Fix rendering of edited polls
diff --git a/coverage.gradle b/coverage.gradle
index 2c0af25368..421c500728 100644
--- a/coverage.gradle
+++ b/coverage.gradle
@@ -80,12 +80,12 @@ task generateCoverageReport(type: JacocoReport) {
task unitTestsWithCoverage(type: GradleBuild) {
// the 7.1.3 android gradle plugin has a bug where enableTestCoverage generates invalid coverage
- startParameter.projectProperties.coverage = [enableTestCoverage: false]
+ startParameter.projectProperties.coverage = "false"
tasks = ['testDebugUnitTest']
}
task instrumentationTestsWithCoverage(type: GradleBuild) {
- startParameter.projectProperties.coverage = [enableTestCoverage: true]
+ startParameter.projectProperties.coverage = "true"
startParameter.projectProperties['android.testInstrumentationRunnerArguments.notPackage'] = 'im.vector.app.ui'
tasks = [':vector-app:connectedGplayDebugAndroidTest', ':vector:connectedDebugAndroidTest', 'matrix-sdk-android:connectedDebugAndroidTest']
}
diff --git a/dependencies.gradle b/dependencies.gradle
index 25785e984e..4977543822 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -8,7 +8,7 @@ ext.versions = [
def gradle = "7.3.1"
// Ref: https://kotlinlang.org/releases.html
-def kotlin = "1.7.22"
+def kotlin = "1.8.0"
def kotlinCoroutines = "1.6.4"
def dagger = "2.44.2"
def firebaseBom = "31.1.1"
@@ -18,7 +18,7 @@ def markwon = "4.6.2"
def moshi = "1.14.0"
def lifecycle = "2.5.1"
def flowBinding = "1.2.0"
-def flipper = "0.176.1"
+def flipper = "0.177.0"
def epoxy = "5.0.0"
def mavericks = "3.0.1"
def glide = "4.14.2"
@@ -28,11 +28,12 @@ def jjwt = "0.11.5"
// the whole commit which set version 0.16.0-SNAPSHOT
def vanniktechEmoji = "0.16.0-SNAPSHOT"
def sentry = "6.11.0"
-def fragment = "1.5.5"
+// Use 1.6.0 alpha to fix issue with test
+def fragment = "1.6.0-alpha04"
// Testing
def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819
-def espresso = "3.4.0"
-def androidxTest = "1.4.0"
+def espresso = "3.5.1"
+def androidxTest = "1.5.0"
def androidxOrchestrator = "1.4.2"
def paparazzi = "1.1.0"
@@ -49,13 +50,14 @@ ext.libs = [
],
androidx : [
'activity' : "androidx.activity:activity-ktx:1.6.1",
- 'appCompat' : "androidx.appcompat:appcompat:1.5.1",
+ 'appCompat' : "androidx.appcompat:appcompat:1.6.0",
'biometric' : "androidx.biometric:biometric:1.1.0",
'core' : "androidx.core:core-ktx:1.9.0",
'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1",
'exifinterface' : "androidx.exifinterface:exifinterface:1.3.5",
'fragmentKtx' : "androidx.fragment:fragment-ktx:$fragment",
'fragmentTesting' : "androidx.fragment:fragment-testing:$fragment",
+ 'fragmentTestingManifest' : "androidx.fragment:fragment-testing-manifest:$fragment",
'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.4",
'work' : "androidx.work:work-runtime-ktx:2.7.1",
'autoFill' : "androidx.autofill:autofill:1.1.0",
@@ -101,7 +103,7 @@ ext.libs = [
],
element : [
'opusencoder' : "io.element.android:opusencoder:1.1.0",
- 'wysiwyg' : "io.element.android:wysiwyg:0.15.0"
+ 'wysiwyg' : "io.element.android:wysiwyg:0.18.0"
],
squareup : [
'moshi' : "com.squareup.moshi:moshi:$moshi",
diff --git a/library/ui-strings/src/main/res/values-cs/strings.xml b/library/ui-strings/src/main/res/values-cs/strings.xml
index 0a7998deaa..2d2b91d645 100644
--- a/library/ui-strings/src/main/res/values-cs/strings.xml
+++ b/library/ui-strings/src/main/res/values-cs/strings.xml
@@ -2860,7 +2860,7 @@
Přihlásit se pomocí QR kódu
Naskenovat QR kód
Možnost nahrávat a odesílat hlasové vysílání na časové ose místnosti.
- Povolit hlasové vysílání (v aktivním vývoji)
+ Povolit hlasové vysílání
Domovský server nepodporuje přihlášení pomocí QR kódu.
Přihlášení bylo na druhém zařízení zrušeno.
Tento QR kód je neplatný.
@@ -2946,4 +2946,13 @@
Odkaz
Text
Nastavit odkaz
+ Přístupový token umožňuje plný přístup k účtu. Nikomu ho nesdělujte.
+ Přístupový token
+ Přepnout na odrážky
+ Přepnout na číslovaný seznam
+ V této místnosti nejsou žádné předchozí hlasování
+ Předchozí hlasování
+ V této místnosti nejsou žádné aktivní hlasování
+ Aktivní hlasování
+ Historie hlasování
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-eo/strings.xml b/library/ui-strings/src/main/res/values-eo/strings.xml
index 4521e840a6..e417d183bf 100644
--- a/library/ui-strings/src/main/res/values-eo/strings.xml
+++ b/library/ui-strings/src/main/res/values-eo/strings.xml
@@ -1123,9 +1123,7 @@
Elektu landon
Administri retpoŝtadresojn kaj telefonnumerojn ligitajn al via konto de Matrix
Retpoŝtadresoj kaj telefonnumeroj
- Ĉu montri ĉiujn mesaĝojn de %s\?
-\n
-\nSciu ke tiu ĉi ago reekigos la aplikaĵon, kaj tio povas daŭri iom da tempo.
+ Ĉu montri ĉiujn mesaĝojn de %s\?
Via pasvorto ĝisdatiĝis
La pasvorto ne validas
Malsukcesis ĝisdatigi pasvorton
@@ -2201,4 +2199,5 @@
Sonorante…
Aroj
- Iom uzantoj reatentita
+ \@room
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values-hu/strings.xml b/library/ui-strings/src/main/res/values-hu/strings.xml
index 1be136bb39..9fdad2dbf0 100644
--- a/library/ui-strings/src/main/res/values-hu/strings.xml
+++ b/library/ui-strings/src/main/res/values-hu/strings.xml
@@ -2890,4 +2890,13 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze
Hivatkozás
Szöveg
Hivatkozás beállítása
+ A hozzáférési kulcs teljes elérést biztosít a fiókhoz. Soha ne ossza meg mással.
+ Elérési kulcs
+ Lista ki-,bekapcsolása
+ Számozott lista ki-,bekapcsolása
+ Nincsenek régi szavazások ebben a szobában
+ Régi szavazások
+ Nincsenek aktív szavazások ebben a szobában
+ Aktív szavazások
+ Szavazás alakulása
\ No newline at end of file
diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index 658b08e36d..703cf011f1 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -794,7 +794,7 @@
Shows all threads you’ve participated in
Keep discussions organized with threads
Threads help keep your conversations on-topic and easy to track.
- You\'re homeserver does not support listing threads yet.
+ Your homeserver does not support listing threads yet.
Tip: Long tap a message and use “%s”.
From a Thread
@@ -2298,6 +2298,7 @@
Verification Conclusion
Shared their location
Shared their live location
+ Started a voice broadcast
Waiting…
%s canceled
@@ -3124,6 +3125,7 @@
You don’t have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.
Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.
You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.
+ Unable to play this voice broadcast.
%1$s left
Stop live broadcasting?
@@ -3197,10 +3199,22 @@
Closed poll
Results are only revealed when you end the poll
Ended the poll.
+ Due to decryption errors, some votes may not be counted
Active polls
There are no active polls in this room
+
+ - "There are no active polls for the past day.\nLoad more polls to view polls for previous days."
+ - "There are no active polls for the past %1$d days.\nLoad more polls to view polls for previous days."
+
Past polls
There are no past polls in this room
+
+ - "There are no past polls for the past day.\nLoad more polls to view polls for previous days."
+ - "There are no past polls for the past %1$d days.\nLoad more polls to view polls for previous days."
+
+ Displaying polls
+ Load more polls
+ Error fetching polls.
Share location
diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index ef6cb8cddb..9c9d2dd0dc 100644
--- a/matrix-sdk-android/build.gradle
+++ b/matrix-sdk-android/build.gradle
@@ -81,7 +81,7 @@ android {
buildTypes {
debug {
if (project.hasProperty("coverage")) {
- testCoverageEnabled = coverage.enableTestCoverage
+ testCoverageEnabled = coverage == "true"
}
// Set to true to log privacy or sensible data, such as token
buildConfigField "boolean", "LOG_PRIVATE_DATA", project.property("vector.debugPrivateData")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollResponseAggregatedSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollResponseAggregatedSummary.kt
index b16852e47d..e8b4ef6ed6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollResponseAggregatedSummary.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollResponseAggregatedSummary.kt
@@ -23,5 +23,7 @@ data class PollResponseAggregatedSummary(
val nbOptions: Int = 0,
// The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk)
val sourceEvents: List,
- val localEchos: List
+ val localEchos: List,
+ // list of related event ids which are encrypted due to decryption failure
+ val encryptedRelatedEventIds: List,
)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
index 6320ea964d..3aa480094c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
@@ -148,8 +148,8 @@ fun TimelineEvent.getLastMessageContent(): MessageContent? {
// Polls/Beacon are not message contents like others as there is no msgtype subtype to discriminate moshi parsing
// so toModel won't parse them correctly
// It's discriminated on event type instead. Maybe it shouldn't be MessageContent at all to avoid confusion?
- in EventType.POLL_START.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel()
- in EventType.POLL_END.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel()
+ in EventType.POLL_START.values -> (getLastPollEditNewContent() ?: root.getClearContent()).toModel()
+ in EventType.POLL_END.values -> (getLastPollEditNewContent() ?: root.getClearContent()).toModel()
in EventType.STATE_ROOM_BEACON_INFO.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel()
in EventType.BEACON_LOCATION_DATA.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel()
else -> (getLastEditNewContent() ?: root.getClearContent()).toModel()
@@ -160,6 +160,10 @@ fun TimelineEvent.getLastEditNewContent(): Content? {
return annotations?.editSummary?.latestEdit?.getClearContent()?.toModel()?.newContent
}
+private fun TimelineEvent.getLastPollEditNewContent(): Content? {
+ return annotations?.editSummary?.latestEdit?.getClearContent()?.toModel()?.newContent
+}
+
/**
* Returns true if it's a reply.
*/
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
index c9eabeab48..03672ae81c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
@@ -59,7 +59,7 @@ internal class EventDecryptor @Inject constructor(
private val sendToDeviceTask: SendToDeviceTask,
private val deviceListManager: DeviceListManager,
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
- private val cryptoStore: IMXCryptoStore
+ private val cryptoStore: IMXCryptoStore,
) {
/**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt
index d1ca4f48a6..a3f38cf2c6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt
@@ -17,11 +17,16 @@
package org.matrix.android.sdk.internal.database
import com.zhuinden.monarchy.Monarchy
+import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.RealmResults
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
+import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventInsertEntity
@@ -34,7 +39,7 @@ import javax.inject.Inject
internal class EventInsertLiveObserver @Inject constructor(
@SessionDatabase realmConfiguration: RealmConfiguration,
- private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>
+ private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>,
) :
RealmLiveEntityObserver(realmConfiguration) {
@@ -50,48 +55,90 @@ internal class EventInsertLiveObserver @Inject constructor(
if (!results.isLoaded || results.isEmpty()) {
return@withLock
}
- val idsToDeleteAfterProcess = ArrayList()
- val filteredEvents = ArrayList(results.size)
+ val eventsToProcess = ArrayList(results.size)
+ val eventsToIgnore = ArrayList(results.size)
+
Timber.v("EventInsertEntity updated with ${results.size} results in db")
results.forEach {
- if (shouldProcess(it)) {
- // don't use copy from realm over there
- val copiedEvent = EventInsertEntity(
- eventId = it.eventId,
- eventType = it.eventType
- ).apply {
- insertType = it.insertType
- }
- filteredEvents.add(copiedEvent)
+ // don't use copy from realm over there
+ val copiedEvent = EventInsertEntity(
+ eventId = it.eventId,
+ eventType = it.eventType
+ ).apply {
+ insertType = it.insertType
+ }
+
+ if (shouldProcess(it)) {
+ eventsToProcess.add(copiedEvent)
+ } else {
+ eventsToIgnore.add(copiedEvent)
}
- idsToDeleteAfterProcess.add(it.eventId)
}
+
awaitTransaction(realmConfiguration) { realm ->
- Timber.v("##Transaction: There are ${filteredEvents.size} events to process ")
- filteredEvents.forEach { eventInsert ->
+ Timber.v("##Transaction: There are ${eventsToProcess.size} events to process")
+
+ val idsToDeleteAfterProcess = ArrayList()
+ val idsOfEncryptedEvents = ArrayList()
+ val getAndTriageEvent: (EventInsertEntity) -> Event? = { eventInsert ->
val eventId = eventInsert.eventId
- val event = EventEntity.where(realm, eventId).findFirst()
- if (event == null) {
- Timber.v("Event $eventId not found")
+ val event = getEvent(realm, eventId)
+ if (event?.getClearType() == EventType.ENCRYPTED) {
+ idsOfEncryptedEvents.add(eventId)
+ } else {
+ idsToDeleteAfterProcess.add(eventId)
+ }
+ event
+ }
+
+ eventsToProcess.forEach { eventInsert ->
+ val eventId = eventInsert.eventId
+ val event = getAndTriageEvent(eventInsert)
+
+ if (event != null && canProcessEvent(event)) {
+ processors.filter {
+ it.shouldProcess(eventId, event.getClearType(), eventInsert.insertType)
+ }.forEach {
+ it.process(realm, event)
+ }
+ } else {
+ Timber.v("Cannot process event with id $eventId")
return@forEach
}
- val domainEvent = event.asDomain()
- processors.filter {
- it.shouldProcess(eventId, domainEvent.getClearType(), eventInsert.insertType)
- }.forEach {
- it.process(realm, domainEvent)
- }
}
+
+ eventsToIgnore.forEach { getAndTriageEvent(it) }
+
realm.where(EventInsertEntity::class.java)
.`in`(EventInsertEntityFields.EVENT_ID, idsToDeleteAfterProcess.toTypedArray())
.findAll()
.deleteAllFromRealm()
+
+ // make the encrypted events not processable: they will be processed again after decryption
+ realm.where(EventInsertEntity::class.java)
+ .`in`(EventInsertEntityFields.EVENT_ID, idsOfEncryptedEvents.toTypedArray())
+ .findAll()
+ .forEach { it.canBeProcessed = false }
}
processors.forEach { it.onPostProcess() }
}
}
}
+ private fun getEvent(realm: Realm, eventId: String): Event? {
+ val event = EventEntity.where(realm, eventId).findFirst()
+ if (event == null) {
+ Timber.v("Event $eventId not found")
+ }
+ return event?.asDomain()
+ }
+
+ private fun canProcessEvent(event: Event): Boolean {
+ // event should be either not encrypted or if encrypted it should contain relatesTo content
+ return event.getClearType() != EventType.ENCRYPTED ||
+ event.content.toModel()?.relatesTo != null
+ }
+
private fun shouldProcess(eventInsertEntity: EventInsertEntity): Boolean {
return processors.any {
it.shouldProcess(eventInsertEntity.eventId, eventInsertEntity.eventType, eventInsertEntity.insertType)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PollResponseAggregatedSummaryEntityMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PollResponseAggregatedSummaryEntityMapper.kt
index 00998af9bb..808a49b958 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PollResponseAggregatedSummaryEntityMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PollResponseAggregatedSummaryEntityMapper.kt
@@ -30,7 +30,8 @@ internal object PollResponseAggregatedSummaryEntityMapper {
closedTime = entity.closedTime,
localEchos = entity.sourceLocalEchoEvents.toList(),
sourceEvents = entity.sourceEvents.toList(),
- nbOptions = entity.nbOptions
+ nbOptions = entity.nbOptions,
+ encryptedRelatedEventIds = entity.encryptedRelatedEventIds.toList(),
)
}
@@ -40,7 +41,8 @@ internal object PollResponseAggregatedSummaryEntityMapper {
nbOptions = model.nbOptions,
closedTime = model.closedTime,
sourceEvents = RealmList().apply { addAll(model.sourceEvents) },
- sourceLocalEchoEvents = RealmList().apply { addAll(model.localEchos) }
+ sourceLocalEchoEvents = RealmList().apply { addAll(model.localEchos) },
+ encryptedRelatedEventIds = RealmList().apply { addAll(model.encryptedRelatedEventIds) },
)
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo048.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo048.kt
index ae847ced94..4299054c56 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo048.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo048.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ * Copyright (c) 2023 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,15 +17,16 @@
package org.matrix.android.sdk.internal.database.migration
import io.realm.DynamicRealm
-import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
-import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities
+import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntityFields
import org.matrix.android.sdk.internal.util.database.RealmMigrator
+/**
+ * Adding a new field in poll summary to keep track of non decrypted related events.
+ */
internal class MigrateSessionTo048(realm: DynamicRealm) : RealmMigrator(realm, 48) {
override fun doMigrate(realm: DynamicRealm) {
- realm.schema.get("HomeServerCapabilitiesEntity")
- ?.addField(HomeServerCapabilitiesEntityFields.EXTERNAL_ACCOUNT_MANAGEMENT_URL, String::class.java)
- ?.forceRefreshOfHomeServerCapabilities()
+ realm.schema.get("PollResponseAggregatedSummaryEntity")
+ ?.addRealmListField(PollResponseAggregatedSummaryEntityFields.ENCRYPTED_RELATED_EVENT_IDS.`$`, String::class.java)
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventInsertEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventInsertEntity.kt
index eff332dc3a..054094c398 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventInsertEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventInsertEntity.kt
@@ -27,7 +27,7 @@ internal open class EventInsertEntity(
var eventType: String = "",
/**
* This flag will be used to filter EventInsertEntity in EventInsertLiveObserver.
- * Currently it's set to false when the event content is encrypted.
+ * Currently it's set to false after an event with encrypted content has been processed.
*/
var canBeProcessed: Boolean = true
) : RealmObject() {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollResponseAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollResponseAggregatedSummaryEntity.kt
index d759bd3cd9..906e329f6f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollResponseAggregatedSummaryEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollResponseAggregatedSummaryEntity.kt
@@ -33,7 +33,9 @@ internal open class PollResponseAggregatedSummaryEntity(
// The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk)
var sourceEvents: RealmList = RealmList(),
- var sourceLocalEchoEvents: RealmList = RealmList()
+ var sourceLocalEchoEvents: RealmList = RealmList(),
+ // list of related event ids which are encrypted due to decryption failure
+ var encryptedRelatedEventIds: RealmList = RealmList(),
) : RealmObject() {
companion object
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
index 0d998e8fe1..93fe1bd1d2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
@@ -72,7 +72,7 @@ import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntit
SpaceParentSummaryEntity::class,
UserPresenceEntity::class,
ThreadSummaryEntity::class,
- ThreadListPageEntity::class
+ ThreadListPageEntity::class,
]
)
internal class SessionRealmModule
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt
index 0f1c226044..4805c36f8c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt
@@ -20,7 +20,6 @@ import io.realm.Realm
import io.realm.RealmList
import io.realm.RealmQuery
import io.realm.kotlin.where
-import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventEntityFields
import org.matrix.android.sdk.internal.database.model.EventInsertEntity
@@ -32,10 +31,9 @@ internal fun EventEntity.copyToRealmOrIgnore(realm: Realm, insertType: EventInse
.equalTo(EventEntityFields.ROOM_ID, roomId)
.findFirst()
return if (eventEntity == null) {
- val canBeProcessed = type != EventType.ENCRYPTED || decryptionResultJson != null
- val insertEntity = EventInsertEntity(eventId = eventId, eventType = type, canBeProcessed = canBeProcessed).apply {
- this.insertType = insertType
- }
+ val insertEntity = EventInsertEntity(eventId = eventId, eventType = type, canBeProcessed = true)
+ insertEntity.insertType = insertType
+
realm.insert(insertEntity)
// copy this event entity and return it
realm.copyToRealm(this)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt
index 41d0c3f6ab..5a66e7e62d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt
@@ -16,13 +16,16 @@
package org.matrix.android.sdk.internal.session.room
+import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.LocalEcho
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.getRelationContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.events.model.toValidDecryptedEvent
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
+import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import timber.log.Timber
import javax.inject.Inject
@@ -101,7 +104,7 @@ internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCrypto
if (originalDecrypted.type != replaceDecrypted.type) {
return EditValidity.Invalid("replacement and original events must have the same type")
}
- if (replaceDecrypted.clearContent.toModel()?.newContent == null) {
+ if (!hasNewContent(replaceDecrypted.type, replaceDecrypted.clearContent)) {
return EditValidity.Invalid("replacement event must have an m.new_content property")
}
} else {
@@ -116,11 +119,18 @@ internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCrypto
if (originalEvent.type != replaceEvent.type) {
return EditValidity.Invalid("replacement and original events must have the same type")
}
- if (replaceEvent.content.toModel()?.newContent == null) {
+ if (!hasNewContent(replaceEvent.type, replaceEvent.content)) {
return EditValidity.Invalid("replacement event must have an m.new_content property")
}
}
return EditValidity.Valid
}
+
+ private fun hasNewContent(eventType: String?, content: Content?): Boolean {
+ return when (eventType) {
+ in EventType.POLL_START.values -> content.toModel()?.newContent != null
+ else -> content.toModel()?.newContent != null
+ }
+ }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
index be73309837..edc10bd187 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
@@ -61,6 +61,7 @@ import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.LiveLocationAggregationProcessor
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollAggregationProcessor
+import org.matrix.android.sdk.internal.session.room.aggregation.utd.EncryptedReferenceAggregationProcessor
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber
@@ -73,6 +74,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
private val sessionManager: SessionManager,
private val liveLocationAggregationProcessor: LiveLocationAggregationProcessor,
private val pollAggregationProcessor: PollAggregationProcessor,
+ private val encryptedReferenceAggregationProcessor: EncryptedReferenceAggregationProcessor,
private val editValidator: EventEditValidator,
private val clock: Clock,
) : EventInsertLiveProcessor {
@@ -140,6 +142,16 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}")
handleReaction(realm, event, roomId, isLocalEcho)
}
+ EventType.ENCRYPTED -> {
+ val encryptedEventContent = event.content.toModel()
+ processEncryptedContent(
+ encryptedEventContent = encryptedEventContent,
+ realm = realm,
+ event = event,
+ roomId = roomId,
+ isLocalEcho = isLocalEcho,
+ )
+ }
EventType.MESSAGE -> {
if (event.unsignedData?.relations?.annotations != null) {
Timber.v("###REACTION Aggregation in room $roomId for event ${event.eventId}")
@@ -170,32 +182,6 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
}
}
}
- // As for now Live event processors are not receiving UTD events.
- // They will get an update if the event is decrypted later
- EventType.ENCRYPTED -> {
- // Relation type is in clear, it might be possible to do some things?
- // Notice that if the event is decrypted later, process be called again
- val encryptedEventContent = event.content.toModel()
- when (encryptedEventContent?.relatesTo?.type) {
- RelationType.REPLACE -> {
- Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
- // A replace!
- handleReplace(realm, event, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
- }
- RelationType.RESPONSE -> {
- // can we / should we do we something for UTD response??
- Timber.w("## UTD response in room $roomId related to ${encryptedEventContent.relatesTo.eventId}")
- }
- RelationType.REFERENCE -> {
- // can we / should we do we something for UTD reference??
- Timber.w("## UTD reference in room $roomId related to ${encryptedEventContent.relatesTo.eventId}")
- }
- RelationType.ANNOTATION -> {
- // can we / should we do we something for UTD annotation??
- Timber.w("## UTD annotation in room $roomId related to ${encryptedEventContent.relatesTo.eventId}")
- }
- }
- }
EventType.REDACTION -> {
val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() }
?: return
@@ -250,6 +236,36 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
}
}
+ private fun processEncryptedContent(
+ encryptedEventContent: EncryptedEventContent?,
+ realm: Realm,
+ event: Event,
+ roomId: String,
+ isLocalEcho: Boolean,
+ ) {
+ when (encryptedEventContent?.relatesTo?.type) {
+ RelationType.REPLACE -> {
+ Timber.w("## UTD replace in room $roomId for event ${event.eventId}")
+ }
+ RelationType.RESPONSE -> {
+ Timber.w("## UTD response in room $roomId related to ${encryptedEventContent.relatesTo.eventId}")
+ }
+ RelationType.REFERENCE -> {
+ Timber.w("## UTD reference in room $roomId related to ${encryptedEventContent.relatesTo.eventId}")
+ encryptedReferenceAggregationProcessor.handle(
+ realm = realm,
+ event = event,
+ isLocalEcho = isLocalEcho,
+ relatedEventId = encryptedEventContent.relatesTo.eventId,
+ )
+ }
+ RelationType.ANNOTATION -> {
+ Timber.w("## UTD annotation in room $roomId related to ${encryptedEventContent.relatesTo.eventId}")
+ }
+ else -> Unit
+ }
+ }
+
// OPT OUT serer aggregation until API mature enough
private val SHOULD_HANDLE_SERVER_AGREGGATION = false // should be true to work with e2e
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt
index a424becbd6..2ff43d6812 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt
@@ -155,6 +155,8 @@ internal class DefaultPollAggregationProcessor @Inject constructor(
)
aggregatedPollSummaryEntity.aggregatedContent = ContentMapper.map(newSumModel.toContent())
+ event.eventId?.let { removeEncryptedRelatedEventIdIfNeeded(aggregatedPollSummaryEntity, it) }
+
return true
}
@@ -180,6 +182,8 @@ internal class DefaultPollAggregationProcessor @Inject constructor(
aggregatedPollSummaryEntity.sourceEvents.add(event.eventId)
}
+ event.eventId?.let { removeEncryptedRelatedEventIdIfNeeded(aggregatedPollSummaryEntity, it) }
+
if (!isLocalEcho) {
ensurePollIsFullyAggregated(roomId, pollEventId)
}
@@ -226,4 +230,10 @@ internal class DefaultPollAggregationProcessor @Inject constructor(
fetchPollResponseEventsTask.execute(params)
}
}
+
+ private fun removeEncryptedRelatedEventIdIfNeeded(aggregatedPollSummaryEntity: PollResponseAggregatedSummaryEntity, eventId: String) {
+ if (aggregatedPollSummaryEntity.encryptedRelatedEventIds.contains(eventId)) {
+ aggregatedPollSummaryEntity.encryptedRelatedEventIds.remove(eventId)
+ }
+ }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessor.kt
index 848643b435..33a69b720a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessor.kt
@@ -21,7 +21,7 @@ import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
-interface PollAggregationProcessor {
+internal interface PollAggregationProcessor {
/**
* Poll start events don't need to be processed by the aggregator.
* This function will only handle if the poll is edited and will update the poll summary entity.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessor.kt
new file mode 100644
index 0000000000..43631fcc3e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessor.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.room.aggregation.utd
+
+import io.realm.Realm
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntity
+import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntityFields
+import javax.inject.Inject
+
+internal class EncryptedReferenceAggregationProcessor @Inject constructor() {
+
+ fun handle(
+ realm: Realm,
+ event: Event,
+ isLocalEcho: Boolean,
+ relatedEventId: String?
+ ): Boolean {
+ return if (isLocalEcho || relatedEventId.isNullOrEmpty()) {
+ false
+ } else {
+ handlePollReference(realm = realm, event = event, relatedEventId = relatedEventId)
+ true
+ }
+ }
+
+ private fun handlePollReference(
+ realm: Realm,
+ event: Event,
+ relatedEventId: String
+ ) {
+ event.eventId?.let { eventId ->
+ val existingRelatedPoll = getPollSummaryWithEventId(realm, relatedEventId)
+ if (eventId !in existingRelatedPoll?.encryptedRelatedEventIds.orEmpty()) {
+ existingRelatedPoll?.encryptedRelatedEventIds?.add(eventId)
+ }
+ }
+ }
+
+ private fun getPollSummaryWithEventId(realm: Realm, eventId: String): PollResponseAggregatedSummaryEntity? {
+ return realm.where(PollResponseAggregatedSummaryEntity::class.java)
+ .containsValue(PollResponseAggregatedSummaryEntityFields.SOURCE_EVENTS.`$`, eventId)
+ .findFirst()
+ }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessorTest.kt
new file mode 100644
index 0000000000..ff803c4f1a
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessorTest.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.room
+
+import io.mockk.every
+import io.mockk.mockk
+import org.junit.Test
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.RelationType
+import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
+import org.matrix.android.sdk.api.session.events.model.toContent
+import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
+import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
+import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntityFields
+import org.matrix.android.sdk.test.fakes.FakeClock
+import org.matrix.android.sdk.test.fakes.FakeRealm
+import org.matrix.android.sdk.test.fakes.FakeStateEventDataSource
+import org.matrix.android.sdk.test.fakes.givenEqualTo
+import org.matrix.android.sdk.test.fakes.givenFindFirst
+import org.matrix.android.sdk.test.fakes.internal.FakeEventEditValidator
+import org.matrix.android.sdk.test.fakes.internal.FakeLiveLocationAggregationProcessor
+import org.matrix.android.sdk.test.fakes.internal.FakePollAggregationProcessor
+import org.matrix.android.sdk.test.fakes.internal.FakeSessionManager
+import org.matrix.android.sdk.test.fakes.internal.session.room.aggregation.utd.FakeEncryptedReferenceAggregationProcessor
+
+private const val A_ROOM_ID = "room-id"
+private const val AN_EVENT_ID = "event-id"
+
+internal class EventRelationsAggregationProcessorTest {
+
+ private val fakeStateEventDataSource = FakeStateEventDataSource()
+ private val fakeSessionManager = FakeSessionManager()
+ private val fakeLiveLocationAggregationProcessor = FakeLiveLocationAggregationProcessor()
+ private val fakePollAggregationProcessor = FakePollAggregationProcessor()
+ private val fakeEncryptedReferenceAggregationProcessor = FakeEncryptedReferenceAggregationProcessor()
+ private val fakeEventEditValidator = FakeEventEditValidator()
+ private val fakeClock = FakeClock()
+ private val fakeRealm = FakeRealm()
+
+ private val encryptedEventRelationsAggregationProcessor = EventRelationsAggregationProcessor(
+ userId = "userId",
+ stateEventDataSource = fakeStateEventDataSource.instance,
+ sessionId = "sessionId",
+ sessionManager = fakeSessionManager.instance,
+ liveLocationAggregationProcessor = fakeLiveLocationAggregationProcessor.instance,
+ pollAggregationProcessor = fakePollAggregationProcessor.instance,
+ encryptedReferenceAggregationProcessor = fakeEncryptedReferenceAggregationProcessor.instance,
+ editValidator = fakeEventEditValidator.instance,
+ clock = fakeClock,
+ )
+
+ @Test
+ fun `given an encrypted reference event when process then reference is processed`() {
+ // Given
+ val anEvent = givenAnEvent(
+ eventId = AN_EVENT_ID,
+ roomId = A_ROOM_ID,
+ eventType = EventType.ENCRYPTED,
+ )
+ val relatedEventId = "related-event-id"
+ val encryptedEventContent = givenEncryptedEventContent(
+ relationType = RelationType.REFERENCE,
+ relatedEventId = relatedEventId,
+ )
+ every { anEvent.content } returns encryptedEventContent.toContent()
+ val resultOfReferenceProcess = false
+ fakeEncryptedReferenceAggregationProcessor.givenHandleReturns(resultOfReferenceProcess)
+ givenEventAnnotationsSummary(roomId = A_ROOM_ID, eventId = AN_EVENT_ID, annotationsSummary = null)
+
+ // When
+ encryptedEventRelationsAggregationProcessor.process(
+ realm = fakeRealm.instance,
+ event = anEvent,
+ )
+
+ // Then
+ fakeEncryptedReferenceAggregationProcessor.verifyHandle(
+ realm = fakeRealm.instance,
+ event = anEvent,
+ isLocalEcho = false,
+ relatedEventId = relatedEventId,
+ )
+ }
+
+ private fun givenAnEvent(
+ eventId: String,
+ roomId: String?,
+ eventType: String,
+ ): Event {
+ return mockk().also {
+ every { it.eventId } returns eventId
+ every { it.roomId } returns roomId
+ every { it.getClearType() } returns eventType
+ }
+ }
+
+ private fun givenEncryptedEventContent(relationType: String, relatedEventId: String): EncryptedEventContent {
+ val relationContent = RelationDefaultContent(
+ eventId = relatedEventId,
+ type = relationType,
+ )
+ return EncryptedEventContent(
+ relatesTo = relationContent,
+ )
+ }
+
+ private fun givenEventAnnotationsSummary(
+ roomId: String,
+ eventId: String,
+ annotationsSummary: EventAnnotationsSummaryEntity?
+ ) {
+ fakeRealm.givenWhere()
+ .givenEqualTo(EventAnnotationsSummaryEntityFields.ROOM_ID, roomId)
+ .givenEqualTo(EventAnnotationsSummaryEntityFields.EVENT_ID, eventId)
+ .givenFindFirst(annotationsSummary)
+ }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt
index 0888d82907..766e51a8e5 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt
@@ -25,6 +25,8 @@ import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeFalse
import org.amshove.kluent.shouldBeTrue
+import org.amshove.kluent.shouldContain
+import org.amshove.kluent.shouldNotContain
import org.junit.Before
import org.junit.Test
import org.matrix.android.sdk.api.session.Session
@@ -105,6 +107,24 @@ class DefaultPollAggregationProcessorTest {
pollAggregationProcessor.handlePollResponseEvent(session, realm.instance, A_POLL_RESPONSE_EVENT).shouldBeTrue()
}
+ @Test
+ fun `given a poll response event with a reference, when processing, then event id is removed from encrypted events list`() {
+ // Given
+ val anotherEventId = "other-event-id"
+ val pollResponseAggregatedSummaryEntity = PollResponseAggregatedSummaryEntity(
+ encryptedRelatedEventIds = RealmList(AN_EVENT_ID, anotherEventId)
+ )
+ every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns pollResponseAggregatedSummaryEntity
+
+ // When
+ val result = pollAggregationProcessor.handlePollResponseEvent(session, realm.instance, A_POLL_RESPONSE_EVENT)
+
+ // Then
+ result.shouldBeTrue()
+ pollResponseAggregatedSummaryEntity.encryptedRelatedEventIds.shouldNotContain(AN_EVENT_ID)
+ pollResponseAggregatedSummaryEntity.encryptedRelatedEventIds.shouldContain(anotherEventId)
+ }
+
@Test
fun `given a poll response event after poll is closed, when processing, then is ignored and returns false`() {
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity().apply {
@@ -132,12 +152,33 @@ class DefaultPollAggregationProcessorTest {
// Given
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity()
every { fakeTaskExecutor.instance.executorScope } returns this
-
- // When
val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, true)
+ // When
+ val result = pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT)
+
// Then
- pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue()
+ result.shouldBeTrue()
+ }
+
+ @Test
+ fun `given a poll end event, when processing, then event id is removed from encrypted events list`() = runTest {
+ // Given
+ val anotherEventId = "other-event-id"
+ val pollResponseAggregatedSummaryEntity = PollResponseAggregatedSummaryEntity(
+ encryptedRelatedEventIds = RealmList(AN_EVENT_ID, anotherEventId)
+ )
+ every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns pollResponseAggregatedSummaryEntity
+ every { fakeTaskExecutor.instance.executorScope } returns this
+ val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, true)
+
+ // When
+ val result = pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT)
+
+ // Then
+ result.shouldBeTrue()
+ pollResponseAggregatedSummaryEntity.encryptedRelatedEventIds.shouldNotContain(AN_EVENT_ID)
+ pollResponseAggregatedSummaryEntity.encryptedRelatedEventIds.shouldContain(anotherEventId)
}
@Test
@@ -145,12 +186,13 @@ class DefaultPollAggregationProcessorTest {
// Given
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity()
every { fakeTaskExecutor.instance.executorScope } returns this
-
- // When
val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, false)
+ // When
+ val result = pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT)
+
// Then
- pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue()
+ result.shouldBeTrue()
}
@Test
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessorTest.kt
new file mode 100644
index 0000000000..2998b9bff0
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessorTest.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.room.aggregation.utd
+
+import io.mockk.every
+import io.mockk.mockk
+import io.realm.RealmList
+import org.amshove.kluent.shouldBeFalse
+import org.amshove.kluent.shouldBeTrue
+import org.amshove.kluent.shouldContain
+import org.junit.Test
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntity
+import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntityFields
+import org.matrix.android.sdk.test.fakes.FakeRealm
+import org.matrix.android.sdk.test.fakes.givenContainsValue
+import org.matrix.android.sdk.test.fakes.givenFindFirst
+
+internal class EncryptedReferenceAggregationProcessorTest {
+
+ private val fakeRealm = FakeRealm()
+
+ private val encryptedReferenceAggregationProcessor = EncryptedReferenceAggregationProcessor()
+
+ @Test
+ fun `given local echo when process then result is false`() {
+ // Given
+ val anEvent = mockk()
+ val isLocalEcho = true
+ val relatedEventId = "event-id"
+
+ // When
+ val result = encryptedReferenceAggregationProcessor.handle(
+ realm = fakeRealm.instance,
+ event = anEvent,
+ isLocalEcho = isLocalEcho,
+ relatedEventId = relatedEventId,
+ )
+
+ // Then
+ result.shouldBeFalse()
+ }
+
+ @Test
+ fun `given invalid event id when process then result is false`() {
+ // Given
+ val anEvent = mockk()
+ val isLocalEcho = false
+
+ // When
+ val result1 = encryptedReferenceAggregationProcessor.handle(
+ realm = fakeRealm.instance,
+ event = anEvent,
+ isLocalEcho = isLocalEcho,
+ relatedEventId = null,
+ )
+ val result2 = encryptedReferenceAggregationProcessor.handle(
+ realm = fakeRealm.instance,
+ event = anEvent,
+ isLocalEcho = isLocalEcho,
+ relatedEventId = "",
+ )
+
+ // Then
+ result1.shouldBeFalse()
+ result2.shouldBeFalse()
+ }
+
+ @Test
+ fun `given related event id of an existing poll when process then result is true and event id is stored in poll summary`() {
+ // Given
+ val anEventId = "event-id"
+ val anEvent = givenAnEvent(anEventId)
+ val isLocalEcho = false
+ val relatedEventId = "related-event-id"
+ val pollResponseAggregatedSummaryEntity = PollResponseAggregatedSummaryEntity(
+ encryptedRelatedEventIds = RealmList(),
+ )
+ fakeRealm.givenWhere()
+ .givenContainsValue(PollResponseAggregatedSummaryEntityFields.SOURCE_EVENTS.`$`, relatedEventId)
+ .givenFindFirst(pollResponseAggregatedSummaryEntity)
+
+ // When
+ val result = encryptedReferenceAggregationProcessor.handle(
+ realm = fakeRealm.instance,
+ event = anEvent,
+ isLocalEcho = isLocalEcho,
+ relatedEventId = relatedEventId,
+ )
+
+ // Then
+ result.shouldBeTrue()
+ pollResponseAggregatedSummaryEntity.encryptedRelatedEventIds.shouldContain(anEventId)
+ }
+
+ @Test
+ fun `given related event id but no existing related poll when process then result is true and event id is not stored`() {
+ // Given
+ val anEventId = "event-id"
+ val anEvent = givenAnEvent(anEventId)
+ val isLocalEcho = false
+ val relatedEventId = "related-event-id"
+ fakeRealm.givenWhere()
+ .givenContainsValue(PollResponseAggregatedSummaryEntityFields.SOURCE_EVENTS.`$`, relatedEventId)
+ .givenFindFirst(null)
+
+ // When
+ val result = encryptedReferenceAggregationProcessor.handle(
+ realm = fakeRealm.instance,
+ event = anEvent,
+ isLocalEcho = isLocalEcho,
+ relatedEventId = relatedEventId,
+ )
+
+ // Then
+ result.shouldBeTrue()
+ }
+
+ private fun givenAnEvent(eventId: String): Event {
+ return mockk().also {
+ every { it.eventId } returns eventId
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt
index ba124a86aa..49d64c1835 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt
@@ -117,6 +117,14 @@ inline fun RealmQuery.givenIn(
return this
}
+inline fun RealmQuery.givenContainsValue(
+ fieldName: String,
+ value: String,
+): RealmQuery {
+ every { containsValue(fieldName, value) } returns this
+ return this
+}
+
/**
* Should be called on a mocked RealmObject and not on a real RealmObject so that the underlying final method is mocked.
*/
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakeEventEditValidator.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakeEventEditValidator.kt
new file mode 100644
index 0000000000..2fa36cf60d
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakeEventEditValidator.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2023 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.test.fakes.internal
+
+import io.mockk.mockk
+import org.matrix.android.sdk.internal.session.room.EventEditValidator
+
+internal class FakeEventEditValidator {
+
+ val instance: EventEditValidator = mockk()
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakeLiveLocationAggregationProcessor.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakeLiveLocationAggregationProcessor.kt
new file mode 100644
index 0000000000..6385110963
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakeLiveLocationAggregationProcessor.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2023 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.test.fakes.internal
+
+import io.mockk.mockk
+import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.LiveLocationAggregationProcessor
+
+internal class FakeLiveLocationAggregationProcessor {
+
+ val instance: LiveLocationAggregationProcessor = mockk()
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakePollAggregationProcessor.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakePollAggregationProcessor.kt
new file mode 100644
index 0000000000..5187c785ca
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakePollAggregationProcessor.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2023 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.test.fakes.internal
+
+import io.mockk.mockk
+import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollAggregationProcessor
+
+internal class FakePollAggregationProcessor {
+
+ val instance: PollAggregationProcessor = mockk()
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/aggregation/utd/FakeEncryptedReferenceAggregationProcessor.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/aggregation/utd/FakeEncryptedReferenceAggregationProcessor.kt
new file mode 100644
index 0000000000..7661095fe3
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/aggregation/utd/FakeEncryptedReferenceAggregationProcessor.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.test.fakes.internal.session.room.aggregation.utd
+
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import io.realm.Realm
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.internal.session.room.aggregation.utd.EncryptedReferenceAggregationProcessor
+
+internal class FakeEncryptedReferenceAggregationProcessor {
+
+ val instance: EncryptedReferenceAggregationProcessor = mockk()
+
+ fun givenHandleReturns(result: Boolean) {
+ every { instance.handle(any(), any(), any(), any()) } returns result
+ }
+
+ fun verifyHandle(
+ realm: Realm,
+ event: Event,
+ isLocalEcho: Boolean,
+ relatedEventId: String?,
+ ) {
+ verify { instance.handle(realm, event, isLocalEcho, relatedEventId) }
+ }
+}
diff --git a/vector-app/build.gradle b/vector-app/build.gradle
index 11119a75cc..824f651b4d 100644
--- a/vector-app/build.gradle
+++ b/vector-app/build.gradle
@@ -232,7 +232,7 @@ android {
resValue "color", "launcher_background", "#0DBD8B"
if (project.hasProperty("coverage")) {
- testCoverageEnabled = coverage.enableTestCoverage
+ testCoverageEnabled = coverage == "true"
}
}
@@ -403,8 +403,8 @@ dependencies {
androidTestImplementation libs.mockk.mockkAndroid
androidTestUtil libs.androidx.orchestrator
androidTestImplementation libs.androidx.fragmentTesting
- androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.22"
- debugImplementation libs.androidx.fragmentTesting
+ androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.8.0"
+ debugImplementation libs.androidx.fragmentTestingManifest
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
}
diff --git a/vector/build.gradle b/vector/build.gradle
index 2224634194..efea312bed 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -69,7 +69,7 @@ android {
buildTypes {
debug {
if (project.hasProperty("coverage")) {
- testCoverageEnabled = coverage.enableTestCoverage
+ testCoverageEnabled = coverage == "true"
}
}
}
@@ -330,6 +330,7 @@ dependencies {
}
androidTestImplementation libs.mockk.mockkAndroid
androidTestUtil libs.androidx.orchestrator
- debugImplementation libs.androidx.fragmentTesting
- androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.22"
+ debugImplementation libs.androidx.fragmentTestingManifest
+ androidTestImplementation libs.androidx.fragmentTesting
+ androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.8.0"
}
diff --git a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt
index 380c80775b..c1e201cfc4 100644
--- a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt
+++ b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt
@@ -20,6 +20,7 @@ import android.content.ActivityNotFoundException
import im.vector.app.R
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.call.dialpad.DialPadLookup
+import im.vector.app.features.roomprofile.polls.RoomPollsLoadingError
import im.vector.app.features.voice.VoiceFailure
import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure
import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure.RecordingError
@@ -138,6 +139,7 @@ class DefaultErrorFormatter @Inject constructor(
stringProvider.getString(R.string.login_signin_matrix_id_error_invalid_matrix_id)
is VoiceFailure -> voiceMessageError(throwable)
is VoiceBroadcastFailure -> voiceBroadcastMessageError(throwable)
+ is RoomPollsLoadingError -> stringProvider.getString(R.string.room_polls_loading_error)
is ActivityNotFoundException ->
stringProvider.getString(R.string.error_no_external_application_found)
else -> throwable.localizedMessage
@@ -157,6 +159,8 @@ class DefaultErrorFormatter @Inject constructor(
RecordingError.BlockedBySomeoneElse -> stringProvider.getString(R.string.error_voice_broadcast_blocked_by_someone_else_message)
RecordingError.NoPermission -> stringProvider.getString(R.string.error_voice_broadcast_permission_denied_message)
RecordingError.UserAlreadyBroadcasting -> stringProvider.getString(R.string.error_voice_broadcast_already_in_progress_message)
+ is VoiceBroadcastFailure.ListeningError.UnableToPlay,
+ is VoiceBroadcastFailure.ListeningError.DownloadError -> stringProvider.getString(R.string.error_voice_broadcast_unable_to_play)
}
}
diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt
index 38b72f2022..3b9de57be8 100644
--- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt
+++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt
@@ -17,6 +17,7 @@ package im.vector.app.features.crypto.verification
import android.app.Activity
import android.app.Dialog
+import android.content.DialogInterface
import android.os.Bundle
import android.os.Parcelable
import android.view.KeyEvent
@@ -84,10 +85,6 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment {
voiceMessageViews.renderIdle()
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
index 646cfa50d2..d442c1f1ba 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
@@ -216,8 +216,8 @@ class MessageActionsViewModel @AssistedInject constructor(
noticeEventFormatter.format(timelineEvent, room?.roomSummary()?.isDirect.orFalse())
}
in EventType.POLL_START.values -> {
- timelineEvent.root.getClearContent().toModel(catchError = true)
- ?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: ""
+ (timelineEvent.getVectorLastMessageContent() as? MessagePollContent)?.getBestPollCreationInfo()?.question?.getBestQuestion()
+ ?: stringProvider.getString(R.string.message_reply_to_poll_preview)
}
else -> null
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt
index 13f63e86c4..7abc51fa51 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt
@@ -83,9 +83,14 @@ class PollItemViewStateFactory @Inject constructor(
totalVotes: Int,
winnerVoteCount: Int?,
): PollViewState {
+ val totalVotesText = if (pollResponseSummary?.hasEncryptedRelatedEvents.orFalse()) {
+ stringProvider.getString(R.string.unable_to_decrypt_some_events_in_poll)
+ } else {
+ stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, totalVotes, totalVotes)
+ }
return PollViewState(
question = question,
- votesStatus = stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, totalVotes, totalVotes),
+ votesStatus = totalVotesText,
canVote = false,
optionViewStates = pollCreationInfo?.answers?.map { answer ->
val voteSummary = pollResponseSummary?.getVoteSummaryOfAnOption(answer.id ?: "")
@@ -126,9 +131,14 @@ class PollItemViewStateFactory @Inject constructor(
pollResponseSummary: PollResponseData?,
totalVotes: Int
): PollViewState {
+ val totalVotesText = if (pollResponseSummary?.hasEncryptedRelatedEvents.orFalse()) {
+ stringProvider.getString(R.string.unable_to_decrypt_some_events_in_poll)
+ } else {
+ stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, totalVotes, totalVotes)
+ }
return PollViewState(
question = question,
- votesStatus = stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, totalVotes, totalVotes),
+ votesStatus = totalVotesText,
canVote = true,
optionViewStates = pollCreationInfo?.answers?.map { answer ->
val isMyVote = pollResponseSummary?.myVote == answer.id
@@ -144,7 +154,11 @@ class PollItemViewStateFactory @Inject constructor(
)
}
- private fun createReadyPollViewState(question: String, pollCreationInfo: PollCreationInfo?, totalVotes: Int): PollViewState {
+ private fun createReadyPollViewState(
+ question: String,
+ pollCreationInfo: PollCreationInfo?,
+ totalVotes: Int
+ ): PollViewState {
val totalVotesText = if (totalVotes == 0) {
stringProvider.getString(R.string.poll_no_votes_cast)
} else {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt
index cc3a015120..3439fb1f57 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt
@@ -15,9 +15,9 @@
*/
package im.vector.app.features.home.room.detail.timeline.factory
+import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.DrawableProvider
-import im.vector.app.features.displayname.getBestName
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
import im.vector.app.features.home.room.detail.timeline.helper.VoiceBroadcastEventsGroup
@@ -36,7 +36,6 @@ import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.getRoom
-import org.matrix.android.sdk.api.session.getUserOrDefault
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
@@ -45,6 +44,7 @@ class VoiceBroadcastItemFactory @Inject constructor(
private val avatarSizeProvider: AvatarSizeProvider,
private val colorProvider: ColorProvider,
private val drawableProvider: DrawableProvider,
+ private val errorFormatter: ErrorFormatter,
private val voiceBroadcastRecorder: VoiceBroadcastRecorder?,
private val voiceBroadcastPlayer: VoiceBroadcastPlayer,
private val playbackTracker: AudioMessagePlaybackTracker,
@@ -75,13 +75,14 @@ class VoiceBroadcastItemFactory @Inject constructor(
voiceBroadcast = voiceBroadcast,
voiceBroadcastState = voiceBroadcastContent.voiceBroadcastState,
duration = voiceBroadcastEventsGroup.getDuration(),
- recorderName = params.event.root.stateKey?.let { session.getUserOrDefault(it) }?.toMatrixItem()?.getBestName().orEmpty(),
+ recorderName = params.event.senderInfo.disambiguatedDisplayName,
recorder = voiceBroadcastRecorder,
player = voiceBroadcastPlayer,
playbackTracker = playbackTracker,
roomItem = session.getRoom(params.event.roomId)?.roomSummary()?.toMatrixItem(),
colorProvider = colorProvider,
drawableProvider = drawableProvider,
+ errorFormatter = errorFormatter,
)
return if (isRecording) {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
index 5fa9576dd4..eaa0bbb51a 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
@@ -27,6 +27,7 @@ import im.vector.app.core.resources.StringProvider
import im.vector.app.features.html.EventHtmlRenderer
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
import im.vector.app.features.voicebroadcast.isLive
+import im.vector.app.features.voicebroadcast.isVoiceBroadcast
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
import me.gujun.android.span.image
import me.gujun.android.span.span
@@ -39,6 +40,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType
+import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getTextDisplayableContent
@@ -86,10 +88,16 @@ class DisplayableEventFormatter @Inject constructor(
simpleFormat(senderName, stringProvider.getString(R.string.sent_an_image), appendAuthor)
}
MessageType.MSGTYPE_AUDIO -> {
- if ((messageContent as? MessageAudioContent)?.voiceMessageIndicator != null) {
- simpleFormat(senderName, stringProvider.getString(R.string.sent_a_voice_message), appendAuthor)
- } else {
- simpleFormat(senderName, stringProvider.getString(R.string.sent_an_audio_file), appendAuthor)
+ when {
+ (messageContent as? MessageAudioContent)?.voiceMessageIndicator == null -> {
+ simpleFormat(senderName, stringProvider.getString(R.string.sent_an_audio_file), appendAuthor)
+ }
+ timelineEvent.root.asMessageAudioEvent().isVoiceBroadcast() -> {
+ simpleFormat(senderName, stringProvider.getString(R.string.started_a_voice_broadcast), appendAuthor)
+ }
+ else -> {
+ simpleFormat(senderName, stringProvider.getString(R.string.sent_a_voice_message), appendAuthor)
+ }
}
}
MessageType.MSGTYPE_VIDEO -> {
@@ -130,7 +138,7 @@ class DisplayableEventFormatter @Inject constructor(
span { }
}
in EventType.POLL_START.values -> {
- timelineEvent.root.getClearContent().toModel(catchError = true)?.getBestPollCreationInfo()?.question?.getBestQuestion()
+ (timelineEvent.getVectorLastMessageContent() as? MessagePollContent)?.getBestPollCreationInfo()?.question?.getBestQuestion()
?: stringProvider.getString(R.string.sent_a_poll)
}
in EventType.POLL_RESPONSE.values -> {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt
index c34cbbc74a..c598a99af7 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt
@@ -50,8 +50,11 @@ class AudioMessagePlaybackTracker @Inject constructor() {
listeners.remove(id)
}
- fun pauseAllPlaybacks() {
- listeners.keys.forEach(::pausePlayback)
+ fun unregisterListeners() {
+ listeners.forEach {
+ it.value.onUpdate(Listener.State.Idle)
+ }
+ listeners.clear()
}
/**
@@ -84,6 +87,10 @@ class AudioMessagePlaybackTracker @Inject constructor() {
}
}
+ fun pauseAllPlaybacks() {
+ listeners.keys.forEach(::pausePlayback)
+ }
+
fun pausePlayback(id: String) {
val state = getPlaybackState(id)
if (state is Listener.State.Playing) {
@@ -94,7 +101,14 @@ class AudioMessagePlaybackTracker @Inject constructor() {
}
fun stopPlayback(id: String) {
- setState(id, Listener.State.Idle)
+ val state = getPlaybackState(id)
+ if (state !is Listener.State.Error) {
+ setState(id, Listener.State.Idle)
+ }
+ }
+
+ fun onError(id: String, error: Throwable) {
+ setState(id, Listener.State.Error(error))
}
fun updatePlayingAtPlaybackTime(id: String, time: Int, percentage: Float) {
@@ -116,6 +130,7 @@ class AudioMessagePlaybackTracker @Inject constructor() {
is Listener.State.Playing -> state.playbackTime
is Listener.State.Paused -> state.playbackTime
is Listener.State.Recording,
+ is Listener.State.Error,
Listener.State.Idle,
null -> null
}
@@ -126,18 +141,12 @@ class AudioMessagePlaybackTracker @Inject constructor() {
is Listener.State.Playing -> state.percentage
is Listener.State.Paused -> state.percentage
is Listener.State.Recording,
+ is Listener.State.Error,
Listener.State.Idle,
null -> null
}
}
- fun unregisterListeners() {
- listeners.forEach {
- it.value.onUpdate(Listener.State.Idle)
- }
- listeners.clear()
- }
-
companion object {
const val RECORDING_ID = "RECORDING_ID"
}
@@ -148,6 +157,7 @@ class AudioMessagePlaybackTracker @Inject constructor() {
sealed class State {
object Idle : State()
+ data class Error(val failure: Throwable) : State()
data class Playing(val playbackTime: Int, val percentage: Float) : State()
data class Paused(val playbackTime: Int, val percentage: Float) : State()
data class Recording(val amplitudeList: List) : State()
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/PollResponseDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/PollResponseDataFactory.kt
index 533397b4d8..8f81adcd32 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/PollResponseDataFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/PollResponseDataFactory.kt
@@ -44,7 +44,8 @@ class PollResponseDataFactory @Inject constructor(
)
},
winnerVoteCount = it.aggregatedContent?.winnerVoteCount ?: 0,
- totalVotes = it.aggregatedContent?.totalVotes ?: 0
+ totalVotes = it.aggregatedContent?.totalVotes ?: 0,
+ hasEncryptedRelatedEvents = it.encryptedRelatedEventIds.isNotEmpty(),
)
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt
index c6b90cdabe..7cde978e42 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt
@@ -22,6 +22,7 @@ import androidx.annotation.IdRes
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import im.vector.app.R
+import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.tintBackground
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.DrawableProvider
@@ -48,6 +49,7 @@ abstract class AbsMessageVoiceBroadcastItem() {
private fun renderStateBasedOnAudioPlayback(holder: Holder) {
audioMessagePlaybackTracker.track(attributes.informationData.eventId) { state ->
when (state) {
+ is AudioMessagePlaybackTracker.Listener.State.Error,
is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder)
is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state)
is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt
index 757246d4e4..a1a214785e 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt
@@ -90,7 +90,8 @@ data class PollResponseData(
val votes: Map?,
val totalVotes: Int = 0,
val winnerVoteCount: Int = 0,
- val isClosed: Boolean = false
+ val isClosed: Boolean = false,
+ val hasEncryptedRelatedEvents: Boolean = false,
) : Parcelable {
fun getVoteSummaryOfAnOption(optionId: String) = votes?.get(optionId)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt
index b788d79214..0aa2aaad3b 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt
@@ -20,11 +20,13 @@ import android.text.format.DateUtils
import android.widget.ImageButton
import android.widget.SeekBar
import android.widget.TextView
+import androidx.constraintlayout.widget.Group
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.onClick
+import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker.Listener.State
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
@@ -54,6 +56,16 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
}
}
player.addListener(voiceBroadcast, playerListener)
+
+ playbackTracker.track(voiceBroadcast.voiceBroadcastId) { playbackState ->
+ renderBackwardForwardButtons(holder, playbackState)
+ renderPlaybackError(holder, playbackState)
+ renderLiveIndicator(holder)
+ if (!isUserSeeking) {
+ holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) ?: 0
+ }
+ }
+
bindSeekBar(holder)
bindButtons(holder)
}
@@ -63,10 +75,11 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
playPauseButton.setOnClickListener {
if (player.currentVoiceBroadcast == voiceBroadcast) {
when (player.playingState) {
- VoiceBroadcastPlayer.State.PLAYING,
- VoiceBroadcastPlayer.State.BUFFERING -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause)
- VoiceBroadcastPlayer.State.PAUSED,
- VoiceBroadcastPlayer.State.IDLE -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast))
+ VoiceBroadcastPlayer.State.Playing,
+ VoiceBroadcastPlayer.State.Buffering -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause)
+ VoiceBroadcastPlayer.State.Paused,
+ is VoiceBroadcastPlayer.State.Error,
+ VoiceBroadcastPlayer.State.Idle -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast))
}
} else {
callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast))
@@ -100,17 +113,18 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
private fun renderPlayingState(holder: Holder, state: VoiceBroadcastPlayer.State) {
with(holder) {
- bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING
- voiceBroadcastMetadata.isVisible = state != VoiceBroadcastPlayer.State.BUFFERING
+ bufferingView.isVisible = state == VoiceBroadcastPlayer.State.Buffering
+ voiceBroadcastMetadata.isVisible = state != VoiceBroadcastPlayer.State.Buffering
when (state) {
- VoiceBroadcastPlayer.State.PLAYING,
- VoiceBroadcastPlayer.State.BUFFERING -> {
+ VoiceBroadcastPlayer.State.Playing,
+ VoiceBroadcastPlayer.State.Buffering -> {
playPauseButton.setImageResource(R.drawable.ic_play_pause_pause)
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast)
}
- VoiceBroadcastPlayer.State.IDLE,
- VoiceBroadcastPlayer.State.PAUSED -> {
+ is VoiceBroadcastPlayer.State.Error,
+ VoiceBroadcastPlayer.State.Idle,
+ VoiceBroadcastPlayer.State.Paused -> {
playPauseButton.setImageResource(R.drawable.ic_play_pause_play)
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast)
}
@@ -120,6 +134,18 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
}
}
+ private fun renderPlaybackError(holder: Holder, playbackState: State) {
+ with(holder) {
+ if (playbackState is State.Error) {
+ controlsGroup.isVisible = false
+ errorView.setTextOrHide(errorFormatter.toHumanReadable(playbackState.failure))
+ } else {
+ errorView.isVisible = false
+ controlsGroup.isVisible = true
+ }
+ }
+ }
+
private fun bindSeekBar(holder: Holder) {
with(holder) {
remainingTimeView.text = formatRemainingTime(duration)
@@ -141,13 +167,6 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
}
})
}
- playbackTracker.track(voiceBroadcast.voiceBroadcastId) { playbackState ->
- renderBackwardForwardButtons(holder, playbackState)
- renderLiveIndicator(holder)
- if (!isUserSeeking) {
- holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) ?: 0
- }
- }
}
private fun renderBackwardForwardButtons(holder: Holder, playbackState: State) {
@@ -187,6 +206,8 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
val broadcasterNameMetadata by bind(R.id.broadcasterNameMetadata)
val voiceBroadcastMetadata by bind(R.id.voiceBroadcastMetadata)
val listenersCountMetadata by bind(R.id.listenersCountMetadata)
+ val errorView by bind(R.id.errorView)
+ val controlsGroup by bind(R.id.controlsGroup)
}
companion object {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt
index d3f320db7d..a8e215b4a9 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt
@@ -124,6 +124,7 @@ abstract class MessageVoiceItem : AbsMessageItem() {
audioMessagePlaybackTracker.track(attributes.informationData.eventId) { state ->
when (state) {
+ is AudioMessagePlaybackTracker.Listener.State.Error,
is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder, waveformColorIdle, waveformColorPlayed)
is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state, waveformColorIdle, waveformColorPlayed)
is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state, waveformColorIdle, waveformColorPlayed)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt
index a55900a5c4..18c8ea3bde 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt
@@ -22,41 +22,33 @@ import com.airbnb.mvrx.Loading
import im.vector.app.R
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter
-import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.RoomListDisplayMode
import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter
+import im.vector.app.features.home.room.list.usecase.GetLatestPreviewableEventUseCase
import im.vector.app.features.home.room.typing.TypingHelper
import im.vector.app.features.voicebroadcast.isLive
-import im.vector.app.features.voicebroadcast.isVoiceBroadcast
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
-import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import org.matrix.android.sdk.api.extensions.orFalse
-import org.matrix.android.sdk.api.session.events.model.EventType
-import org.matrix.android.sdk.api.session.getRoom
-import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
-import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
-import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class RoomSummaryItemFactory @Inject constructor(
- private val sessionHolder: ActiveSessionHolder,
private val displayableEventFormatter: DisplayableEventFormatter,
private val dateFormatter: VectorDateFormatter,
private val stringProvider: StringProvider,
private val typingHelper: TypingHelper,
private val avatarRenderer: AvatarRenderer,
private val errorFormatter: ErrorFormatter,
- private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase,
+ private val getLatestPreviewableEventUseCase: GetLatestPreviewableEventUseCase,
) {
fun create(
@@ -142,7 +134,7 @@ class RoomSummaryItemFactory @Inject constructor(
val showSelected = selectedRoomIds.contains(roomSummary.roomId)
var latestFormattedEvent: CharSequence = ""
var latestEventTime = ""
- val latestEvent = roomSummary.getVectorLatestPreviewableEvent()
+ val latestEvent = getLatestPreviewableEventUseCase.execute(roomSummary.roomId)
if (latestEvent != null) {
latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect, roomSummary.isDirect.not())
latestEventTime = dateFormatter.format(latestEvent.root.originServerTs, DateFormatKind.ROOM_LIST)
@@ -150,7 +142,8 @@ class RoomSummaryItemFactory @Inject constructor(
val typingMessage = typingHelper.getTypingMessage(roomSummary.typingUsers)
// Skip typing while there is a live voice broadcast
- .takeUnless { latestEvent?.root?.asVoiceBroadcastEvent()?.isLive.orFalse() }.orEmpty()
+ .takeUnless { latestEvent?.root?.asVoiceBroadcastEvent()?.isLive.orFalse() }
+ .orEmpty()
return if (subtitle.isBlank() && displayMode == RoomListDisplayMode.FILTERED) {
createCenteredRoomSummaryItem(roomSummary, displayMode, showSelected, unreadCount, onClick, onLongClick)
@@ -240,14 +233,4 @@ class RoomSummaryItemFactory @Inject constructor(
else -> stringProvider.getQuantityString(R.plurals.search_space_multiple_parents, size - 1, directParentNames[0], size - 1)
}
}
-
- private fun RoomSummary.getVectorLatestPreviewableEvent(): TimelineEvent? {
- val room = sessionHolder.getSafeActiveSession()?.getRoom(roomId) ?: return latestPreviewableEvent
- val liveVoiceBroadcastTimelineEvent = getRoomLiveVoiceBroadcastsUseCase.execute(roomId).lastOrNull()
- ?.root?.eventId?.let { room.getTimelineEvent(it) }
- return latestPreviewableEvent?.takeIf { it.root.getClearType() == EventType.CALL_INVITE }
- ?: liveVoiceBroadcastTimelineEvent
- ?: latestPreviewableEvent
- ?.takeUnless { it.root.asMessageAudioEvent()?.isVoiceBroadcast().orFalse() } // Skip voice messages related to voice broadcast
- }
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCase.kt b/vector/src/main/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCase.kt
new file mode 100644
index 0000000000..6a50e87562
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCase.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2023 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.home.room.list.usecase
+
+import im.vector.app.core.di.ActiveSessionHolder
+import im.vector.app.features.voicebroadcast.isLive
+import im.vector.app.features.voicebroadcast.isVoiceBroadcast
+import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
+import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase
+import im.vector.app.features.voicebroadcast.voiceBroadcastId
+import org.matrix.android.sdk.api.extensions.orFalse
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.getRoom
+import org.matrix.android.sdk.api.session.room.Room
+import org.matrix.android.sdk.api.session.room.getTimelineEvent
+import org.matrix.android.sdk.api.session.room.model.RoomSummary
+import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
+import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+import javax.inject.Inject
+
+class GetLatestPreviewableEventUseCase @Inject constructor(
+ private val sessionHolder: ActiveSessionHolder,
+ private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase,
+) {
+
+ fun execute(roomId: String): TimelineEvent? {
+ val room = sessionHolder.getSafeActiveSession()?.getRoom(roomId) ?: return null
+ val roomSummary = room.roomSummary() ?: return null
+ return getCallEvent(roomSummary)
+ ?: getLiveVoiceBroadcastEvent(room)
+ ?: getDefaultLatestEvent(room, roomSummary)
+ }
+
+ private fun getCallEvent(roomSummary: RoomSummary): TimelineEvent? {
+ return roomSummary.latestPreviewableEvent
+ ?.takeIf { it.root.getClearType() == EventType.CALL_INVITE }
+ }
+
+ private fun getLiveVoiceBroadcastEvent(room: Room): TimelineEvent? {
+ return getRoomLiveVoiceBroadcastsUseCase.execute(room.roomId)
+ .lastOrNull()
+ ?.voiceBroadcastId
+ ?.let { room.getTimelineEvent(it) }
+ }
+
+ private fun getDefaultLatestEvent(room: Room, roomSummary: RoomSummary): TimelineEvent? {
+ val latestPreviewableEvent = roomSummary.latestPreviewableEvent
+
+ // If the default latest event is a live voice broadcast (paused or resumed), rely to the started event
+ val liveVoiceBroadcastEventId = latestPreviewableEvent?.root?.asVoiceBroadcastEvent()?.takeIf { it.isLive }?.voiceBroadcastId
+ if (liveVoiceBroadcastEventId != null) {
+ return room.getTimelineEvent(liveVoiceBroadcastEventId)
+ }
+
+ return latestPreviewableEvent
+ ?.takeUnless { it.root.asMessageAudioEvent()?.isVoiceBroadcast().orFalse() } // Skip voice messages related to voice broadcast
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/notifications/FilteredEventDetector.kt b/vector/src/main/java/im/vector/app/features/notifications/FilteredEventDetector.kt
new file mode 100644
index 0000000000..e21462b182
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/notifications/FilteredEventDetector.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2023 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.notifications
+
+import im.vector.app.ActiveSessionDataSource
+import im.vector.app.features.voicebroadcast.isVoiceBroadcast
+import im.vector.app.features.voicebroadcast.sequence
+import org.matrix.android.sdk.api.session.events.model.isVoiceMessage
+import org.matrix.android.sdk.api.session.getRoom
+import org.matrix.android.sdk.api.session.room.getTimelineEvent
+import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
+import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+import javax.inject.Inject
+
+class FilteredEventDetector @Inject constructor(
+ private val activeSessionDataSource: ActiveSessionDataSource
+) {
+
+ /**
+ * Returns true if the given event should be ignored.
+ * Used to skip notifications if a non expected message is received.
+ */
+ fun shouldBeIgnored(notifiableEvent: NotifiableEvent): Boolean {
+ val session = activeSessionDataSource.currentValue?.orNull() ?: return false
+
+ if (notifiableEvent is NotifiableMessageEvent) {
+ val room = session.getRoom(notifiableEvent.roomId) ?: return false
+ val timelineEvent = room.getTimelineEvent(notifiableEvent.eventId) ?: return false
+ return timelineEvent.shouldBeIgnored()
+ }
+ return false
+ }
+
+ /**
+ * Whether the timeline event should be ignored.
+ */
+ private fun TimelineEvent.shouldBeIgnored(): Boolean {
+ if (root.isVoiceMessage()) {
+ val audioEvent = root.asMessageAudioEvent()
+ // if the event is a voice message related to a voice broadcast, only show the event on the first chunk.
+ return audioEvent.isVoiceBroadcast() && audioEvent?.sequence != 1
+ }
+
+ return false
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt
index 4f05e83bd4..2d799034d9 100644
--- a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt
+++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt
@@ -47,6 +47,7 @@ class NotificationDrawerManager @Inject constructor(
private val notifiableEventProcessor: NotifiableEventProcessor,
private val notificationRenderer: NotificationRenderer,
private val notificationEventPersistence: NotificationEventPersistence,
+ private val filteredEventDetector: FilteredEventDetector,
private val buildMeta: BuildMeta,
) {
@@ -100,6 +101,11 @@ class NotificationDrawerManager @Inject constructor(
Timber.d("onNotifiableEventReceived(): is push: ${notifiableEvent.canBeReplaced}")
}
+ if (filteredEventDetector.shouldBeIgnored(notifiableEvent)) {
+ Timber.d("onNotifiableEventReceived(): ignore the event")
+ return
+ }
+
add(notifiableEvent)
}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt
index 3c37c92650..3ee1ed867c 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt
@@ -76,6 +76,8 @@ class RoomProfileActivity :
return ActivitySimpleBinding.inflate(layoutInflater)
}
+ override fun getCoordinatorLayout() = views.coordinatorLayout
+
override fun initUiAndData() {
sharedActionViewModel = viewModelProvider.get(RoomProfileSharedActionViewModel::class.java)
roomProfileArgs = intent?.extras?.getParcelableCompat(Mavericks.KEY_ARG) ?: return
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsAction.kt
index c18142a306..3fedbfc4a8 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsAction.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsAction.kt
@@ -18,4 +18,6 @@ package im.vector.app.features.roomprofile.polls
import im.vector.app.core.platform.VectorViewModelAction
-sealed interface RoomPollsAction : VectorViewModelAction
+sealed interface RoomPollsAction : VectorViewModelAction {
+ object LoadMorePolls : RoomPollsAction
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsLoadingError.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsLoadingError.kt
new file mode 100644
index 0000000000..71365087f1
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsLoadingError.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2023 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.roomprofile.polls
+
+class RoomPollsLoadingError : Throwable()
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewEvent.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewEvent.kt
index 231123563a..cb2069d824 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewEvent.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewEvent.kt
@@ -18,4 +18,6 @@ package im.vector.app.features.roomprofile.polls
import im.vector.app.core.platform.VectorViewEvents
-sealed class RoomPollsViewEvent : VectorViewEvents
+sealed class RoomPollsViewEvent : VectorViewEvents {
+ object LoadingError : RoomPollsViewEvent()
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModel.kt
index 95cb4717ca..b634881f70 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModel.kt
@@ -23,12 +23,20 @@ import dagger.assisted.AssistedInject
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel
+import im.vector.app.features.roomprofile.polls.list.domain.GetLoadedPollsStatusUseCase
+import im.vector.app.features.roomprofile.polls.list.domain.GetPollsUseCase
+import im.vector.app.features.roomprofile.polls.list.domain.LoadMorePollsUseCase
+import im.vector.app.features.roomprofile.polls.list.domain.SyncPollsUseCase
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
class RoomPollsViewModel @AssistedInject constructor(
@Assisted initialState: RoomPollsViewState,
private val getPollsUseCase: GetPollsUseCase,
+ private val getLoadedPollsStatusUseCase: GetLoadedPollsStatusUseCase,
+ private val loadMorePollsUseCase: LoadMorePollsUseCase,
+ private val syncPollsUseCase: SyncPollsUseCase,
) : VectorViewModel(initialState) {
@AssistedFactory
@@ -39,16 +47,63 @@ class RoomPollsViewModel @AssistedInject constructor(
companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory()
init {
- observePolls()
+ val roomId = initialState.roomId
+ updateLoadedPollStatus(roomId)
+ syncPolls(roomId)
+ observePolls(roomId)
}
- private fun observePolls() {
- getPollsUseCase.execute()
+ private fun updateLoadedPollStatus(roomId: String) {
+ val loadedPollsStatus = getLoadedPollsStatusUseCase.execute(roomId)
+ setState {
+ copy(
+ canLoadMore = loadedPollsStatus.canLoadMore,
+ nbLoadedDays = loadedPollsStatus.nbLoadedDays
+ )
+ }
+ }
+
+ private fun syncPolls(roomId: String) {
+ viewModelScope.launch {
+ setState { copy(isSyncing = true) }
+ val result = runCatching {
+ syncPollsUseCase.execute(roomId)
+ }
+ if (result.isFailure) {
+ _viewEvents.post(RoomPollsViewEvent.LoadingError)
+ }
+ setState { copy(isSyncing = false) }
+ }
+ }
+
+ private fun observePolls(roomId: String) {
+ getPollsUseCase.execute(roomId)
.onEach { setState { copy(polls = it) } }
.launchIn(viewModelScope)
}
override fun handle(action: RoomPollsAction) {
- // do nothing for now
+ when (action) {
+ RoomPollsAction.LoadMorePolls -> handleLoadMore()
+ }
+ }
+
+ private fun handleLoadMore() = withState { viewState ->
+ viewModelScope.launch {
+ setState { copy(isLoadingMore = true) }
+ val result = runCatching {
+ val status = loadMorePollsUseCase.execute(viewState.roomId)
+ setState {
+ copy(
+ canLoadMore = status.canLoadMore,
+ nbLoadedDays = status.nbLoadedDays,
+ )
+ }
+ }
+ if (result.isFailure) {
+ _viewEvents.post(RoomPollsViewEvent.LoadingError)
+ }
+ setState { copy(isLoadingMore = false) }
+ }
}
}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewState.kt
index 74794c99b1..fa985c5c76 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewState.kt
@@ -18,11 +18,19 @@ package im.vector.app.features.roomprofile.polls
import com.airbnb.mvrx.MavericksState
import im.vector.app.features.roomprofile.RoomProfileArgs
+import im.vector.app.features.roomprofile.polls.list.ui.PollSummary
data class RoomPollsViewState(
val roomId: String,
val polls: List = emptyList(),
+ val isLoadingMore: Boolean = false,
+ val canLoadMore: Boolean = true,
+ val nbLoadedDays: Int = 0,
+ val isSyncing: Boolean = false,
) : MavericksState {
constructor(roomProfileArgs: RoomProfileArgs) : this(roomId = roomProfileArgs.roomId)
+
+ fun hasNoPolls() = polls.isEmpty()
+ fun hasNoPollsAndCanLoadMore() = !isSyncing && hasNoPolls() && canLoadMore
}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/active/RoomActivePollsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/active/RoomActivePollsFragment.kt
index 1c6a03c480..441a4489b3 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/active/RoomActivePollsFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/active/RoomActivePollsFragment.kt
@@ -19,13 +19,17 @@ package im.vector.app.features.roomprofile.polls.active
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.features.roomprofile.polls.RoomPollsType
-import im.vector.app.features.roomprofile.polls.list.RoomPollsListFragment
+import im.vector.app.features.roomprofile.polls.list.ui.RoomPollsListFragment
@AndroidEntryPoint
class RoomActivePollsFragment : RoomPollsListFragment() {
- override fun getEmptyListTitle(): String {
- return getString(R.string.room_polls_active_no_item)
+ override fun getEmptyListTitle(canLoadMore: Boolean, nbLoadedDays: Int): String {
+ return if (canLoadMore) {
+ stringProvider.getQuantityString(R.plurals.room_polls_active_no_item_for_loaded_period, nbLoadedDays, nbLoadedDays)
+ } else {
+ getString(R.string.room_polls_active_no_item)
+ }
}
override fun getRoomPollsType(): RoomPollsType {
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/ended/RoomEndedPollsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/ended/RoomEndedPollsFragment.kt
index 8dd0cadadf..53f61126b5 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/ended/RoomEndedPollsFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/ended/RoomEndedPollsFragment.kt
@@ -19,13 +19,17 @@ package im.vector.app.features.roomprofile.polls.ended
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.features.roomprofile.polls.RoomPollsType
-import im.vector.app.features.roomprofile.polls.list.RoomPollsListFragment
+import im.vector.app.features.roomprofile.polls.list.ui.RoomPollsListFragment
@AndroidEntryPoint
class RoomEndedPollsFragment : RoomPollsListFragment() {
- override fun getEmptyListTitle(): String {
- return getString(R.string.room_polls_ended_no_item)
+ override fun getEmptyListTitle(canLoadMore: Boolean, nbLoadedDays: Int): String {
+ return if (canLoadMore) {
+ stringProvider.getQuantityString(R.plurals.room_polls_ended_no_item_for_loaded_period, nbLoadedDays, nbLoadedDays)
+ } else {
+ getString(R.string.room_polls_ended_no_item)
+ }
}
override fun getRoomPollsType(): RoomPollsType {
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollsListFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollsListFragment.kt
deleted file mode 100644
index 0d97bd8dcb..0000000000
--- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollsListFragment.kt
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (c) 2022 New Vector Ltd
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package im.vector.app.features.roomprofile.polls.list
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.core.view.isVisible
-import com.airbnb.mvrx.parentFragmentViewModel
-import com.airbnb.mvrx.withState
-import im.vector.app.core.extensions.cleanup
-import im.vector.app.core.extensions.configureWith
-import im.vector.app.core.platform.VectorBaseFragment
-import im.vector.app.databinding.FragmentRoomPollsListBinding
-import im.vector.app.features.roomprofile.polls.PollSummary
-import im.vector.app.features.roomprofile.polls.RoomPollsType
-import im.vector.app.features.roomprofile.polls.RoomPollsViewModel
-import timber.log.Timber
-import javax.inject.Inject
-
-abstract class RoomPollsListFragment :
- VectorBaseFragment(),
- RoomPollsController.Listener {
-
- @Inject
- lateinit var roomPollsController: RoomPollsController
-
- private val viewModel: RoomPollsViewModel by parentFragmentViewModel(RoomPollsViewModel::class)
-
- override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRoomPollsListBinding {
- return FragmentRoomPollsListBinding.inflate(inflater, container, false)
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- setupList()
- }
-
- abstract fun getEmptyListTitle(): String
-
- abstract fun getRoomPollsType(): RoomPollsType
-
- private fun setupList() {
- roomPollsController.listener = this
- views.roomPollsList.configureWith(roomPollsController)
- views.roomPollsEmptyTitle.text = getEmptyListTitle()
- }
-
- override fun onDestroyView() {
- cleanUpList()
- super.onDestroyView()
- }
-
- private fun cleanUpList() {
- views.roomPollsList.cleanup()
- roomPollsController.listener = null
- }
-
- override fun invalidate() = withState(viewModel) { viewState ->
- when (getRoomPollsType()) {
- RoomPollsType.ACTIVE -> renderList(viewState.polls.filterIsInstance(PollSummary.ActivePoll::class.java))
- RoomPollsType.ENDED -> renderList(viewState.polls.filterIsInstance(PollSummary.EndedPoll::class.java))
- }
- }
-
- private fun renderList(polls: List) {
- roomPollsController.setData(polls)
- views.roomPollsEmptyTitle.isVisible = polls.isEmpty()
- }
-
- override fun onPollClicked(pollId: String) {
- // TODO navigate to details
- Timber.d("poll with id $pollId clicked")
- }
-}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/LoadedPollsStatus.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/LoadedPollsStatus.kt
new file mode 100644
index 0000000000..c3971bb289
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/LoadedPollsStatus.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2023 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.roomprofile.polls.list.data
+
+data class LoadedPollsStatus(
+ val canLoadMore: Boolean,
+ val nbLoadedDays: Int,
+)
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/GetPollsUseCase.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollDataSource.kt
similarity index 64%
rename from vector/src/main/java/im/vector/app/features/roomprofile/polls/GetPollsUseCase.kt
rename to vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollDataSource.kt
index 6f2a757ed7..c0efb1efa1 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/GetPollsUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollDataSource.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2022 New Vector Ltd
+ * Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,23 +14,60 @@
* limitations under the License.
*/
-package im.vector.app.features.roomprofile.polls
+package im.vector.app.features.roomprofile.polls.list.data
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState
+import im.vector.app.features.roomprofile.polls.list.ui.PollSummary
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import timber.log.Timber
import javax.inject.Inject
+import javax.inject.Singleton
-class GetPollsUseCase @Inject constructor() {
+@Singleton
+class RoomPollDataSource @Inject constructor() {
- fun execute(): Flow> {
- // TODO unmock and add unit tests
- return flowOf(getActivePolls() + getEndedPolls())
- .map { it.sortedByDescending { poll -> poll.creationTimestamp } }
+ private val pollsFlow = MutableSharedFlow>(replay = 1)
+ private val polls = mutableListOf()
+ private var fakeLoadCounter = 0
+
+ // TODO
+ // unmock using SDK service + add unit tests
+ // after unmock, expose domain layer model (entity) and do the mapping to PollSummary in the UI layer
+ fun getPolls(roomId: String): Flow> {
+ Timber.d("roomId=$roomId")
+ return pollsFlow.asSharedFlow()
}
- private fun getActivePolls(): List {
+ fun getLoadedPollsStatus(roomId: String): LoadedPollsStatus {
+ Timber.d("roomId=$roomId")
+ return LoadedPollsStatus(
+ canLoadMore = canLoadMore(),
+ nbLoadedDays = fakeLoadCounter * 30,
+ )
+ }
+
+ private fun canLoadMore(): Boolean {
+ return fakeLoadCounter < 2
+ }
+
+ suspend fun loadMorePolls(roomId: String): LoadedPollsStatus {
+ // TODO
+ // unmock using SDK service + add unit tests
+ delay(3000)
+ fakeLoadCounter++
+ when (fakeLoadCounter) {
+ 1 -> polls.addAll(getActivePollsPart1() + getEndedPollsPart1())
+ 2 -> polls.addAll(getActivePollsPart2() + getEndedPollsPart2())
+ else -> Unit
+ }
+ pollsFlow.emit(polls)
+ return getLoadedPollsStatus(roomId)
+ }
+
+ private fun getActivePollsPart1(): List {
return listOf(
PollSummary.ActivePoll(
id = "id1",
@@ -44,6 +81,11 @@ class GetPollsUseCase @Inject constructor() {
creationTimestamp = 1656194400000,
title = "Which sport should the pupils do this year?"
),
+ )
+ }
+
+ private fun getActivePollsPart2(): List {
+ return listOf(
PollSummary.ActivePoll(
id = "id3",
// 2022/06/24 UTC+1
@@ -59,7 +101,7 @@ class GetPollsUseCase @Inject constructor() {
)
}
- private fun getEndedPolls(): List {
+ private fun getEndedPollsPart1(): List {
return listOf(
PollSummary.EndedPoll(
id = "id1-ended",
@@ -77,6 +119,11 @@ class GetPollsUseCase @Inject constructor() {
)
),
),
+ )
+ }
+
+ private fun getEndedPollsPart2(): List {
+ return listOf(
PollSummary.EndedPoll(
id = "id2-ended",
// 2022/06/26 UTC+1
@@ -111,4 +158,17 @@ class GetPollsUseCase @Inject constructor() {
),
)
}
+
+ suspend fun syncPolls(roomId: String) {
+ Timber.d("roomId=$roomId")
+ // TODO
+ // unmock using SDK service + add unit tests
+ if (fakeLoadCounter == 0) {
+ // fake first load
+ loadMorePolls(roomId)
+ } else {
+ // fake sync
+ delay(3000)
+ }
+ }
}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollRepository.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollRepository.kt
new file mode 100644
index 0000000000..d3577df6c1
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollRepository.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2023 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.roomprofile.polls.list.data
+
+import im.vector.app.features.roomprofile.polls.list.ui.PollSummary
+import kotlinx.coroutines.flow.Flow
+import javax.inject.Inject
+
+class RoomPollRepository @Inject constructor(
+ private val roomPollDataSource: RoomPollDataSource,
+) {
+
+ // TODO after unmock, expose domain layer model (entity) and do the mapping to PollSummary in the UI layer
+ fun getPolls(roomId: String): Flow> {
+ return roomPollDataSource.getPolls(roomId)
+ }
+
+ fun getLoadedPollsStatus(roomId: String): LoadedPollsStatus {
+ return roomPollDataSource.getLoadedPollsStatus(roomId)
+ }
+
+ suspend fun loadMorePolls(roomId: String): LoadedPollsStatus {
+ return roomPollDataSource.loadMorePolls(roomId)
+ }
+
+ suspend fun syncPolls(roomId: String) {
+ return roomPollDataSource.syncPolls(roomId)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCase.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCase.kt
new file mode 100644
index 0000000000..55324b253f
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCase.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2023 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.roomprofile.polls.list.domain
+
+import im.vector.app.features.roomprofile.polls.list.data.LoadedPollsStatus
+import im.vector.app.features.roomprofile.polls.list.data.RoomPollRepository
+import javax.inject.Inject
+
+class GetLoadedPollsStatusUseCase @Inject constructor(
+ private val roomPollRepository: RoomPollRepository,
+) {
+
+ fun execute(roomId: String): LoadedPollsStatus {
+ return roomPollRepository.getLoadedPollsStatus(roomId)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/GetPollsUseCase.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/GetPollsUseCase.kt
new file mode 100644
index 0000000000..be2afb226f
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/GetPollsUseCase.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2023 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.roomprofile.polls.list.domain
+
+import im.vector.app.features.roomprofile.polls.list.data.RoomPollRepository
+import im.vector.app.features.roomprofile.polls.list.ui.PollSummary
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import javax.inject.Inject
+
+class GetPollsUseCase @Inject constructor(
+ private val roomPollRepository: RoomPollRepository,
+) {
+
+ fun execute(roomId: String): Flow> {
+ return roomPollRepository.getPolls(roomId)
+ .map { it.sortedByDescending { poll -> poll.creationTimestamp } }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/LoadMorePollsUseCase.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/LoadMorePollsUseCase.kt
new file mode 100644
index 0000000000..df3270552d
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/LoadMorePollsUseCase.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2023 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.roomprofile.polls.list.domain
+
+import im.vector.app.features.roomprofile.polls.list.data.LoadedPollsStatus
+import im.vector.app.features.roomprofile.polls.list.data.RoomPollRepository
+import javax.inject.Inject
+
+class LoadMorePollsUseCase @Inject constructor(
+ private val roomPollRepository: RoomPollRepository,
+) {
+
+ suspend fun execute(roomId: String): LoadedPollsStatus {
+ return roomPollRepository.loadMorePolls(roomId)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/SyncPollsUseCase.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/SyncPollsUseCase.kt
new file mode 100644
index 0000000000..b6a344f7f8
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/SyncPollsUseCase.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2023 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.roomprofile.polls.list.domain
+
+import im.vector.app.features.roomprofile.polls.list.data.RoomPollRepository
+import javax.inject.Inject
+
+/**
+ * Sync the polls of a given room from last manual loading (see LoadMorePollsUseCase) until now.
+ */
+class SyncPollsUseCase @Inject constructor(
+ private val roomPollRepository: RoomPollRepository,
+) {
+
+ suspend fun execute(roomId: String) {
+ roomPollRepository.syncPolls(roomId)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/PollSummary.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/PollSummary.kt
similarity index 92%
rename from vector/src/main/java/im/vector/app/features/roomprofile/polls/PollSummary.kt
rename to vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/PollSummary.kt
index f24ac8b8a6..5c1eee0d00 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/PollSummary.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/PollSummary.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2022 New Vector Ltd
+ * Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package im.vector.app.features.roomprofile.polls
+package im.vector.app.features.roomprofile.polls.list.ui
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollItem.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/RoomPollItem.kt
similarity index 96%
rename from vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollItem.kt
rename to vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/RoomPollItem.kt
index da00fedddb..d675fe9bce 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollItem.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/RoomPollItem.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2022 New Vector Ltd
+ * Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package im.vector.app.features.roomprofile.polls.list
+package im.vector.app.features.roomprofile.polls.list.ui
import android.widget.LinearLayout
import android.widget.TextView
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/RoomPollLoadMoreItem.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/RoomPollLoadMoreItem.kt
new file mode 100644
index 0000000000..f16b9fa5a0
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/RoomPollLoadMoreItem.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2023 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.roomprofile.polls.list.ui
+
+import android.widget.Button
+import android.widget.ProgressBar
+import androidx.core.view.isVisible
+import com.airbnb.epoxy.EpoxyAttribute
+import com.airbnb.epoxy.EpoxyModelClass
+import im.vector.app.R
+import im.vector.app.core.epoxy.ClickListener
+import im.vector.app.core.epoxy.VectorEpoxyHolder
+import im.vector.app.core.epoxy.VectorEpoxyModel
+import im.vector.app.core.epoxy.onClick
+
+@EpoxyModelClass
+abstract class RoomPollLoadMoreItem : VectorEpoxyModel(R.layout.item_poll_load_more) {
+
+ @EpoxyAttribute
+ var loadingMore: Boolean = false
+
+ @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
+ var clickListener: ClickListener? = null
+
+ override fun bind(holder: Holder) {
+ super.bind(holder)
+ holder.loadMoreButton.isEnabled = loadingMore.not()
+ holder.loadMoreButton.onClick(clickListener)
+ holder.loadMoreProgressBar.isVisible = loadingMore
+ }
+
+ class Holder : VectorEpoxyHolder() {
+ val loadMoreButton by bind