Merge branch 'develop' into feature/bca/rust_flavor
This commit is contained in:
commit
1ab4a2fd8a
|
@ -0,0 +1 @@
|
||||||
|
[Poll] Warning message on decryption failure of some events
|
|
@ -0,0 +1 @@
|
||||||
|
[Poll] History list: Load more UI mechanism
|
|
@ -18,7 +18,7 @@ def markwon = "4.6.2"
|
||||||
def moshi = "1.14.0"
|
def moshi = "1.14.0"
|
||||||
def lifecycle = "2.5.1"
|
def lifecycle = "2.5.1"
|
||||||
def flowBinding = "1.2.0"
|
def flowBinding = "1.2.0"
|
||||||
def flipper = "0.176.1"
|
def flipper = "0.177.0"
|
||||||
def epoxy = "5.0.0"
|
def epoxy = "5.0.0"
|
||||||
def mavericks = "3.0.1"
|
def mavericks = "3.0.1"
|
||||||
def glide = "4.14.2"
|
def glide = "4.14.2"
|
||||||
|
@ -103,7 +103,7 @@ ext.libs = [
|
||||||
],
|
],
|
||||||
element : [
|
element : [
|
||||||
'opusencoder' : "io.element.android:opusencoder:1.1.0",
|
'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 : [
|
squareup : [
|
||||||
'moshi' : "com.squareup.moshi:moshi:$moshi",
|
'moshi' : "com.squareup.moshi:moshi:$moshi",
|
||||||
|
|
|
@ -795,7 +795,7 @@
|
||||||
<string name="thread_list_modal_my_threads_subtitle">Shows all threads you’ve participated in</string>
|
<string name="thread_list_modal_my_threads_subtitle">Shows all threads you’ve participated in</string>
|
||||||
<string name="thread_list_empty_title">Keep discussions organized with threads</string>
|
<string name="thread_list_empty_title">Keep discussions organized with threads</string>
|
||||||
<string name="thread_list_empty_subtitle">Threads help keep your conversations on-topic and easy to track.</string>
|
<string name="thread_list_empty_subtitle">Threads help keep your conversations on-topic and easy to track.</string>
|
||||||
<string name="thread_list_not_available">You\'re homeserver does not support listing threads yet.</string>
|
<string name="thread_list_not_available">Your homeserver does not support listing threads yet.</string>
|
||||||
<!-- Parameter %s will be replaced by the value of string reply_in_thread -->
|
<!-- Parameter %s will be replaced by the value of string reply_in_thread -->
|
||||||
<string name="thread_list_empty_notice">Tip: Long tap a message and use “%s”.</string>
|
<string name="thread_list_empty_notice">Tip: Long tap a message and use “%s”.</string>
|
||||||
<string name="search_thread_from_a_thread">From a Thread</string>
|
<string name="search_thread_from_a_thread">From a Thread</string>
|
||||||
|
@ -3207,10 +3207,22 @@
|
||||||
<string name="closed_poll_option_title">Closed poll</string>
|
<string name="closed_poll_option_title">Closed poll</string>
|
||||||
<string name="closed_poll_option_description">Results are only revealed when you end the poll</string>
|
<string name="closed_poll_option_description">Results are only revealed when you end the poll</string>
|
||||||
<string name="ended_poll_indicator">Ended the poll.</string>
|
<string name="ended_poll_indicator">Ended the poll.</string>
|
||||||
|
<string name="unable_to_decrypt_some_events_in_poll">Due to decryption errors, some votes may not be counted</string>
|
||||||
<string name="room_polls_active">Active polls</string>
|
<string name="room_polls_active">Active polls</string>
|
||||||
<string name="room_polls_active_no_item">There are no active polls in this room</string>
|
<string name="room_polls_active_no_item">There are no active polls in this room</string>
|
||||||
|
<plurals name="room_polls_active_no_item_for_loaded_period">
|
||||||
|
<item quantity="one">"There are no active polls for the past day.\nLoad more polls to view polls for previous days."</item>
|
||||||
|
<item quantity="other">"There are no active polls for the past %1$d days.\nLoad more polls to view polls for previous days."</item>
|
||||||
|
</plurals>
|
||||||
<string name="room_polls_ended">Past polls</string>
|
<string name="room_polls_ended">Past polls</string>
|
||||||
<string name="room_polls_ended_no_item">There are no past polls in this room</string>
|
<string name="room_polls_ended_no_item">There are no past polls in this room</string>
|
||||||
|
<plurals name="room_polls_ended_no_item_for_loaded_period">
|
||||||
|
<item quantity="one">"There are no past polls for the past day.\nLoad more polls to view polls for previous days."</item>
|
||||||
|
<item quantity="other">"There are no past polls for the past %1$d days.\nLoad more polls to view polls for previous days."</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="room_polls_wait_for_display">Displaying polls</string>
|
||||||
|
<string name="room_polls_load_more">Load more polls</string>
|
||||||
|
<string name="room_polls_loading_error">Error fetching polls.</string>
|
||||||
|
|
||||||
<!-- Location -->
|
<!-- Location -->
|
||||||
<string name="location_activity_title_static_sharing">Share location</string>
|
<string name="location_activity_title_static_sharing">Share location</string>
|
||||||
|
|
|
@ -59,7 +59,7 @@ internal class EventDecryptor @Inject constructor(
|
||||||
private val sendToDeviceTask: SendToDeviceTask,
|
private val sendToDeviceTask: SendToDeviceTask,
|
||||||
private val deviceListManager: DeviceListManager,
|
private val deviceListManager: DeviceListManager,
|
||||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
||||||
private val cryptoStore: IMXCryptoStore
|
private val cryptoStore: IMXCryptoStore,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -23,5 +23,7 @@ data class PollResponseAggregatedSummary(
|
||||||
val nbOptions: Int = 0,
|
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)
|
// The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk)
|
||||||
val sourceEvents: List<String>,
|
val sourceEvents: List<String>,
|
||||||
val localEchos: List<String>
|
val localEchos: List<String>,
|
||||||
|
// list of related event ids which are encrypted due to decryption failure
|
||||||
|
val encryptedRelatedEventIds: List<String>,
|
||||||
)
|
)
|
||||||
|
|
|
@ -17,11 +17,16 @@
|
||||||
package org.matrix.android.sdk.internal.database
|
package org.matrix.android.sdk.internal.database
|
||||||
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
|
import io.realm.Realm
|
||||||
import io.realm.RealmConfiguration
|
import io.realm.RealmConfiguration
|
||||||
import io.realm.RealmResults
|
import io.realm.RealmResults
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
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.mapper.asDomain
|
||||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.EventInsertEntity
|
import org.matrix.android.sdk.internal.database.model.EventInsertEntity
|
||||||
|
@ -34,7 +39,7 @@ import javax.inject.Inject
|
||||||
|
|
||||||
internal class EventInsertLiveObserver @Inject constructor(
|
internal class EventInsertLiveObserver @Inject constructor(
|
||||||
@SessionDatabase realmConfiguration: RealmConfiguration,
|
@SessionDatabase realmConfiguration: RealmConfiguration,
|
||||||
private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>
|
private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>,
|
||||||
) :
|
) :
|
||||||
RealmLiveEntityObserver<EventInsertEntity>(realmConfiguration) {
|
RealmLiveEntityObserver<EventInsertEntity>(realmConfiguration) {
|
||||||
|
|
||||||
|
@ -50,48 +55,90 @@ internal class EventInsertLiveObserver @Inject constructor(
|
||||||
if (!results.isLoaded || results.isEmpty()) {
|
if (!results.isLoaded || results.isEmpty()) {
|
||||||
return@withLock
|
return@withLock
|
||||||
}
|
}
|
||||||
val idsToDeleteAfterProcess = ArrayList<String>()
|
val eventsToProcess = ArrayList<EventInsertEntity>(results.size)
|
||||||
val filteredEvents = ArrayList<EventInsertEntity>(results.size)
|
val eventsToIgnore = ArrayList<EventInsertEntity>(results.size)
|
||||||
|
|
||||||
Timber.v("EventInsertEntity updated with ${results.size} results in db")
|
Timber.v("EventInsertEntity updated with ${results.size} results in db")
|
||||||
results.forEach {
|
results.forEach {
|
||||||
if (shouldProcess(it)) {
|
// don't use copy from realm over there
|
||||||
// don't use copy from realm over there
|
val copiedEvent = EventInsertEntity(
|
||||||
val copiedEvent = EventInsertEntity(
|
eventId = it.eventId,
|
||||||
eventId = it.eventId,
|
eventType = it.eventType
|
||||||
eventType = it.eventType
|
).apply {
|
||||||
).apply {
|
insertType = it.insertType
|
||||||
insertType = it.insertType
|
}
|
||||||
}
|
|
||||||
filteredEvents.add(copiedEvent)
|
if (shouldProcess(it)) {
|
||||||
|
eventsToProcess.add(copiedEvent)
|
||||||
|
} else {
|
||||||
|
eventsToIgnore.add(copiedEvent)
|
||||||
}
|
}
|
||||||
idsToDeleteAfterProcess.add(it.eventId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
awaitTransaction(realmConfiguration) { realm ->
|
awaitTransaction(realmConfiguration) { realm ->
|
||||||
Timber.v("##Transaction: There are ${filteredEvents.size} events to process ")
|
Timber.v("##Transaction: There are ${eventsToProcess.size} events to process")
|
||||||
filteredEvents.forEach { eventInsert ->
|
|
||||||
|
val idsToDeleteAfterProcess = ArrayList<String>()
|
||||||
|
val idsOfEncryptedEvents = ArrayList<String>()
|
||||||
|
val getAndTriageEvent: (EventInsertEntity) -> Event? = { eventInsert ->
|
||||||
val eventId = eventInsert.eventId
|
val eventId = eventInsert.eventId
|
||||||
val event = EventEntity.where(realm, eventId).findFirst()
|
val event = getEvent(realm, eventId)
|
||||||
if (event == null) {
|
if (event?.getClearType() == EventType.ENCRYPTED) {
|
||||||
Timber.v("Event $eventId not found")
|
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
|
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)
|
realm.where(EventInsertEntity::class.java)
|
||||||
.`in`(EventInsertEntityFields.EVENT_ID, idsToDeleteAfterProcess.toTypedArray())
|
.`in`(EventInsertEntityFields.EVENT_ID, idsToDeleteAfterProcess.toTypedArray())
|
||||||
.findAll()
|
.findAll()
|
||||||
.deleteAllFromRealm()
|
.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() }
|
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<EncryptedEventContent>()?.relatesTo != null
|
||||||
|
}
|
||||||
|
|
||||||
private fun shouldProcess(eventInsertEntity: EventInsertEntity): Boolean {
|
private fun shouldProcess(eventInsertEntity: EventInsertEntity): Boolean {
|
||||||
return processors.any {
|
return processors.any {
|
||||||
it.shouldProcess(eventInsertEntity.eventId, eventInsertEntity.eventType, eventInsertEntity.insertType)
|
it.shouldProcess(eventInsertEntity.eventId, eventInsertEntity.eventType, eventInsertEntity.insertType)
|
||||||
|
|
|
@ -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.MigrateSessionTo045
|
||||||
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo046
|
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.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.Normalizer
|
||||||
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
|
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -72,7 +73,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
||||||
private val normalizer: Normalizer
|
private val normalizer: Normalizer
|
||||||
) : MatrixRealmMigration(
|
) : MatrixRealmMigration(
|
||||||
dbName = "Session",
|
dbName = "Session",
|
||||||
schemaVersion = 47L,
|
schemaVersion = 48L,
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* Forces all RealmSessionStoreMigration instances to be equal.
|
* Forces all RealmSessionStoreMigration instances to be equal.
|
||||||
|
@ -129,5 +130,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
||||||
if (oldVersion < 45) MigrateSessionTo045(realm).perform()
|
if (oldVersion < 45) MigrateSessionTo045(realm).perform()
|
||||||
if (oldVersion < 46) MigrateSessionTo046(realm).perform()
|
if (oldVersion < 46) MigrateSessionTo046(realm).perform()
|
||||||
if (oldVersion < 47) MigrateSessionTo047(realm).perform()
|
if (oldVersion < 47) MigrateSessionTo047(realm).perform()
|
||||||
|
if (oldVersion < 48) MigrateSessionTo048(realm).perform()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,8 @@ internal object PollResponseAggregatedSummaryEntityMapper {
|
||||||
closedTime = entity.closedTime,
|
closedTime = entity.closedTime,
|
||||||
localEchos = entity.sourceLocalEchoEvents.toList(),
|
localEchos = entity.sourceLocalEchoEvents.toList(),
|
||||||
sourceEvents = entity.sourceEvents.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,
|
nbOptions = model.nbOptions,
|
||||||
closedTime = model.closedTime,
|
closedTime = model.closedTime,
|
||||||
sourceEvents = RealmList<String>().apply { addAll(model.sourceEvents) },
|
sourceEvents = RealmList<String>().apply { addAll(model.sourceEvents) },
|
||||||
sourceLocalEchoEvents = RealmList<String>().apply { addAll(model.localEchos) }
|
sourceLocalEchoEvents = RealmList<String>().apply { addAll(model.localEchos) },
|
||||||
|
encryptedRelatedEventIds = RealmList<String>().apply { addAll(model.encryptedRelatedEventIds) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,7 +27,7 @@ internal open class EventInsertEntity(
|
||||||
var eventType: String = "",
|
var eventType: String = "",
|
||||||
/**
|
/**
|
||||||
* This flag will be used to filter EventInsertEntity in EventInsertLiveObserver.
|
* 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
|
var canBeProcessed: Boolean = true
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
|
@ -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)
|
// The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk)
|
||||||
var sourceEvents: RealmList<String> = RealmList(),
|
var sourceEvents: RealmList<String> = RealmList(),
|
||||||
var sourceLocalEchoEvents: RealmList<String> = RealmList()
|
var sourceLocalEchoEvents: RealmList<String> = RealmList(),
|
||||||
|
// list of related event ids which are encrypted due to decryption failure
|
||||||
|
var encryptedRelatedEventIds: RealmList<String> = RealmList(),
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
||||||
companion object
|
companion object
|
||||||
|
|
|
@ -72,7 +72,7 @@ import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntit
|
||||||
SpaceParentSummaryEntity::class,
|
SpaceParentSummaryEntity::class,
|
||||||
UserPresenceEntity::class,
|
UserPresenceEntity::class,
|
||||||
ThreadSummaryEntity::class,
|
ThreadSummaryEntity::class,
|
||||||
ThreadListPageEntity::class
|
ThreadListPageEntity::class,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
internal class SessionRealmModule
|
internal class SessionRealmModule
|
||||||
|
|
|
@ -20,7 +20,6 @@ import io.realm.Realm
|
||||||
import io.realm.RealmList
|
import io.realm.RealmList
|
||||||
import io.realm.RealmQuery
|
import io.realm.RealmQuery
|
||||||
import io.realm.kotlin.where
|
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.EventEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.EventEntityFields
|
import org.matrix.android.sdk.internal.database.model.EventEntityFields
|
||||||
import org.matrix.android.sdk.internal.database.model.EventInsertEntity
|
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)
|
.equalTo(EventEntityFields.ROOM_ID, roomId)
|
||||||
.findFirst()
|
.findFirst()
|
||||||
return if (eventEntity == null) {
|
return if (eventEntity == null) {
|
||||||
val canBeProcessed = type != EventType.ENCRYPTED || decryptionResultJson != null
|
val insertEntity = EventInsertEntity(eventId = eventId, eventType = type, canBeProcessed = true)
|
||||||
val insertEntity = EventInsertEntity(eventId = eventId, eventType = type, canBeProcessed = canBeProcessed).apply {
|
insertEntity.insertType = insertType
|
||||||
this.insertType = insertType
|
|
||||||
}
|
|
||||||
realm.insert(insertEntity)
|
realm.insert(insertEntity)
|
||||||
// copy this event entity and return it
|
// copy this event entity and return it
|
||||||
realm.copyToRealm(this)
|
realm.copyToRealm(this)
|
||||||
|
|
|
@ -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.EventInsertLiveProcessor
|
||||||
import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.LiveLocationAggregationProcessor
|
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.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.session.room.state.StateEventDataSource
|
||||||
import org.matrix.android.sdk.internal.util.time.Clock
|
import org.matrix.android.sdk.internal.util.time.Clock
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -73,6 +74,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
||||||
private val sessionManager: SessionManager,
|
private val sessionManager: SessionManager,
|
||||||
private val liveLocationAggregationProcessor: LiveLocationAggregationProcessor,
|
private val liveLocationAggregationProcessor: LiveLocationAggregationProcessor,
|
||||||
private val pollAggregationProcessor: PollAggregationProcessor,
|
private val pollAggregationProcessor: PollAggregationProcessor,
|
||||||
|
private val encryptedReferenceAggregationProcessor: EncryptedReferenceAggregationProcessor,
|
||||||
private val editValidator: EventEditValidator,
|
private val editValidator: EventEditValidator,
|
||||||
private val clock: Clock,
|
private val clock: Clock,
|
||||||
) : EventInsertLiveProcessor {
|
) : EventInsertLiveProcessor {
|
||||||
|
@ -140,6 +142,16 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
||||||
Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}")
|
Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}")
|
||||||
handleReaction(realm, event, roomId, isLocalEcho)
|
handleReaction(realm, event, roomId, isLocalEcho)
|
||||||
}
|
}
|
||||||
|
EventType.ENCRYPTED -> {
|
||||||
|
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
|
||||||
|
processEncryptedContent(
|
||||||
|
encryptedEventContent = encryptedEventContent,
|
||||||
|
realm = realm,
|
||||||
|
event = event,
|
||||||
|
roomId = roomId,
|
||||||
|
isLocalEcho = isLocalEcho,
|
||||||
|
)
|
||||||
|
}
|
||||||
EventType.MESSAGE -> {
|
EventType.MESSAGE -> {
|
||||||
if (event.unsignedData?.relations?.annotations != null) {
|
if (event.unsignedData?.relations?.annotations != null) {
|
||||||
Timber.v("###REACTION Aggregation in room $roomId for event ${event.eventId}")
|
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<EncryptedEventContent>()
|
|
||||||
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 -> {
|
EventType.REDACTION -> {
|
||||||
val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() }
|
val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() }
|
||||||
?: return
|
?: 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
|
// OPT OUT serer aggregation until API mature enough
|
||||||
private val SHOULD_HANDLE_SERVER_AGREGGATION = false // should be true to work with e2e
|
private val SHOULD_HANDLE_SERVER_AGREGGATION = false // should be true to work with e2e
|
||||||
|
|
||||||
|
|
|
@ -155,6 +155,8 @@ internal class DefaultPollAggregationProcessor @Inject constructor(
|
||||||
)
|
)
|
||||||
aggregatedPollSummaryEntity.aggregatedContent = ContentMapper.map(newSumModel.toContent())
|
aggregatedPollSummaryEntity.aggregatedContent = ContentMapper.map(newSumModel.toContent())
|
||||||
|
|
||||||
|
event.eventId?.let { removeEncryptedRelatedEventIdIfNeeded(aggregatedPollSummaryEntity, it) }
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,6 +182,8 @@ internal class DefaultPollAggregationProcessor @Inject constructor(
|
||||||
aggregatedPollSummaryEntity.sourceEvents.add(event.eventId)
|
aggregatedPollSummaryEntity.sourceEvents.add(event.eventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
event.eventId?.let { removeEncryptedRelatedEventIdIfNeeded(aggregatedPollSummaryEntity, it) }
|
||||||
|
|
||||||
if (!isLocalEcho) {
|
if (!isLocalEcho) {
|
||||||
ensurePollIsFullyAggregated(roomId, pollEventId)
|
ensurePollIsFullyAggregated(roomId, pollEventId)
|
||||||
}
|
}
|
||||||
|
@ -226,4 +230,10 @@ internal class DefaultPollAggregationProcessor @Inject constructor(
|
||||||
fetchPollResponseEventsTask.execute(params)
|
fetchPollResponseEventsTask.execute(params)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun removeEncryptedRelatedEventIdIfNeeded(aggregatedPollSummaryEntity: PollResponseAggregatedSummaryEntity, eventId: String) {
|
||||||
|
if (aggregatedPollSummaryEntity.encryptedRelatedEventIds.contains(eventId)) {
|
||||||
|
aggregatedPollSummaryEntity.encryptedRelatedEventIds.remove(eventId)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
|
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.
|
* 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.
|
* This function will only handle if the poll is edited and will update the poll summary entity.
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Event>().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<EventAnnotationsSummaryEntity>()
|
||||||
|
.givenEqualTo(EventAnnotationsSummaryEntityFields.ROOM_ID, roomId)
|
||||||
|
.givenEqualTo(EventAnnotationsSummaryEntityFields.EVENT_ID, eventId)
|
||||||
|
.givenFindFirst(annotationsSummary)
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,6 +25,8 @@ import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.amshove.kluent.shouldBeFalse
|
import org.amshove.kluent.shouldBeFalse
|
||||||
import org.amshove.kluent.shouldBeTrue
|
import org.amshove.kluent.shouldBeTrue
|
||||||
|
import org.amshove.kluent.shouldContain
|
||||||
|
import org.amshove.kluent.shouldNotContain
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
@ -105,6 +107,24 @@ class DefaultPollAggregationProcessorTest {
|
||||||
pollAggregationProcessor.handlePollResponseEvent(session, realm.instance, A_POLL_RESPONSE_EVENT).shouldBeTrue()
|
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
|
@Test
|
||||||
fun `given a poll response event after poll is closed, when processing, then is ignored and returns false`() {
|
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 {
|
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity().apply {
|
||||||
|
@ -132,12 +152,33 @@ class DefaultPollAggregationProcessorTest {
|
||||||
// Given
|
// Given
|
||||||
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity()
|
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity()
|
||||||
every { fakeTaskExecutor.instance.executorScope } returns this
|
every { fakeTaskExecutor.instance.executorScope } returns this
|
||||||
|
|
||||||
// When
|
|
||||||
val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, true)
|
val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, true)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT)
|
||||||
|
|
||||||
// Then
|
// 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
|
@Test
|
||||||
|
@ -145,12 +186,13 @@ class DefaultPollAggregationProcessorTest {
|
||||||
// Given
|
// Given
|
||||||
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity()
|
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity()
|
||||||
every { fakeTaskExecutor.instance.executorScope } returns this
|
every { fakeTaskExecutor.instance.executorScope } returns this
|
||||||
|
|
||||||
// When
|
|
||||||
val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, false)
|
val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, false)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT)
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue()
|
result.shouldBeTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -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<Event>()
|
||||||
|
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<Event>()
|
||||||
|
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<PollResponseAggregatedSummaryEntity>()
|
||||||
|
.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<PollResponseAggregatedSummaryEntity>()
|
||||||
|
.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<Event>().also {
|
||||||
|
every { it.eventId } returns eventId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -117,6 +117,14 @@ inline fun <reified T : RealmModel> RealmQuery<T>.givenIn(
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline fun <reified T : RealmModel> RealmQuery<T>.givenContainsValue(
|
||||||
|
fieldName: String,
|
||||||
|
value: String,
|
||||||
|
): RealmQuery<T> {
|
||||||
|
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.
|
* Should be called on a mocked RealmObject and not on a real RealmObject so that the underlying final method is mocked.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,6 +20,7 @@ import android.content.ActivityNotFoundException
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.resources.StringProvider
|
import im.vector.app.core.resources.StringProvider
|
||||||
import im.vector.app.features.call.dialpad.DialPadLookup
|
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.voice.VoiceFailure
|
||||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure
|
import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure
|
||||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure.RecordingError
|
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)
|
stringProvider.getString(R.string.login_signin_matrix_id_error_invalid_matrix_id)
|
||||||
is VoiceFailure -> voiceMessageError(throwable)
|
is VoiceFailure -> voiceMessageError(throwable)
|
||||||
is VoiceBroadcastFailure -> voiceBroadcastMessageError(throwable)
|
is VoiceBroadcastFailure -> voiceBroadcastMessageError(throwable)
|
||||||
|
is RoomPollsLoadingError -> stringProvider.getString(R.string.room_polls_loading_error)
|
||||||
is ActivityNotFoundException ->
|
is ActivityNotFoundException ->
|
||||||
stringProvider.getString(R.string.error_no_external_application_found)
|
stringProvider.getString(R.string.error_no_external_application_found)
|
||||||
else -> throwable.localizedMessage
|
else -> throwable.localizedMessage
|
||||||
|
|
|
@ -138,7 +138,7 @@ class MessageComposerViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleOnTextChanged(action: MessageComposerAction.OnTextChanged) {
|
private fun handleOnTextChanged(action: MessageComposerAction.OnTextChanged) {
|
||||||
val needsSendButtonVisibilityUpdate = currentComposerText.isEmpty() != action.text.isEmpty()
|
val needsSendButtonVisibilityUpdate = currentComposerText.isBlank() != action.text.isBlank()
|
||||||
currentComposerText = SpannableString(action.text)
|
currentComposerText = SpannableString(action.text)
|
||||||
if (needsSendButtonVisibilityUpdate) {
|
if (needsSendButtonVisibilityUpdate) {
|
||||||
updateIsSendButtonVisibility(true)
|
updateIsSendButtonVisibility(true)
|
||||||
|
|
|
@ -83,9 +83,14 @@ class PollItemViewStateFactory @Inject constructor(
|
||||||
totalVotes: Int,
|
totalVotes: Int,
|
||||||
winnerVoteCount: Int?,
|
winnerVoteCount: Int?,
|
||||||
): PollViewState {
|
): 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(
|
return PollViewState(
|
||||||
question = question,
|
question = question,
|
||||||
votesStatus = stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, totalVotes, totalVotes),
|
votesStatus = totalVotesText,
|
||||||
canVote = false,
|
canVote = false,
|
||||||
optionViewStates = pollCreationInfo?.answers?.map { answer ->
|
optionViewStates = pollCreationInfo?.answers?.map { answer ->
|
||||||
val voteSummary = pollResponseSummary?.getVoteSummaryOfAnOption(answer.id ?: "")
|
val voteSummary = pollResponseSummary?.getVoteSummaryOfAnOption(answer.id ?: "")
|
||||||
|
@ -126,9 +131,14 @@ class PollItemViewStateFactory @Inject constructor(
|
||||||
pollResponseSummary: PollResponseData?,
|
pollResponseSummary: PollResponseData?,
|
||||||
totalVotes: Int
|
totalVotes: Int
|
||||||
): PollViewState {
|
): 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(
|
return PollViewState(
|
||||||
question = question,
|
question = question,
|
||||||
votesStatus = stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, totalVotes, totalVotes),
|
votesStatus = totalVotesText,
|
||||||
canVote = true,
|
canVote = true,
|
||||||
optionViewStates = pollCreationInfo?.answers?.map { answer ->
|
optionViewStates = pollCreationInfo?.answers?.map { answer ->
|
||||||
val isMyVote = pollResponseSummary?.myVote == answer.id
|
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) {
|
val totalVotesText = if (totalVotes == 0) {
|
||||||
stringProvider.getString(R.string.poll_no_votes_cast)
|
stringProvider.getString(R.string.poll_no_votes_cast)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -44,7 +44,8 @@ class PollResponseDataFactory @Inject constructor(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
winnerVoteCount = it.aggregatedContent?.winnerVoteCount ?: 0,
|
winnerVoteCount = it.aggregatedContent?.winnerVoteCount ?: 0,
|
||||||
totalVotes = it.aggregatedContent?.totalVotes ?: 0
|
totalVotes = it.aggregatedContent?.totalVotes ?: 0,
|
||||||
|
hasEncryptedRelatedEvents = it.encryptedRelatedEventIds.isNotEmpty(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,7 +90,8 @@ data class PollResponseData(
|
||||||
val votes: Map<String, PollVoteSummaryData>?,
|
val votes: Map<String, PollVoteSummaryData>?,
|
||||||
val totalVotes: Int = 0,
|
val totalVotes: Int = 0,
|
||||||
val winnerVoteCount: Int = 0,
|
val winnerVoteCount: Int = 0,
|
||||||
val isClosed: Boolean = false
|
val isClosed: Boolean = false,
|
||||||
|
val hasEncryptedRelatedEvents: Boolean = false,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
fun getVoteSummaryOfAnOption(optionId: String) = votes?.get(optionId)
|
fun getVoteSummaryOfAnOption(optionId: String) = votes?.get(optionId)
|
||||||
|
|
|
@ -76,6 +76,8 @@ class RoomProfileActivity :
|
||||||
return ActivitySimpleBinding.inflate(layoutInflater)
|
return ActivitySimpleBinding.inflate(layoutInflater)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getCoordinatorLayout() = views.coordinatorLayout
|
||||||
|
|
||||||
override fun initUiAndData() {
|
override fun initUiAndData() {
|
||||||
sharedActionViewModel = viewModelProvider.get(RoomProfileSharedActionViewModel::class.java)
|
sharedActionViewModel = viewModelProvider.get(RoomProfileSharedActionViewModel::class.java)
|
||||||
roomProfileArgs = intent?.extras?.getParcelableCompat(Mavericks.KEY_ARG) ?: return
|
roomProfileArgs = intent?.extras?.getParcelableCompat(Mavericks.KEY_ARG) ?: return
|
||||||
|
|
|
@ -18,4 +18,6 @@ package im.vector.app.features.roomprofile.polls
|
||||||
|
|
||||||
import im.vector.app.core.platform.VectorViewModelAction
|
import im.vector.app.core.platform.VectorViewModelAction
|
||||||
|
|
||||||
sealed interface RoomPollsAction : VectorViewModelAction
|
sealed interface RoomPollsAction : VectorViewModelAction {
|
||||||
|
object LoadMorePolls : RoomPollsAction
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
|
@ -18,4 +18,6 @@ package im.vector.app.features.roomprofile.polls
|
||||||
|
|
||||||
import im.vector.app.core.platform.VectorViewEvents
|
import im.vector.app.core.platform.VectorViewEvents
|
||||||
|
|
||||||
sealed class RoomPollsViewEvent : VectorViewEvents
|
sealed class RoomPollsViewEvent : VectorViewEvents {
|
||||||
|
object LoadingError : RoomPollsViewEvent()
|
||||||
|
}
|
||||||
|
|
|
@ -23,12 +23,20 @@ import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
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.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class RoomPollsViewModel @AssistedInject constructor(
|
class RoomPollsViewModel @AssistedInject constructor(
|
||||||
@Assisted initialState: RoomPollsViewState,
|
@Assisted initialState: RoomPollsViewState,
|
||||||
private val getPollsUseCase: GetPollsUseCase,
|
private val getPollsUseCase: GetPollsUseCase,
|
||||||
|
private val getLoadedPollsStatusUseCase: GetLoadedPollsStatusUseCase,
|
||||||
|
private val loadMorePollsUseCase: LoadMorePollsUseCase,
|
||||||
|
private val syncPollsUseCase: SyncPollsUseCase,
|
||||||
) : VectorViewModel<RoomPollsViewState, RoomPollsAction, RoomPollsViewEvent>(initialState) {
|
) : VectorViewModel<RoomPollsViewState, RoomPollsAction, RoomPollsViewEvent>(initialState) {
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
|
@ -39,16 +47,63 @@ class RoomPollsViewModel @AssistedInject constructor(
|
||||||
companion object : MavericksViewModelFactory<RoomPollsViewModel, RoomPollsViewState> by hiltMavericksViewModelFactory()
|
companion object : MavericksViewModelFactory<RoomPollsViewModel, RoomPollsViewState> by hiltMavericksViewModelFactory()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
observePolls()
|
val roomId = initialState.roomId
|
||||||
|
updateLoadedPollStatus(roomId)
|
||||||
|
syncPolls(roomId)
|
||||||
|
observePolls(roomId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observePolls() {
|
private fun updateLoadedPollStatus(roomId: String) {
|
||||||
getPollsUseCase.execute()
|
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) } }
|
.onEach { setState { copy(polls = it) } }
|
||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handle(action: RoomPollsAction) {
|
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) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,11 +18,19 @@ package im.vector.app.features.roomprofile.polls
|
||||||
|
|
||||||
import com.airbnb.mvrx.MavericksState
|
import com.airbnb.mvrx.MavericksState
|
||||||
import im.vector.app.features.roomprofile.RoomProfileArgs
|
import im.vector.app.features.roomprofile.RoomProfileArgs
|
||||||
|
import im.vector.app.features.roomprofile.polls.list.ui.PollSummary
|
||||||
|
|
||||||
data class RoomPollsViewState(
|
data class RoomPollsViewState(
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val polls: List<PollSummary> = emptyList(),
|
val polls: List<PollSummary> = emptyList(),
|
||||||
|
val isLoadingMore: Boolean = false,
|
||||||
|
val canLoadMore: Boolean = true,
|
||||||
|
val nbLoadedDays: Int = 0,
|
||||||
|
val isSyncing: Boolean = false,
|
||||||
) : MavericksState {
|
) : MavericksState {
|
||||||
|
|
||||||
constructor(roomProfileArgs: RoomProfileArgs) : this(roomId = roomProfileArgs.roomId)
|
constructor(roomProfileArgs: RoomProfileArgs) : this(roomId = roomProfileArgs.roomId)
|
||||||
|
|
||||||
|
fun hasNoPolls() = polls.isEmpty()
|
||||||
|
fun hasNoPollsAndCanLoadMore() = !isSyncing && hasNoPolls() && canLoadMore
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,13 +19,17 @@ package im.vector.app.features.roomprofile.polls.active
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.features.roomprofile.polls.RoomPollsType
|
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
|
@AndroidEntryPoint
|
||||||
class RoomActivePollsFragment : RoomPollsListFragment() {
|
class RoomActivePollsFragment : RoomPollsListFragment() {
|
||||||
|
|
||||||
override fun getEmptyListTitle(): String {
|
override fun getEmptyListTitle(canLoadMore: Boolean, nbLoadedDays: Int): String {
|
||||||
return getString(R.string.room_polls_active_no_item)
|
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 {
|
override fun getRoomPollsType(): RoomPollsType {
|
||||||
|
|
|
@ -19,13 +19,17 @@ package im.vector.app.features.roomprofile.polls.ended
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.features.roomprofile.polls.RoomPollsType
|
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
|
@AndroidEntryPoint
|
||||||
class RoomEndedPollsFragment : RoomPollsListFragment() {
|
class RoomEndedPollsFragment : RoomPollsListFragment() {
|
||||||
|
|
||||||
override fun getEmptyListTitle(): String {
|
override fun getEmptyListTitle(canLoadMore: Boolean, nbLoadedDays: Int): String {
|
||||||
return getString(R.string.room_polls_ended_no_item)
|
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 {
|
override fun getRoomPollsType(): RoomPollsType {
|
||||||
|
|
|
@ -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<FragmentRoomPollsListBinding>(),
|
|
||||||
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<PollSummary>) {
|
|
||||||
roomPollsController.setData(polls)
|
|
||||||
views.roomPollsEmptyTitle.isVisible = polls.isEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPollClicked(pollId: String) {
|
|
||||||
// TODO navigate to details
|
|
||||||
Timber.d("poll with id $pollId clicked")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
||||||
|
)
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -14,23 +14,60 @@
|
||||||
* limitations under the License.
|
* 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.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.Flow
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
class GetPollsUseCase @Inject constructor() {
|
@Singleton
|
||||||
|
class RoomPollDataSource @Inject constructor() {
|
||||||
|
|
||||||
fun execute(): Flow<List<PollSummary>> {
|
private val pollsFlow = MutableSharedFlow<List<PollSummary>>(replay = 1)
|
||||||
// TODO unmock and add unit tests
|
private val polls = mutableListOf<PollSummary>()
|
||||||
return flowOf(getActivePolls() + getEndedPolls())
|
private var fakeLoadCounter = 0
|
||||||
.map { it.sortedByDescending { poll -> poll.creationTimestamp } }
|
|
||||||
|
// 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<List<PollSummary>> {
|
||||||
|
Timber.d("roomId=$roomId")
|
||||||
|
return pollsFlow.asSharedFlow()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getActivePolls(): List<PollSummary.ActivePoll> {
|
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<PollSummary.ActivePoll> {
|
||||||
return listOf(
|
return listOf(
|
||||||
PollSummary.ActivePoll(
|
PollSummary.ActivePoll(
|
||||||
id = "id1",
|
id = "id1",
|
||||||
|
@ -44,6 +81,11 @@ class GetPollsUseCase @Inject constructor() {
|
||||||
creationTimestamp = 1656194400000,
|
creationTimestamp = 1656194400000,
|
||||||
title = "Which sport should the pupils do this year?"
|
title = "Which sport should the pupils do this year?"
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getActivePollsPart2(): List<PollSummary.ActivePoll> {
|
||||||
|
return listOf(
|
||||||
PollSummary.ActivePoll(
|
PollSummary.ActivePoll(
|
||||||
id = "id3",
|
id = "id3",
|
||||||
// 2022/06/24 UTC+1
|
// 2022/06/24 UTC+1
|
||||||
|
@ -59,7 +101,7 @@ class GetPollsUseCase @Inject constructor() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getEndedPolls(): List<PollSummary.EndedPoll> {
|
private fun getEndedPollsPart1(): List<PollSummary.EndedPoll> {
|
||||||
return listOf(
|
return listOf(
|
||||||
PollSummary.EndedPoll(
|
PollSummary.EndedPoll(
|
||||||
id = "id1-ended",
|
id = "id1-ended",
|
||||||
|
@ -77,6 +119,11 @@ class GetPollsUseCase @Inject constructor() {
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getEndedPollsPart2(): List<PollSummary.EndedPoll> {
|
||||||
|
return listOf(
|
||||||
PollSummary.EndedPoll(
|
PollSummary.EndedPoll(
|
||||||
id = "id2-ended",
|
id = "id2-ended",
|
||||||
// 2022/06/26 UTC+1
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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<List<PollSummary>> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<List<PollSummary>> {
|
||||||
|
return roomPollRepository.getPolls(roomId)
|
||||||
|
.map { it.sortedByDescending { poll -> poll.creationTimestamp } }
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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
|
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState
|
||||||
|
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
|
@ -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<RoomPollLoadMoreItem.Holder>(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<Button>(R.id.roomPollsLoadMore)
|
||||||
|
val loadMoreProgressBar by bind<ProgressBar>(R.id.roomPollsLoadMoreProgress)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -14,38 +14,45 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.app.features.roomprofile.polls.list
|
package im.vector.app.features.roomprofile.polls.list.ui
|
||||||
|
|
||||||
import com.airbnb.epoxy.TypedEpoxyController
|
import com.airbnb.epoxy.TypedEpoxyController
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.date.DateFormatKind
|
import im.vector.app.core.date.DateFormatKind
|
||||||
import im.vector.app.core.date.VectorDateFormatter
|
import im.vector.app.core.date.VectorDateFormatter
|
||||||
import im.vector.app.core.resources.StringProvider
|
import im.vector.app.core.resources.StringProvider
|
||||||
import im.vector.app.features.roomprofile.polls.PollSummary
|
import im.vector.app.features.roomprofile.polls.RoomPollsViewState
|
||||||
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class RoomPollsController @Inject constructor(
|
class RoomPollsController @Inject constructor(
|
||||||
val dateFormatter: VectorDateFormatter,
|
val dateFormatter: VectorDateFormatter,
|
||||||
val stringProvider: StringProvider,
|
val stringProvider: StringProvider,
|
||||||
) : TypedEpoxyController<List<PollSummary>>() {
|
) : TypedEpoxyController<RoomPollsViewState>() {
|
||||||
|
|
||||||
interface Listener {
|
interface Listener {
|
||||||
fun onPollClicked(pollId: String)
|
fun onPollClicked(pollId: String)
|
||||||
|
fun onLoadMoreClicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
var listener: Listener? = null
|
var listener: Listener? = null
|
||||||
|
|
||||||
override fun buildModels(data: List<PollSummary>?) {
|
override fun buildModels(viewState: RoomPollsViewState?) {
|
||||||
if (data.isNullOrEmpty()) {
|
val polls = viewState?.polls
|
||||||
|
if (polls.isNullOrEmpty() || viewState.isSyncing) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for (poll in data) {
|
for (poll in polls) {
|
||||||
when (poll) {
|
when (poll) {
|
||||||
is PollSummary.ActivePoll -> buildActivePollItem(poll)
|
is PollSummary.ActivePoll -> buildActivePollItem(poll)
|
||||||
is PollSummary.EndedPoll -> buildEndedPollItem(poll)
|
is PollSummary.EndedPoll -> buildEndedPollItem(poll)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (viewState.canLoadMore) {
|
||||||
|
buildLoadMoreItem(viewState.isLoadingMore)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildActivePollItem(poll: PollSummary.ActivePoll) {
|
private fun buildActivePollItem(poll: PollSummary.ActivePoll) {
|
||||||
|
@ -73,4 +80,15 @@ class RoomPollsController @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildLoadMoreItem(isLoadingMore: Boolean) {
|
||||||
|
val host = this
|
||||||
|
roomPollLoadMoreItem {
|
||||||
|
id(UUID.randomUUID().toString())
|
||||||
|
loadingMore(isLoadingMore)
|
||||||
|
clickListener {
|
||||||
|
host.listener?.onLoadMoreClicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,136 @@
|
||||||
|
/*
|
||||||
|
* 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.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.epoxy.onClick
|
||||||
|
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.core.resources.StringProvider
|
||||||
|
import im.vector.app.databinding.FragmentRoomPollsListBinding
|
||||||
|
import im.vector.app.features.roomprofile.polls.RoomPollsAction
|
||||||
|
import im.vector.app.features.roomprofile.polls.RoomPollsLoadingError
|
||||||
|
import im.vector.app.features.roomprofile.polls.RoomPollsType
|
||||||
|
import im.vector.app.features.roomprofile.polls.RoomPollsViewEvent
|
||||||
|
import im.vector.app.features.roomprofile.polls.RoomPollsViewModel
|
||||||
|
import im.vector.app.features.roomprofile.polls.RoomPollsViewState
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
abstract class RoomPollsListFragment :
|
||||||
|
VectorBaseFragment<FragmentRoomPollsListBinding>(),
|
||||||
|
RoomPollsController.Listener {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var roomPollsController: RoomPollsController
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var stringProvider: StringProvider
|
||||||
|
|
||||||
|
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)
|
||||||
|
observeViewEvents()
|
||||||
|
setupList()
|
||||||
|
setupLoadMoreButton()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeViewEvents() {
|
||||||
|
viewModel.observeViewEvents { viewEvent ->
|
||||||
|
when (viewEvent) {
|
||||||
|
RoomPollsViewEvent.LoadingError -> showErrorInSnackbar(RoomPollsLoadingError())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun getEmptyListTitle(canLoadMore: Boolean, nbLoadedDays: Int): String
|
||||||
|
|
||||||
|
abstract fun getRoomPollsType(): RoomPollsType
|
||||||
|
|
||||||
|
private fun setupList() = withState(viewModel) { viewState ->
|
||||||
|
roomPollsController.listener = this
|
||||||
|
views.roomPollsList.configureWith(roomPollsController)
|
||||||
|
views.roomPollsEmptyTitle.text = getEmptyListTitle(
|
||||||
|
canLoadMore = viewState.canLoadMore,
|
||||||
|
nbLoadedDays = viewState.nbLoadedDays,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupLoadMoreButton() {
|
||||||
|
views.roomPollsLoadMoreWhenEmpty.onClick {
|
||||||
|
onLoadMoreClicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
cleanUpList()
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cleanUpList() {
|
||||||
|
views.roomPollsList.cleanup()
|
||||||
|
roomPollsController.listener = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invalidate() = withState(viewModel) { viewState ->
|
||||||
|
val filteredPolls = when (getRoomPollsType()) {
|
||||||
|
RoomPollsType.ACTIVE -> viewState.polls.filterIsInstance(PollSummary.ActivePoll::class.java)
|
||||||
|
RoomPollsType.ENDED -> viewState.polls.filterIsInstance(PollSummary.EndedPoll::class.java)
|
||||||
|
}
|
||||||
|
val updatedViewState = viewState.copy(polls = filteredPolls)
|
||||||
|
renderList(updatedViewState)
|
||||||
|
renderSyncingView(updatedViewState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderSyncingView(viewState: RoomPollsViewState) {
|
||||||
|
views.roomPollsSyncingTitle.isVisible = viewState.isSyncing
|
||||||
|
views.roomPollsSyncingProgress.isVisible = viewState.isSyncing
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderList(viewState: RoomPollsViewState) {
|
||||||
|
roomPollsController.setData(viewState)
|
||||||
|
views.roomPollsEmptyTitle.text = getEmptyListTitle(
|
||||||
|
canLoadMore = viewState.canLoadMore,
|
||||||
|
nbLoadedDays = viewState.nbLoadedDays,
|
||||||
|
)
|
||||||
|
views.roomPollsEmptyTitle.isVisible = !viewState.isSyncing && viewState.hasNoPolls()
|
||||||
|
views.roomPollsLoadMoreWhenEmpty.isVisible = viewState.hasNoPollsAndCanLoadMore()
|
||||||
|
views.roomPollsLoadMoreWhenEmpty.isEnabled = !viewState.isLoadingMore
|
||||||
|
views.roomPollsLoadMoreWhenEmptyProgress.isVisible = viewState.hasNoPollsAndCanLoadMore() && viewState.isLoadingMore
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPollClicked(pollId: String) {
|
||||||
|
// TODO navigate to details
|
||||||
|
Timber.d("poll with id $pollId clicked")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLoadMoreClicked() {
|
||||||
|
viewModel.handle(RoomPollsAction.LoadMorePolls)
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,34 @@
|
||||||
tools:itemCount="5"
|
tools:itemCount="5"
|
||||||
tools:listitem="@layout/item_poll" />
|
tools:listitem="@layout/item_poll" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/roomPollsSyncingProgress"
|
||||||
|
style="?android:attr/progressBarStyle"
|
||||||
|
android:layout_width="16dp"
|
||||||
|
android:layout_height="16dp"
|
||||||
|
android:indeterminateTint="?vctr_content_secondary"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/roomPollsSyncingTitle"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/roomPollsSyncingTitle"
|
||||||
|
app:layout_constraintHorizontal_chainStyle="packed"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/roomPollsSyncingTitle" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/roomPollsSyncingTitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="9dp"
|
||||||
|
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/room_polls_wait_for_display"
|
||||||
|
android:textAppearance="@style/TextAppearance.Vector.Body"
|
||||||
|
android:textColor="?vctr_content_secondary"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/roomPollsSyncingProgress"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/roomPollsTitleGuideline" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/roomPollsEmptyTitle"
|
android:id="@+id/roomPollsEmptyTitle"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
@ -26,14 +54,39 @@
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:textAppearance="@style/TextAppearance.Vector.Body"
|
android:textAppearance="@style/TextAppearance.Vector.Body"
|
||||||
android:textColor="?vctr_content_secondary"
|
android:textColor="?vctr_content_secondary"
|
||||||
|
android:textSize="17sp"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="@id/roomPollsEmptyGuideline"
|
app:layout_constraintTop_toTopOf="@id/roomPollsTitleGuideline"
|
||||||
tools:text="@string/room_polls_active_no_item" />
|
tools:text="@string/room_polls_active_no_item" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/roomPollsLoadMoreWhenEmpty"
|
||||||
|
style="@style/Widget.Vector.Button.Text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="@dimen/layout_horizontal_margin"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/room_polls_load_more"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/roomPollsEmptyTitle" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/roomPollsLoadMoreWhenEmptyProgress"
|
||||||
|
style="?android:attr/progressBarStyle"
|
||||||
|
android:layout_width="16dp"
|
||||||
|
android:layout_height="16dp"
|
||||||
|
android:layout_marginStart="9dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/roomPollsLoadMoreWhenEmpty"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/roomPollsLoadMoreWhenEmpty"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/roomPollsLoadMoreWhenEmpty" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Guideline
|
<androidx.constraintlayout.widget.Guideline
|
||||||
android:id="@+id/roomPollsEmptyGuideline"
|
android:id="@+id/roomPollsTitleGuideline"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
|
|
|
@ -85,6 +85,7 @@
|
||||||
app:layout_constraintCircle="@id/breadcrumbsImageView"
|
app:layout_constraintCircle="@id/breadcrumbsImageView"
|
||||||
app:layout_constraintCircleAngle="225"
|
app:layout_constraintCircleAngle="225"
|
||||||
app:layout_constraintCircleRadius="28dp"
|
app:layout_constraintCircleRadius="28dp"
|
||||||
|
app:tint="?vctr_content_primary"
|
||||||
tools:ignore="MissingConstraints"
|
tools:ignore="MissingConstraints"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/roomPollsLoadMore"
|
||||||
|
style="@style/Widget.Vector.Button.Text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="38dp"
|
||||||
|
android:layout_marginBottom="46dp"
|
||||||
|
android:padding="0dp"
|
||||||
|
android:text="@string/room_polls_load_more"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/roomPollsLoadMoreProgress"
|
||||||
|
style="?android:attr/progressBarStyle"
|
||||||
|
android:layout_width="16dp"
|
||||||
|
android:layout_height="16dp"
|
||||||
|
android:layout_marginStart="9dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/roomPollsLoadMore"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/roomPollsLoadMore"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/roomPollsLoadMore" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -131,6 +131,24 @@ class PollItemViewStateFactoryTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a sent poll state with some decryption error when poll is closed then warning message is displayed`() {
|
||||||
|
// Given
|
||||||
|
val stringProvider = FakeStringProvider()
|
||||||
|
val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance)
|
||||||
|
val closedPollSummary = A_POLL_RESPONSE_DATA.copy(isClosed = true, hasEncryptedRelatedEvents = true)
|
||||||
|
val closedPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(pollResponseAggregatedSummary = closedPollSummary)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val pollViewState = pollItemViewStateFactory.create(
|
||||||
|
pollContent = A_POLL_CONTENT,
|
||||||
|
informationData = closedPollInformationData,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
pollViewState.votesStatus shouldBeEqualTo stringProvider.instance.getString(R.string.unable_to_decrypt_some_events_in_poll)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given a sent poll when undisclosed poll type is selected then poll is votable and option states are PollUndisclosed`() {
|
fun `given a sent poll when undisclosed poll type is selected then poll is votable and option states are PollUndisclosed`() {
|
||||||
val stringProvider = FakeStringProvider()
|
val stringProvider = FakeStringProvider()
|
||||||
|
@ -193,6 +211,34 @@ class PollItemViewStateFactoryTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a sent poll with decryption failure when my vote exists then a warning message is displayed`() {
|
||||||
|
// Given
|
||||||
|
val stringProvider = FakeStringProvider()
|
||||||
|
val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance)
|
||||||
|
val votedPollData = A_POLL_RESPONSE_DATA.copy(
|
||||||
|
totalVotes = 1,
|
||||||
|
myVote = A_POLL_OPTION_IDS[0],
|
||||||
|
votes = mapOf(A_POLL_OPTION_IDS[0] to PollVoteSummaryData(total = 1, percentage = 1.0)),
|
||||||
|
hasEncryptedRelatedEvents = true,
|
||||||
|
)
|
||||||
|
val disclosedPollContent = A_POLL_CONTENT.copy(
|
||||||
|
unstablePollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo()?.copy(
|
||||||
|
kind = PollType.DISCLOSED_UNSTABLE
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val votedInformationData = A_MESSAGE_INFORMATION_DATA.copy(pollResponseAggregatedSummary = votedPollData)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val pollViewState = pollItemViewStateFactory.create(
|
||||||
|
pollContent = disclosedPollContent,
|
||||||
|
informationData = votedInformationData,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
pollViewState.votesStatus shouldBeEqualTo stringProvider.instance.getString(R.string.unable_to_decrypt_some_events_in_poll)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given a sent poll when poll type is disclosed then poll is votable and option view states are PollReady`() {
|
fun `given a sent poll when poll type is disclosed then poll is votable and option view states are PollReady`() {
|
||||||
val stringProvider = FakeStringProvider()
|
val stringProvider = FakeStringProvider()
|
||||||
|
|
|
@ -17,8 +17,17 @@
|
||||||
package im.vector.app.features.roomprofile.polls
|
package im.vector.app.features.roomprofile.polls
|
||||||
|
|
||||||
import com.airbnb.mvrx.test.MavericksTestRule
|
import com.airbnb.mvrx.test.MavericksTestRule
|
||||||
|
import im.vector.app.features.roomprofile.polls.list.data.LoadedPollsStatus
|
||||||
|
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 im.vector.app.features.roomprofile.polls.list.ui.PollSummary
|
||||||
import im.vector.app.test.test
|
import im.vector.app.test.test
|
||||||
import im.vector.app.test.testDispatcher
|
import im.vector.app.test.testDispatcher
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.coJustRun
|
||||||
|
import io.mockk.coVerify
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
|
@ -26,7 +35,7 @@ import kotlinx.coroutines.flow.flowOf
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
private const val ROOM_ID = "room-id"
|
private const val A_ROOM_ID = "room-id"
|
||||||
|
|
||||||
class RoomPollsViewModelTest {
|
class RoomPollsViewModelTest {
|
||||||
|
|
||||||
|
@ -34,21 +43,33 @@ class RoomPollsViewModelTest {
|
||||||
val mavericksTestRule = MavericksTestRule(testDispatcher = testDispatcher)
|
val mavericksTestRule = MavericksTestRule(testDispatcher = testDispatcher)
|
||||||
|
|
||||||
private val fakeGetPollsUseCase = mockk<GetPollsUseCase>()
|
private val fakeGetPollsUseCase = mockk<GetPollsUseCase>()
|
||||||
private val initialState = RoomPollsViewState(ROOM_ID)
|
private val fakeGetLoadedPollsStatusUseCase = mockk<GetLoadedPollsStatusUseCase>()
|
||||||
|
private val fakeLoadMorePollsUseCase = mockk<LoadMorePollsUseCase>()
|
||||||
|
private val fakeSyncPollsUseCase = mockk<SyncPollsUseCase>()
|
||||||
|
private val initialState = RoomPollsViewState(A_ROOM_ID)
|
||||||
|
|
||||||
private fun createViewModel(): RoomPollsViewModel {
|
private fun createViewModel(): RoomPollsViewModel {
|
||||||
return RoomPollsViewModel(
|
return RoomPollsViewModel(
|
||||||
initialState = initialState,
|
initialState = initialState,
|
||||||
getPollsUseCase = fakeGetPollsUseCase,
|
getPollsUseCase = fakeGetPollsUseCase,
|
||||||
|
getLoadedPollsStatusUseCase = fakeGetLoadedPollsStatusUseCase,
|
||||||
|
loadMorePollsUseCase = fakeLoadMorePollsUseCase,
|
||||||
|
syncPollsUseCase = fakeSyncPollsUseCase,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given viewModel when created then polls list is observed and viewState is updated`() {
|
fun `given viewModel when created then polls list is observed, sync is launched and viewState is updated`() {
|
||||||
// Given
|
// Given
|
||||||
|
val loadedPollsStatus = givenGetLoadedPollsStatusSuccess()
|
||||||
|
givenSyncPollsWithSuccess()
|
||||||
val polls = listOf(givenAPollSummary())
|
val polls = listOf(givenAPollSummary())
|
||||||
every { fakeGetPollsUseCase.execute() } returns flowOf(polls)
|
every { fakeGetPollsUseCase.execute(A_ROOM_ID) } returns flowOf(polls)
|
||||||
val expectedViewState = initialState.copy(polls = polls)
|
val expectedViewState = initialState.copy(
|
||||||
|
polls = polls,
|
||||||
|
canLoadMore = loadedPollsStatus.canLoadMore,
|
||||||
|
nbLoadedDays = loadedPollsStatus.nbLoadedDays,
|
||||||
|
)
|
||||||
|
|
||||||
// When
|
// When
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
|
@ -59,11 +80,88 @@ class RoomPollsViewModelTest {
|
||||||
.assertLatestState(expectedViewState)
|
.assertLatestState(expectedViewState)
|
||||||
.finish()
|
.finish()
|
||||||
verify {
|
verify {
|
||||||
fakeGetPollsUseCase.execute()
|
fakeGetPollsUseCase.execute(A_ROOM_ID)
|
||||||
}
|
}
|
||||||
|
coVerify { fakeSyncPollsUseCase.execute(A_ROOM_ID) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given viewModel and error during sync process when created then error is raised in view event`() {
|
||||||
|
// Given
|
||||||
|
givenGetLoadedPollsStatusSuccess()
|
||||||
|
givenSyncPollsWithError(Exception())
|
||||||
|
val polls = listOf(givenAPollSummary())
|
||||||
|
every { fakeGetPollsUseCase.execute(A_ROOM_ID) } returns flowOf(polls)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
val viewModelTest = viewModel.test()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
viewModelTest
|
||||||
|
.assertEvents(RoomPollsViewEvent.LoadingError)
|
||||||
|
.finish()
|
||||||
|
coVerify { fakeSyncPollsUseCase.execute(A_ROOM_ID) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given viewModel when handle load more action then viewState is updated`() {
|
||||||
|
// Given
|
||||||
|
val loadedPollsStatus = givenGetLoadedPollsStatusSuccess()
|
||||||
|
givenSyncPollsWithSuccess()
|
||||||
|
val polls = listOf(givenAPollSummary())
|
||||||
|
every { fakeGetPollsUseCase.execute(A_ROOM_ID) } returns flowOf(polls)
|
||||||
|
val newLoadedPollsStatus = givenLoadMoreWithSuccess()
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
val stateAfterInit = initialState.copy(
|
||||||
|
polls = polls,
|
||||||
|
canLoadMore = loadedPollsStatus.canLoadMore,
|
||||||
|
nbLoadedDays = loadedPollsStatus.nbLoadedDays,
|
||||||
|
)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val viewModelTest = viewModel.test()
|
||||||
|
viewModel.handle(RoomPollsAction.LoadMorePolls)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
viewModelTest
|
||||||
|
.assertStatesChanges(
|
||||||
|
stateAfterInit,
|
||||||
|
{ copy(isLoadingMore = true) },
|
||||||
|
{ copy(canLoadMore = newLoadedPollsStatus.canLoadMore, nbLoadedDays = newLoadedPollsStatus.nbLoadedDays) },
|
||||||
|
{ copy(isLoadingMore = false) },
|
||||||
|
)
|
||||||
|
.finish()
|
||||||
|
coVerify { fakeLoadMorePollsUseCase.execute(A_ROOM_ID) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun givenAPollSummary(): PollSummary {
|
private fun givenAPollSummary(): PollSummary {
|
||||||
return mockk()
|
return mockk()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun givenSyncPollsWithSuccess() {
|
||||||
|
coJustRun { fakeSyncPollsUseCase.execute(A_ROOM_ID) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun givenSyncPollsWithError(error: Exception) {
|
||||||
|
coEvery { fakeSyncPollsUseCase.execute(A_ROOM_ID) } throws error
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun givenLoadMoreWithSuccess(): LoadedPollsStatus {
|
||||||
|
val loadedPollsStatus = givenALoadedPollsStatus(canLoadMore = false, nbLoadedDays = 20)
|
||||||
|
coEvery { fakeLoadMorePollsUseCase.execute(A_ROOM_ID) } returns loadedPollsStatus
|
||||||
|
return loadedPollsStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun givenGetLoadedPollsStatusSuccess(): LoadedPollsStatus {
|
||||||
|
val loadedPollsStatus = givenALoadedPollsStatus()
|
||||||
|
every { fakeGetLoadedPollsStatusUseCase.execute(A_ROOM_ID) } returns loadedPollsStatus
|
||||||
|
return loadedPollsStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun givenALoadedPollsStatus(canLoadMore: Boolean = true, nbLoadedDays: Int = 10) =
|
||||||
|
LoadedPollsStatus(
|
||||||
|
canLoadMore = canLoadMore,
|
||||||
|
nbLoadedDays = nbLoadedDays,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
/*
|
||||||
|
* 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 io.mockk.coJustRun
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
private const val A_ROOM_ID = "room-id"
|
||||||
|
|
||||||
|
class RoomPollRepositoryTest {
|
||||||
|
|
||||||
|
private val fakeRoomPollDataSource = mockk<RoomPollDataSource>()
|
||||||
|
|
||||||
|
private val roomPollRepository = RoomPollRepository(
|
||||||
|
roomPollDataSource = fakeRoomPollDataSource,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given data source when getting polls then correct method of data source is called`() = runTest {
|
||||||
|
// Given
|
||||||
|
val expectedPolls = listOf<PollSummary>()
|
||||||
|
every { fakeRoomPollDataSource.getPolls(A_ROOM_ID) } returns flowOf(expectedPolls)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = roomPollRepository.getPolls(A_ROOM_ID).firstOrNull()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
result shouldBeEqualTo expectedPolls
|
||||||
|
verify { fakeRoomPollDataSource.getPolls(A_ROOM_ID) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given data source when getting loaded polls status then correct method of data source is called`() {
|
||||||
|
// Given
|
||||||
|
val expectedStatus = LoadedPollsStatus(
|
||||||
|
canLoadMore = true,
|
||||||
|
nbLoadedDays = 10,
|
||||||
|
)
|
||||||
|
every { fakeRoomPollDataSource.getLoadedPollsStatus(A_ROOM_ID) } returns expectedStatus
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = roomPollRepository.getLoadedPollsStatus(A_ROOM_ID)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
result shouldBeEqualTo expectedStatus
|
||||||
|
verify { fakeRoomPollDataSource.getLoadedPollsStatus(A_ROOM_ID) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given data source when loading more polls then correct method of data source is called`() = runTest {
|
||||||
|
// Given
|
||||||
|
coJustRun { fakeRoomPollDataSource.loadMorePolls(A_ROOM_ID) }
|
||||||
|
|
||||||
|
// When
|
||||||
|
roomPollRepository.loadMorePolls(A_ROOM_ID)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
coVerify { fakeRoomPollDataSource.loadMorePolls(A_ROOM_ID) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given data source when syncing polls then correct method of data source is called`() = runTest {
|
||||||
|
// Given
|
||||||
|
coJustRun { fakeRoomPollDataSource.syncPolls(A_ROOM_ID) }
|
||||||
|
|
||||||
|
// When
|
||||||
|
roomPollRepository.syncPolls(A_ROOM_ID)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
coVerify { fakeRoomPollDataSource.syncPolls(A_ROOM_ID) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* 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 io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class GetLoadedPollsStatusUseCaseTest {
|
||||||
|
|
||||||
|
private val fakeRoomPollRepository = mockk<RoomPollRepository>()
|
||||||
|
|
||||||
|
private val getLoadedPollsStatusUseCase = GetLoadedPollsStatusUseCase(
|
||||||
|
roomPollRepository = fakeRoomPollRepository,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given repo when execute then correct method of repo is called`() {
|
||||||
|
// Given
|
||||||
|
val aRoomId = "roomId"
|
||||||
|
val expectedStatus = LoadedPollsStatus(
|
||||||
|
canLoadMore = true,
|
||||||
|
nbLoadedDays = 10,
|
||||||
|
)
|
||||||
|
every { fakeRoomPollRepository.getLoadedPollsStatus(aRoomId) } returns expectedStatus
|
||||||
|
|
||||||
|
// When
|
||||||
|
val status = getLoadedPollsStatusUseCase.execute(aRoomId)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
status shouldBeEqualTo expectedStatus
|
||||||
|
verify { fakeRoomPollRepository.getLoadedPollsStatus(aRoomId) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
* 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 im.vector.app.test.fixtures.RoomPollFixture
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class GetPollsUseCaseTest {
|
||||||
|
private val fakeRoomPollRepository = mockk<RoomPollRepository>()
|
||||||
|
|
||||||
|
private val getPollsUseCase = GetPollsUseCase(
|
||||||
|
roomPollRepository = fakeRoomPollRepository,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given repo when execute then correct method of repo is called and polls are sorted most recent first`() = runTest {
|
||||||
|
// Given
|
||||||
|
val aRoomId = "roomId"
|
||||||
|
val poll1 = RoomPollFixture.anActivePollSummary(timestamp = 1)
|
||||||
|
val poll2 = RoomPollFixture.anActivePollSummary(timestamp = 2)
|
||||||
|
val poll3 = RoomPollFixture.anActivePollSummary(timestamp = 3)
|
||||||
|
val polls = listOf<PollSummary>(
|
||||||
|
poll1,
|
||||||
|
poll2,
|
||||||
|
poll3,
|
||||||
|
)
|
||||||
|
every { fakeRoomPollRepository.getPolls(aRoomId) } returns flowOf(polls)
|
||||||
|
val expectedPolls = listOf<PollSummary>(
|
||||||
|
poll3,
|
||||||
|
poll2,
|
||||||
|
poll1,
|
||||||
|
)
|
||||||
|
// When
|
||||||
|
val result = getPollsUseCase.execute(aRoomId).first()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
result shouldBeEqualTo expectedPolls
|
||||||
|
verify { fakeRoomPollRepository.getPolls(aRoomId) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* 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 io.mockk.coJustRun
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class LoadMorePollsUseCaseTest {
|
||||||
|
|
||||||
|
private val fakeRoomPollRepository = mockk<RoomPollRepository>()
|
||||||
|
|
||||||
|
private val loadMorePollsUseCase = LoadMorePollsUseCase(
|
||||||
|
roomPollRepository = fakeRoomPollRepository,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given repo when execute then correct method of repo is called`() = runTest {
|
||||||
|
// Given
|
||||||
|
val aRoomId = "roomId"
|
||||||
|
coJustRun { fakeRoomPollRepository.loadMorePolls(aRoomId) }
|
||||||
|
|
||||||
|
// When
|
||||||
|
loadMorePollsUseCase.execute(aRoomId)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
coVerify { fakeRoomPollRepository.loadMorePolls(aRoomId) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* 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 io.mockk.coJustRun
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class SyncPollsUseCaseTest {
|
||||||
|
|
||||||
|
private val fakeRoomPollRepository = mockk<RoomPollRepository>()
|
||||||
|
|
||||||
|
private val syncPollsUseCase = SyncPollsUseCase(
|
||||||
|
roomPollRepository = fakeRoomPollRepository,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given repo when execute then correct method of repo is called`() = runTest {
|
||||||
|
// Given
|
||||||
|
val aRoomId = "roomId"
|
||||||
|
coJustRun { fakeRoomPollRepository.syncPolls(aRoomId) }
|
||||||
|
|
||||||
|
// When
|
||||||
|
syncPollsUseCase.execute(aRoomId)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
coVerify { fakeRoomPollRepository.syncPolls(aRoomId) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* 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.test.fixtures
|
||||||
|
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState
|
||||||
|
import im.vector.app.features.roomprofile.polls.list.ui.PollSummary
|
||||||
|
|
||||||
|
object RoomPollFixture {
|
||||||
|
|
||||||
|
fun anActivePollSummary(
|
||||||
|
id: String = "",
|
||||||
|
timestamp: Long,
|
||||||
|
title: String = "",
|
||||||
|
) = PollSummary.ActivePoll(
|
||||||
|
id = id,
|
||||||
|
creationTimestamp = timestamp,
|
||||||
|
title = title,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun anEndedPollSummary(
|
||||||
|
id: String = "",
|
||||||
|
timestamp: Long,
|
||||||
|
title: String = "",
|
||||||
|
totalVotes: Int,
|
||||||
|
winnerOptions: List<PollOptionViewState.PollEnded>
|
||||||
|
) = PollSummary.EndedPoll(
|
||||||
|
id = id,
|
||||||
|
creationTimestamp = timestamp,
|
||||||
|
title = title,
|
||||||
|
totalVotes = totalVotes,
|
||||||
|
winnerOptions = winnerOptions,
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in New Issue