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/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/dependencies.gradle b/dependencies.gradle
index f6be35edc0..4977543822 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -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"
@@ -103,7 +103,7 @@ ext.libs = [
],
element : [
'opusencoder' : "io.element.android:opusencoder:1.1.0",
- 'wysiwyg' : "io.element.android:wysiwyg:0.17.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/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index e4935aa491..e6556520cf 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -795,7 +795,7 @@
Shows all threads you’ve participated inKeep discussions organized with threadsThreads 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
@@ -3207,10 +3207,22 @@
Closed pollResults are only revealed when you end the pollEnded the poll.
+ Due to decryption errors, some votes may not be countedActive pollsThere 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 pollsThere 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/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
index c9eabeab48..03672ae81c 100644
--- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
+++ b/matrix-sdk-android/src/kotlinCrypto/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/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/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/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
index ba102a7a48..2b7e9a04a1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
@@ -64,6 +64,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo044
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo045
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo046
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo047
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo048
import org.matrix.android.sdk.internal.util.Normalizer
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
import javax.inject.Inject
@@ -72,7 +73,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
private val normalizer: Normalizer
) : MatrixRealmMigration(
dbName = "Session",
- schemaVersion = 47L,
+ schemaVersion = 48L,
) {
/**
* Forces all RealmSessionStoreMigration instances to be equal.
@@ -129,5 +130,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 45) MigrateSessionTo045(realm).perform()
if (oldVersion < 46) MigrateSessionTo046(realm).perform()
if (oldVersion < 47) MigrateSessionTo047(realm).perform()
+ if (oldVersion < 48) MigrateSessionTo048(realm).perform()
}
}
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
new file mode 100644
index 0000000000..4299054c56
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo048.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.internal.database.migration
+
+import io.realm.DynamicRealm
+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("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/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/src/main/java/im/vector/app/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt
index 78aaa058e9..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
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
index c02eb1fa8a..56ee9ffb5a 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt
@@ -138,7 +138,7 @@ class MessageComposerViewModel @AssistedInject constructor(
}
private fun handleOnTextChanged(action: MessageComposerAction.OnTextChanged) {
- val needsSendButtonVisibilityUpdate = currentComposerText.isEmpty() != action.text.isEmpty()
+ val needsSendButtonVisibilityUpdate = currentComposerText.isBlank() != action.text.isBlank()
currentComposerText = SpannableString(action.text)
if (needsSendButtonVisibilityUpdate) {
updateIsSendButtonVisibility(true)
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/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/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/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