mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-02-16 12:00:03 +03:00
Merge branch 'develop' into hughns/msc3824-oidc-aware
This commit is contained in:
commit
3333d86776
103 changed files with 2693 additions and 374 deletions
5
SECURITY.md
Normal file
5
SECURITY.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Reporting a Vulnerability
|
||||
|
||||
**If you've found a security vulnerability, please report it to security@matrix.org**
|
||||
|
||||
For more information on our security disclosure policy, visit https://www.matrix.org/security-disclosure-policy/
|
|
@ -45,7 +45,7 @@ plugins {
|
|||
// Detekt
|
||||
id "io.gitlab.arturbosch.detekt" version "1.22.0"
|
||||
// Ksp
|
||||
id "com.google.devtools.ksp" version "1.7.22-1.0.8"
|
||||
id "com.google.devtools.ksp" version "1.8.0-1.0.8"
|
||||
|
||||
// Dependency Analysis
|
||||
id 'com.autonomousapps.dependency-analysis' version "1.18.0"
|
||||
|
|
1
changelog.d/4025.bugfix
Normal file
1
changelog.d/4025.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Fix can't get out of a verification dialog
|
1
changelog.d/7824.feature
Normal file
1
changelog.d/7824.feature
Normal file
|
@ -0,0 +1 @@
|
|||
[Poll] Warning message on decryption failure of some events
|
1
changelog.d/7829.bugfix
Normal file
1
changelog.d/7829.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Handle exceptions when listening a voice broadcast
|
1
changelog.d/7832.bugfix
Normal file
1
changelog.d/7832.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
[Voice Broadcast] Fix unexpected "live broadcast" in the room list
|
1
changelog.d/7845.wip
Normal file
1
changelog.d/7845.wip
Normal file
|
@ -0,0 +1 @@
|
|||
[Voice Broadcast] Only display a notification on the first voice chunk
|
1
changelog.d/7864.wip
Normal file
1
changelog.d/7864.wip
Normal file
|
@ -0,0 +1 @@
|
|||
[Poll] History list: Load more UI mechanism
|
1
changelog.d/7936.misc
Normal file
1
changelog.d/7936.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Upgrade to Kotlin 1.8
|
1
changelog.d/7938.bugfix
Normal file
1
changelog.d/7938.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Fix rendering of edited polls
|
|
@ -80,12 +80,12 @@ task generateCoverageReport(type: JacocoReport) {
|
|||
|
||||
task unitTestsWithCoverage(type: GradleBuild) {
|
||||
// the 7.1.3 android gradle plugin has a bug where enableTestCoverage generates invalid coverage
|
||||
startParameter.projectProperties.coverage = [enableTestCoverage: false]
|
||||
startParameter.projectProperties.coverage = "false"
|
||||
tasks = ['testDebugUnitTest']
|
||||
}
|
||||
|
||||
task instrumentationTestsWithCoverage(type: GradleBuild) {
|
||||
startParameter.projectProperties.coverage = [enableTestCoverage: true]
|
||||
startParameter.projectProperties.coverage = "true"
|
||||
startParameter.projectProperties['android.testInstrumentationRunnerArguments.notPackage'] = 'im.vector.app.ui'
|
||||
tasks = [':vector-app:connectedGplayDebugAndroidTest', ':vector:connectedDebugAndroidTest', 'matrix-sdk-android:connectedDebugAndroidTest']
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ ext.versions = [
|
|||
|
||||
def gradle = "7.3.1"
|
||||
// Ref: https://kotlinlang.org/releases.html
|
||||
def kotlin = "1.7.22"
|
||||
def kotlin = "1.8.0"
|
||||
def kotlinCoroutines = "1.6.4"
|
||||
def dagger = "2.44.2"
|
||||
def firebaseBom = "31.1.1"
|
||||
|
@ -18,7 +18,7 @@ def markwon = "4.6.2"
|
|||
def moshi = "1.14.0"
|
||||
def lifecycle = "2.5.1"
|
||||
def flowBinding = "1.2.0"
|
||||
def flipper = "0.176.1"
|
||||
def flipper = "0.177.0"
|
||||
def epoxy = "5.0.0"
|
||||
def mavericks = "3.0.1"
|
||||
def glide = "4.14.2"
|
||||
|
@ -28,11 +28,12 @@ def jjwt = "0.11.5"
|
|||
// the whole commit which set version 0.16.0-SNAPSHOT
|
||||
def vanniktechEmoji = "0.16.0-SNAPSHOT"
|
||||
def sentry = "6.11.0"
|
||||
def fragment = "1.5.5"
|
||||
// Use 1.6.0 alpha to fix issue with test
|
||||
def fragment = "1.6.0-alpha04"
|
||||
// Testing
|
||||
def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819
|
||||
def espresso = "3.4.0"
|
||||
def androidxTest = "1.4.0"
|
||||
def espresso = "3.5.1"
|
||||
def androidxTest = "1.5.0"
|
||||
def androidxOrchestrator = "1.4.2"
|
||||
def paparazzi = "1.1.0"
|
||||
|
||||
|
@ -49,13 +50,14 @@ ext.libs = [
|
|||
],
|
||||
androidx : [
|
||||
'activity' : "androidx.activity:activity-ktx:1.6.1",
|
||||
'appCompat' : "androidx.appcompat:appcompat:1.5.1",
|
||||
'appCompat' : "androidx.appcompat:appcompat:1.6.0",
|
||||
'biometric' : "androidx.biometric:biometric:1.1.0",
|
||||
'core' : "androidx.core:core-ktx:1.9.0",
|
||||
'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1",
|
||||
'exifinterface' : "androidx.exifinterface:exifinterface:1.3.5",
|
||||
'fragmentKtx' : "androidx.fragment:fragment-ktx:$fragment",
|
||||
'fragmentTesting' : "androidx.fragment:fragment-testing:$fragment",
|
||||
'fragmentTestingManifest' : "androidx.fragment:fragment-testing-manifest:$fragment",
|
||||
'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.4",
|
||||
'work' : "androidx.work:work-runtime-ktx:2.7.1",
|
||||
'autoFill' : "androidx.autofill:autofill:1.1.0",
|
||||
|
@ -101,7 +103,7 @@ ext.libs = [
|
|||
],
|
||||
element : [
|
||||
'opusencoder' : "io.element.android:opusencoder:1.1.0",
|
||||
'wysiwyg' : "io.element.android:wysiwyg:0.15.0"
|
||||
'wysiwyg' : "io.element.android:wysiwyg:0.18.0"
|
||||
],
|
||||
squareup : [
|
||||
'moshi' : "com.squareup.moshi:moshi:$moshi",
|
||||
|
|
|
@ -2860,7 +2860,7 @@
|
|||
<string name="device_manager_sessions_sign_in_with_qr_code_title">Přihlásit se pomocí QR kódu</string>
|
||||
<string name="login_scan_qr_code">Naskenovat QR kód</string>
|
||||
<string name="labs_enable_voice_broadcast_summary">Možnost nahrávat a odesílat hlasové vysílání na časové ose místnosti.</string>
|
||||
<string name="labs_enable_voice_broadcast_title">Povolit hlasové vysílání (v aktivním vývoji)</string>
|
||||
<string name="labs_enable_voice_broadcast_title">Povolit hlasové vysílání</string>
|
||||
<string name="qr_code_login_header_failed_homeserver_is_not_supported_description">Domovský server nepodporuje přihlášení pomocí QR kódu.</string>
|
||||
<string name="qr_code_login_header_failed_user_cancelled_description">Přihlášení bylo na druhém zařízení zrušeno.</string>
|
||||
<string name="qr_code_login_header_failed_invalid_qr_code_description">Tento QR kód je neplatný.</string>
|
||||
|
@ -2946,4 +2946,13 @@
|
|||
<string name="set_link_link">Odkaz</string>
|
||||
<string name="set_link_text">Text</string>
|
||||
<string name="rich_text_editor_link">Nastavit odkaz</string>
|
||||
<string name="settings_access_token_summary">Přístupový token umožňuje plný přístup k účtu. Nikomu ho nesdělujte.</string>
|
||||
<string name="settings_access_token">Přístupový token</string>
|
||||
<string name="rich_text_editor_bullet_list">Přepnout na odrážky</string>
|
||||
<string name="rich_text_editor_numbered_list">Přepnout na číslovaný seznam</string>
|
||||
<string name="room_polls_ended_no_item">V této místnosti nejsou žádné předchozí hlasování</string>
|
||||
<string name="room_polls_ended">Předchozí hlasování</string>
|
||||
<string name="room_polls_active_no_item">V této místnosti nejsou žádné aktivní hlasování</string>
|
||||
<string name="room_polls_active">Aktivní hlasování</string>
|
||||
<string name="room_profile_section_more_polls">Historie hlasování</string>
|
||||
</resources>
|
|
@ -1123,9 +1123,7 @@
|
|||
<string name="settings_select_country">Elektu landon</string>
|
||||
<string name="settings_emails_and_phone_numbers_summary">Administri retpoŝtadresojn kaj telefonnumerojn ligitajn al via konto de Matrix</string>
|
||||
<string name="settings_emails_and_phone_numbers_title">Retpoŝtadresoj kaj telefonnumeroj</string>
|
||||
<string name="settings_unignore_user">Ĉu montri ĉiujn mesaĝojn de %s\?
|
||||
\n
|
||||
\nSciu ke tiu ĉi ago reekigos la aplikaĵon, kaj tio povas daŭri iom da tempo.</string>
|
||||
<string name="settings_unignore_user">Ĉu montri ĉiujn mesaĝojn de %s\?</string>
|
||||
<string name="settings_password_updated">Via pasvorto ĝisdatiĝis</string>
|
||||
<string name="settings_fail_to_update_password_invalid_current_password">La pasvorto ne validas</string>
|
||||
<string name="settings_fail_to_update_password">Malsukcesis ĝisdatigi pasvorton</string>
|
||||
|
@ -2201,4 +2199,5 @@
|
|||
<string name="call_ringing">Sonorante…</string>
|
||||
<string name="spaces">Aroj</string>
|
||||
<string name="initial_sync_request_reason_unignored_users">- Iom uzantoj reatentita</string>
|
||||
<string name="settings_mentions_at_room">\@room</string>
|
||||
</resources>
|
|
@ -2890,4 +2890,13 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze
|
|||
<string name="set_link_link">Hivatkozás</string>
|
||||
<string name="set_link_text">Szöveg</string>
|
||||
<string name="rich_text_editor_link">Hivatkozás beállítása</string>
|
||||
<string name="settings_access_token_summary">A hozzáférési kulcs teljes elérést biztosít a fiókhoz. Soha ne ossza meg mással.</string>
|
||||
<string name="settings_access_token">Elérési kulcs</string>
|
||||
<string name="rich_text_editor_bullet_list">Lista ki-,bekapcsolása</string>
|
||||
<string name="rich_text_editor_numbered_list">Számozott lista ki-,bekapcsolása</string>
|
||||
<string name="room_polls_ended_no_item">Nincsenek régi szavazások ebben a szobában</string>
|
||||
<string name="room_polls_ended">Régi szavazások</string>
|
||||
<string name="room_polls_active_no_item">Nincsenek aktív szavazások ebben a szobában</string>
|
||||
<string name="room_polls_active">Aktív szavazások</string>
|
||||
<string name="room_profile_section_more_polls">Szavazás alakulása</string>
|
||||
</resources>
|
|
@ -794,7 +794,7 @@
|
|||
<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_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 -->
|
||||
<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>
|
||||
|
@ -2298,6 +2298,7 @@
|
|||
<string name="sent_verification_conclusion">Verification Conclusion</string>
|
||||
<string name="sent_location">Shared their location</string>
|
||||
<string name="sent_live_location">Shared their live location</string>
|
||||
<string name="started_a_voice_broadcast">Started a voice broadcast</string>
|
||||
|
||||
<string name="verification_request_waiting">Waiting…</string>
|
||||
<string name="verification_request_other_cancelled">%s canceled</string>
|
||||
|
@ -3124,6 +3125,7 @@
|
|||
<string name="error_voice_broadcast_permission_denied_message">You don’t have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.</string>
|
||||
<string name="error_voice_broadcast_blocked_by_someone_else_message">Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.</string>
|
||||
<string name="error_voice_broadcast_already_in_progress_message">You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.</string>
|
||||
<string name="error_voice_broadcast_unable_to_play">Unable to play this voice broadcast.</string>
|
||||
<!-- Examples of usage: 6h 15min 30sec left / 15min 30sec left / 30sec left -->
|
||||
<string name="voice_broadcast_recording_time_left">%1$s left</string>
|
||||
<string name="stop_voice_broadcast_dialog_title">Stop live broadcasting?</string>
|
||||
|
@ -3197,10 +3199,22 @@
|
|||
<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="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_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_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 -->
|
||||
<string name="location_activity_title_static_sharing">Share location</string>
|
||||
|
|
|
@ -81,7 +81,7 @@ android {
|
|||
buildTypes {
|
||||
debug {
|
||||
if (project.hasProperty("coverage")) {
|
||||
testCoverageEnabled = coverage.enableTestCoverage
|
||||
testCoverageEnabled = coverage == "true"
|
||||
}
|
||||
// Set to true to log privacy or sensible data, such as token
|
||||
buildConfigField "boolean", "LOG_PRIVATE_DATA", project.property("vector.debugPrivateData")
|
||||
|
|
|
@ -23,5 +23,7 @@ data class PollResponseAggregatedSummary(
|
|||
val nbOptions: Int = 0,
|
||||
// The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk)
|
||||
val sourceEvents: List<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>,
|
||||
)
|
||||
|
|
|
@ -148,8 +148,8 @@ fun TimelineEvent.getLastMessageContent(): MessageContent? {
|
|||
// Polls/Beacon are not message contents like others as there is no msgtype subtype to discriminate moshi parsing
|
||||
// so toModel<MessageContent> won't parse them correctly
|
||||
// It's discriminated on event type instead. Maybe it shouldn't be MessageContent at all to avoid confusion?
|
||||
in EventType.POLL_START.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel<MessagePollContent>()
|
||||
in EventType.POLL_END.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel<MessageEndPollContent>()
|
||||
in EventType.POLL_START.values -> (getLastPollEditNewContent() ?: root.getClearContent()).toModel<MessagePollContent>()
|
||||
in EventType.POLL_END.values -> (getLastPollEditNewContent() ?: root.getClearContent()).toModel<MessageEndPollContent>()
|
||||
in EventType.STATE_ROOM_BEACON_INFO.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel<MessageBeaconInfoContent>()
|
||||
in EventType.BEACON_LOCATION_DATA.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel<MessageBeaconLocationDataContent>()
|
||||
else -> (getLastEditNewContent() ?: root.getClearContent()).toModel()
|
||||
|
@ -160,6 +160,10 @@ fun TimelineEvent.getLastEditNewContent(): Content? {
|
|||
return annotations?.editSummary?.latestEdit?.getClearContent()?.toModel<MessageContent>()?.newContent
|
||||
}
|
||||
|
||||
private fun TimelineEvent.getLastPollEditNewContent(): Content? {
|
||||
return annotations?.editSummary?.latestEdit?.getClearContent()?.toModel<MessagePollContent>()?.newContent
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if it's a reply.
|
||||
*/
|
||||
|
|
|
@ -59,7 +59,7 @@ internal class EventDecryptor @Inject constructor(
|
|||
private val sendToDeviceTask: SendToDeviceTask,
|
||||
private val deviceListManager: DeviceListManager,
|
||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
||||
private val cryptoStore: IMXCryptoStore
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
) {
|
||||
|
||||
/**
|
||||
|
|
|
@ -17,11 +17,16 @@
|
|||
package org.matrix.android.sdk.internal.database
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.RealmResults
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||
import org.matrix.android.sdk.internal.database.model.EventInsertEntity
|
||||
|
@ -34,7 +39,7 @@ import javax.inject.Inject
|
|||
|
||||
internal class EventInsertLiveObserver @Inject constructor(
|
||||
@SessionDatabase realmConfiguration: RealmConfiguration,
|
||||
private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>
|
||||
private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>,
|
||||
) :
|
||||
RealmLiveEntityObserver<EventInsertEntity>(realmConfiguration) {
|
||||
|
||||
|
@ -50,48 +55,90 @@ internal class EventInsertLiveObserver @Inject constructor(
|
|||
if (!results.isLoaded || results.isEmpty()) {
|
||||
return@withLock
|
||||
}
|
||||
val idsToDeleteAfterProcess = ArrayList<String>()
|
||||
val filteredEvents = ArrayList<EventInsertEntity>(results.size)
|
||||
val eventsToProcess = ArrayList<EventInsertEntity>(results.size)
|
||||
val eventsToIgnore = ArrayList<EventInsertEntity>(results.size)
|
||||
|
||||
Timber.v("EventInsertEntity updated with ${results.size} results in db")
|
||||
results.forEach {
|
||||
if (shouldProcess(it)) {
|
||||
// don't use copy from realm over there
|
||||
val copiedEvent = EventInsertEntity(
|
||||
eventId = it.eventId,
|
||||
eventType = it.eventType
|
||||
).apply {
|
||||
insertType = it.insertType
|
||||
}
|
||||
filteredEvents.add(copiedEvent)
|
||||
// don't use copy from realm over there
|
||||
val copiedEvent = EventInsertEntity(
|
||||
eventId = it.eventId,
|
||||
eventType = it.eventType
|
||||
).apply {
|
||||
insertType = it.insertType
|
||||
}
|
||||
|
||||
if (shouldProcess(it)) {
|
||||
eventsToProcess.add(copiedEvent)
|
||||
} else {
|
||||
eventsToIgnore.add(copiedEvent)
|
||||
}
|
||||
idsToDeleteAfterProcess.add(it.eventId)
|
||||
}
|
||||
|
||||
awaitTransaction(realmConfiguration) { realm ->
|
||||
Timber.v("##Transaction: There are ${filteredEvents.size} events to process ")
|
||||
filteredEvents.forEach { eventInsert ->
|
||||
Timber.v("##Transaction: There are ${eventsToProcess.size} events to process")
|
||||
|
||||
val idsToDeleteAfterProcess = ArrayList<String>()
|
||||
val idsOfEncryptedEvents = ArrayList<String>()
|
||||
val getAndTriageEvent: (EventInsertEntity) -> Event? = { eventInsert ->
|
||||
val eventId = eventInsert.eventId
|
||||
val event = EventEntity.where(realm, eventId).findFirst()
|
||||
if (event == null) {
|
||||
Timber.v("Event $eventId not found")
|
||||
val event = getEvent(realm, eventId)
|
||||
if (event?.getClearType() == EventType.ENCRYPTED) {
|
||||
idsOfEncryptedEvents.add(eventId)
|
||||
} else {
|
||||
idsToDeleteAfterProcess.add(eventId)
|
||||
}
|
||||
event
|
||||
}
|
||||
|
||||
eventsToProcess.forEach { eventInsert ->
|
||||
val eventId = eventInsert.eventId
|
||||
val event = getAndTriageEvent(eventInsert)
|
||||
|
||||
if (event != null && canProcessEvent(event)) {
|
||||
processors.filter {
|
||||
it.shouldProcess(eventId, event.getClearType(), eventInsert.insertType)
|
||||
}.forEach {
|
||||
it.process(realm, event)
|
||||
}
|
||||
} else {
|
||||
Timber.v("Cannot process event with id $eventId")
|
||||
return@forEach
|
||||
}
|
||||
val domainEvent = event.asDomain()
|
||||
processors.filter {
|
||||
it.shouldProcess(eventId, domainEvent.getClearType(), eventInsert.insertType)
|
||||
}.forEach {
|
||||
it.process(realm, domainEvent)
|
||||
}
|
||||
}
|
||||
|
||||
eventsToIgnore.forEach { getAndTriageEvent(it) }
|
||||
|
||||
realm.where(EventInsertEntity::class.java)
|
||||
.`in`(EventInsertEntityFields.EVENT_ID, idsToDeleteAfterProcess.toTypedArray())
|
||||
.findAll()
|
||||
.deleteAllFromRealm()
|
||||
|
||||
// make the encrypted events not processable: they will be processed again after decryption
|
||||
realm.where(EventInsertEntity::class.java)
|
||||
.`in`(EventInsertEntityFields.EVENT_ID, idsOfEncryptedEvents.toTypedArray())
|
||||
.findAll()
|
||||
.forEach { it.canBeProcessed = false }
|
||||
}
|
||||
processors.forEach { it.onPostProcess() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getEvent(realm: Realm, eventId: String): Event? {
|
||||
val event = EventEntity.where(realm, eventId).findFirst()
|
||||
if (event == null) {
|
||||
Timber.v("Event $eventId not found")
|
||||
}
|
||||
return event?.asDomain()
|
||||
}
|
||||
|
||||
private fun canProcessEvent(event: Event): Boolean {
|
||||
// event should be either not encrypted or if encrypted it should contain relatesTo content
|
||||
return event.getClearType() != EventType.ENCRYPTED ||
|
||||
event.content.toModel<EncryptedEventContent>()?.relatesTo != null
|
||||
}
|
||||
|
||||
private fun shouldProcess(eventInsertEntity: EventInsertEntity): Boolean {
|
||||
return processors.any {
|
||||
it.shouldProcess(eventInsertEntity.eventId, eventInsertEntity.eventType, eventInsertEntity.insertType)
|
||||
|
|
|
@ -30,7 +30,8 @@ internal object PollResponseAggregatedSummaryEntityMapper {
|
|||
closedTime = entity.closedTime,
|
||||
localEchos = entity.sourceLocalEchoEvents.toList(),
|
||||
sourceEvents = entity.sourceEvents.toList(),
|
||||
nbOptions = entity.nbOptions
|
||||
nbOptions = entity.nbOptions,
|
||||
encryptedRelatedEventIds = entity.encryptedRelatedEventIds.toList(),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -40,7 +41,8 @@ internal object PollResponseAggregatedSummaryEntityMapper {
|
|||
nbOptions = model.nbOptions,
|
||||
closedTime = model.closedTime,
|
||||
sourceEvents = RealmList<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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||
* Copyright (c) 2023 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -17,15 +17,16 @@
|
|||
package org.matrix.android.sdk.internal.database.migration
|
||||
|
||||
import io.realm.DynamicRealm
|
||||
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
|
||||
import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities
|
||||
import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntityFields
|
||||
import org.matrix.android.sdk.internal.util.database.RealmMigrator
|
||||
|
||||
/**
|
||||
* Adding a new field in poll summary to keep track of non decrypted related events.
|
||||
*/
|
||||
internal class MigrateSessionTo048(realm: DynamicRealm) : RealmMigrator(realm, 48) {
|
||||
|
||||
override fun doMigrate(realm: DynamicRealm) {
|
||||
realm.schema.get("HomeServerCapabilitiesEntity")
|
||||
?.addField(HomeServerCapabilitiesEntityFields.EXTERNAL_ACCOUNT_MANAGEMENT_URL, String::class.java)
|
||||
?.forceRefreshOfHomeServerCapabilities()
|
||||
realm.schema.get("PollResponseAggregatedSummaryEntity")
|
||||
?.addRealmListField(PollResponseAggregatedSummaryEntityFields.ENCRYPTED_RELATED_EVENT_IDS.`$`, String::class.java)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ internal open class EventInsertEntity(
|
|||
var eventType: String = "",
|
||||
/**
|
||||
* This flag will be used to filter EventInsertEntity in EventInsertLiveObserver.
|
||||
* Currently it's set to false when the event content is encrypted.
|
||||
* Currently it's set to false after an event with encrypted content has been processed.
|
||||
*/
|
||||
var canBeProcessed: Boolean = true
|
||||
) : RealmObject() {
|
||||
|
|
|
@ -33,7 +33,9 @@ internal open class PollResponseAggregatedSummaryEntity(
|
|||
|
||||
// The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk)
|
||||
var sourceEvents: RealmList<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() {
|
||||
|
||||
companion object
|
||||
|
|
|
@ -72,7 +72,7 @@ import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntit
|
|||
SpaceParentSummaryEntity::class,
|
||||
UserPresenceEntity::class,
|
||||
ThreadSummaryEntity::class,
|
||||
ThreadListPageEntity::class
|
||||
ThreadListPageEntity::class,
|
||||
]
|
||||
)
|
||||
internal class SessionRealmModule
|
||||
|
|
|
@ -20,7 +20,6 @@ import io.realm.Realm
|
|||
import io.realm.RealmList
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.kotlin.where
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.EventInsertEntity
|
||||
|
@ -32,10 +31,9 @@ internal fun EventEntity.copyToRealmOrIgnore(realm: Realm, insertType: EventInse
|
|||
.equalTo(EventEntityFields.ROOM_ID, roomId)
|
||||
.findFirst()
|
||||
return if (eventEntity == null) {
|
||||
val canBeProcessed = type != EventType.ENCRYPTED || decryptionResultJson != null
|
||||
val insertEntity = EventInsertEntity(eventId = eventId, eventType = type, canBeProcessed = canBeProcessed).apply {
|
||||
this.insertType = insertType
|
||||
}
|
||||
val insertEntity = EventInsertEntity(eventId = eventId, eventType = type, canBeProcessed = true)
|
||||
insertEntity.insertType = insertType
|
||||
|
||||
realm.insert(insertEntity)
|
||||
// copy this event entity and return it
|
||||
realm.copyToRealm(this)
|
||||
|
|
|
@ -16,13 +16,16 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.session.room
|
||||
|
||||
import org.matrix.android.sdk.api.session.events.model.Content
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.LocalEcho
|
||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||
import org.matrix.android.sdk.api.session.events.model.getRelationContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.events.model.toValidDecryptedEvent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
@ -101,7 +104,7 @@ internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCrypto
|
|||
if (originalDecrypted.type != replaceDecrypted.type) {
|
||||
return EditValidity.Invalid("replacement and original events must have the same type")
|
||||
}
|
||||
if (replaceDecrypted.clearContent.toModel<MessageContent>()?.newContent == null) {
|
||||
if (!hasNewContent(replaceDecrypted.type, replaceDecrypted.clearContent)) {
|
||||
return EditValidity.Invalid("replacement event must have an m.new_content property")
|
||||
}
|
||||
} else {
|
||||
|
@ -116,11 +119,18 @@ internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCrypto
|
|||
if (originalEvent.type != replaceEvent.type) {
|
||||
return EditValidity.Invalid("replacement and original events must have the same type")
|
||||
}
|
||||
if (replaceEvent.content.toModel<MessageContent>()?.newContent == null) {
|
||||
if (!hasNewContent(replaceEvent.type, replaceEvent.content)) {
|
||||
return EditValidity.Invalid("replacement event must have an m.new_content property")
|
||||
}
|
||||
}
|
||||
|
||||
return EditValidity.Valid
|
||||
}
|
||||
|
||||
private fun hasNewContent(eventType: String?, content: Content?): Boolean {
|
||||
return when (eventType) {
|
||||
in EventType.POLL_START.values -> content.toModel<MessagePollContent>()?.newContent != null
|
||||
else -> content.toModel<MessageContent>()?.newContent != null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,6 +61,7 @@ import org.matrix.android.sdk.internal.di.UserId
|
|||
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
|
||||
import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.LiveLocationAggregationProcessor
|
||||
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollAggregationProcessor
|
||||
import org.matrix.android.sdk.internal.session.room.aggregation.utd.EncryptedReferenceAggregationProcessor
|
||||
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
|
||||
import org.matrix.android.sdk.internal.util.time.Clock
|
||||
import timber.log.Timber
|
||||
|
@ -73,6 +74,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
private val sessionManager: SessionManager,
|
||||
private val liveLocationAggregationProcessor: LiveLocationAggregationProcessor,
|
||||
private val pollAggregationProcessor: PollAggregationProcessor,
|
||||
private val encryptedReferenceAggregationProcessor: EncryptedReferenceAggregationProcessor,
|
||||
private val editValidator: EventEditValidator,
|
||||
private val clock: Clock,
|
||||
) : EventInsertLiveProcessor {
|
||||
|
@ -140,6 +142,16 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}")
|
||||
handleReaction(realm, event, roomId, isLocalEcho)
|
||||
}
|
||||
EventType.ENCRYPTED -> {
|
||||
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
|
||||
processEncryptedContent(
|
||||
encryptedEventContent = encryptedEventContent,
|
||||
realm = realm,
|
||||
event = event,
|
||||
roomId = roomId,
|
||||
isLocalEcho = isLocalEcho,
|
||||
)
|
||||
}
|
||||
EventType.MESSAGE -> {
|
||||
if (event.unsignedData?.relations?.annotations != null) {
|
||||
Timber.v("###REACTION Aggregation in room $roomId for event ${event.eventId}")
|
||||
|
@ -170,32 +182,6 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
// As for now Live event processors are not receiving UTD events.
|
||||
// They will get an update if the event is decrypted later
|
||||
EventType.ENCRYPTED -> {
|
||||
// Relation type is in clear, it might be possible to do some things?
|
||||
// Notice that if the event is decrypted later, process be called again
|
||||
val encryptedEventContent = event.content.toModel<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 -> {
|
||||
val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() }
|
||||
?: return
|
||||
|
@ -250,6 +236,36 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun processEncryptedContent(
|
||||
encryptedEventContent: EncryptedEventContent?,
|
||||
realm: Realm,
|
||||
event: Event,
|
||||
roomId: String,
|
||||
isLocalEcho: Boolean,
|
||||
) {
|
||||
when (encryptedEventContent?.relatesTo?.type) {
|
||||
RelationType.REPLACE -> {
|
||||
Timber.w("## UTD replace in room $roomId for event ${event.eventId}")
|
||||
}
|
||||
RelationType.RESPONSE -> {
|
||||
Timber.w("## UTD response in room $roomId related to ${encryptedEventContent.relatesTo.eventId}")
|
||||
}
|
||||
RelationType.REFERENCE -> {
|
||||
Timber.w("## UTD reference in room $roomId related to ${encryptedEventContent.relatesTo.eventId}")
|
||||
encryptedReferenceAggregationProcessor.handle(
|
||||
realm = realm,
|
||||
event = event,
|
||||
isLocalEcho = isLocalEcho,
|
||||
relatedEventId = encryptedEventContent.relatesTo.eventId,
|
||||
)
|
||||
}
|
||||
RelationType.ANNOTATION -> {
|
||||
Timber.w("## UTD annotation in room $roomId related to ${encryptedEventContent.relatesTo.eventId}")
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
// OPT OUT serer aggregation until API mature enough
|
||||
private val SHOULD_HANDLE_SERVER_AGREGGATION = false // should be true to work with e2e
|
||||
|
||||
|
|
|
@ -155,6 +155,8 @@ internal class DefaultPollAggregationProcessor @Inject constructor(
|
|||
)
|
||||
aggregatedPollSummaryEntity.aggregatedContent = ContentMapper.map(newSumModel.toContent())
|
||||
|
||||
event.eventId?.let { removeEncryptedRelatedEventIdIfNeeded(aggregatedPollSummaryEntity, it) }
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -180,6 +182,8 @@ internal class DefaultPollAggregationProcessor @Inject constructor(
|
|||
aggregatedPollSummaryEntity.sourceEvents.add(event.eventId)
|
||||
}
|
||||
|
||||
event.eventId?.let { removeEncryptedRelatedEventIdIfNeeded(aggregatedPollSummaryEntity, it) }
|
||||
|
||||
if (!isLocalEcho) {
|
||||
ensurePollIsFullyAggregated(roomId, pollEventId)
|
||||
}
|
||||
|
@ -226,4 +230,10 @@ internal class DefaultPollAggregationProcessor @Inject constructor(
|
|||
fetchPollResponseEventsTask.execute(params)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeEncryptedRelatedEventIdIfNeeded(aggregatedPollSummaryEntity: PollResponseAggregatedSummaryEntity, eventId: String) {
|
||||
if (aggregatedPollSummaryEntity.encryptedRelatedEventIds.contains(eventId)) {
|
||||
aggregatedPollSummaryEntity.encryptedRelatedEventIds.remove(eventId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ import org.matrix.android.sdk.api.session.Session
|
|||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
|
||||
|
||||
interface PollAggregationProcessor {
|
||||
internal interface PollAggregationProcessor {
|
||||
/**
|
||||
* Poll start events don't need to be processed by the aggregator.
|
||||
* This function will only handle if the poll is edited and will update the poll summary entity.
|
||||
|
|
|
@ -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 org.amshove.kluent.shouldBeFalse
|
||||
import org.amshove.kluent.shouldBeTrue
|
||||
import org.amshove.kluent.shouldContain
|
||||
import org.amshove.kluent.shouldNotContain
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
|
@ -105,6 +107,24 @@ class DefaultPollAggregationProcessorTest {
|
|||
pollAggregationProcessor.handlePollResponseEvent(session, realm.instance, A_POLL_RESPONSE_EVENT).shouldBeTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a poll response event with a reference, when processing, then event id is removed from encrypted events list`() {
|
||||
// Given
|
||||
val anotherEventId = "other-event-id"
|
||||
val pollResponseAggregatedSummaryEntity = PollResponseAggregatedSummaryEntity(
|
||||
encryptedRelatedEventIds = RealmList(AN_EVENT_ID, anotherEventId)
|
||||
)
|
||||
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns pollResponseAggregatedSummaryEntity
|
||||
|
||||
// When
|
||||
val result = pollAggregationProcessor.handlePollResponseEvent(session, realm.instance, A_POLL_RESPONSE_EVENT)
|
||||
|
||||
// Then
|
||||
result.shouldBeTrue()
|
||||
pollResponseAggregatedSummaryEntity.encryptedRelatedEventIds.shouldNotContain(AN_EVENT_ID)
|
||||
pollResponseAggregatedSummaryEntity.encryptedRelatedEventIds.shouldContain(anotherEventId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a poll response event after poll is closed, when processing, then is ignored and returns false`() {
|
||||
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity().apply {
|
||||
|
@ -132,12 +152,33 @@ class DefaultPollAggregationProcessorTest {
|
|||
// Given
|
||||
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity()
|
||||
every { fakeTaskExecutor.instance.executorScope } returns this
|
||||
|
||||
// When
|
||||
val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, true)
|
||||
|
||||
// When
|
||||
val result = pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT)
|
||||
|
||||
// Then
|
||||
pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue()
|
||||
result.shouldBeTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a poll end event, when processing, then event id is removed from encrypted events list`() = runTest {
|
||||
// Given
|
||||
val anotherEventId = "other-event-id"
|
||||
val pollResponseAggregatedSummaryEntity = PollResponseAggregatedSummaryEntity(
|
||||
encryptedRelatedEventIds = RealmList(AN_EVENT_ID, anotherEventId)
|
||||
)
|
||||
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns pollResponseAggregatedSummaryEntity
|
||||
every { fakeTaskExecutor.instance.executorScope } returns this
|
||||
val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, true)
|
||||
|
||||
// When
|
||||
val result = pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT)
|
||||
|
||||
// Then
|
||||
result.shouldBeTrue()
|
||||
pollResponseAggregatedSummaryEntity.encryptedRelatedEventIds.shouldNotContain(AN_EVENT_ID)
|
||||
pollResponseAggregatedSummaryEntity.encryptedRelatedEventIds.shouldContain(anotherEventId)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -145,12 +186,13 @@ class DefaultPollAggregationProcessorTest {
|
|||
// Given
|
||||
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity()
|
||||
every { fakeTaskExecutor.instance.executorScope } returns this
|
||||
|
||||
// When
|
||||
val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, false)
|
||||
|
||||
// When
|
||||
val result = pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT)
|
||||
|
||||
// Then
|
||||
pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue()
|
||||
result.shouldBeTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
|
@ -232,7 +232,7 @@ android {
|
|||
resValue "color", "launcher_background", "#0DBD8B"
|
||||
|
||||
if (project.hasProperty("coverage")) {
|
||||
testCoverageEnabled = coverage.enableTestCoverage
|
||||
testCoverageEnabled = coverage == "true"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -403,8 +403,8 @@ dependencies {
|
|||
androidTestImplementation libs.mockk.mockkAndroid
|
||||
androidTestUtil libs.androidx.orchestrator
|
||||
androidTestImplementation libs.androidx.fragmentTesting
|
||||
androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.22"
|
||||
debugImplementation libs.androidx.fragmentTesting
|
||||
androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.8.0"
|
||||
debugImplementation libs.androidx.fragmentTestingManifest
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
|
||||
}
|
||||
|
||||
|
|
|
@ -69,7 +69,7 @@ android {
|
|||
buildTypes {
|
||||
debug {
|
||||
if (project.hasProperty("coverage")) {
|
||||
testCoverageEnabled = coverage.enableTestCoverage
|
||||
testCoverageEnabled = coverage == "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -330,6 +330,7 @@ dependencies {
|
|||
}
|
||||
androidTestImplementation libs.mockk.mockkAndroid
|
||||
androidTestUtil libs.androidx.orchestrator
|
||||
debugImplementation libs.androidx.fragmentTesting
|
||||
androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.22"
|
||||
debugImplementation libs.androidx.fragmentTestingManifest
|
||||
androidTestImplementation libs.androidx.fragmentTesting
|
||||
androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.8.0"
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import android.content.ActivityNotFoundException
|
|||
import im.vector.app.R
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.call.dialpad.DialPadLookup
|
||||
import im.vector.app.features.roomprofile.polls.RoomPollsLoadingError
|
||||
import im.vector.app.features.voice.VoiceFailure
|
||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure
|
||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure.RecordingError
|
||||
|
@ -138,6 +139,7 @@ class DefaultErrorFormatter @Inject constructor(
|
|||
stringProvider.getString(R.string.login_signin_matrix_id_error_invalid_matrix_id)
|
||||
is VoiceFailure -> voiceMessageError(throwable)
|
||||
is VoiceBroadcastFailure -> voiceBroadcastMessageError(throwable)
|
||||
is RoomPollsLoadingError -> stringProvider.getString(R.string.room_polls_loading_error)
|
||||
is ActivityNotFoundException ->
|
||||
stringProvider.getString(R.string.error_no_external_application_found)
|
||||
else -> throwable.localizedMessage
|
||||
|
@ -157,6 +159,8 @@ class DefaultErrorFormatter @Inject constructor(
|
|||
RecordingError.BlockedBySomeoneElse -> stringProvider.getString(R.string.error_voice_broadcast_blocked_by_someone_else_message)
|
||||
RecordingError.NoPermission -> stringProvider.getString(R.string.error_voice_broadcast_permission_denied_message)
|
||||
RecordingError.UserAlreadyBroadcasting -> stringProvider.getString(R.string.error_voice_broadcast_already_in_progress_message)
|
||||
is VoiceBroadcastFailure.ListeningError.UnableToPlay,
|
||||
is VoiceBroadcastFailure.ListeningError.DownloadError -> stringProvider.getString(R.string.error_voice_broadcast_unable_to_play)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ package im.vector.app.features.crypto.verification
|
|||
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.KeyEvent
|
||||
|
@ -84,10 +85,6 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetV
|
|||
return BottomSheetVerificationBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
init {
|
||||
isCancelable = false
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
|
@ -210,6 +207,8 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetV
|
|||
return@withState
|
||||
}
|
||||
|
||||
isCancelable = state.isVerificationRequired.not()
|
||||
|
||||
// Did the request result in a SAS transaction?
|
||||
if (state.sasTransactionState != null) {
|
||||
when (state.sasTransactionState) {
|
||||
|
@ -396,6 +395,11 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetV
|
|||
|
||||
const val WAITING_SELF_VERIF_TAG: String = "WAITING_SELF_VERIF_TAG"
|
||||
}
|
||||
|
||||
override fun onCancel(dialog: DialogInterface) {
|
||||
super.onCancel(dialog)
|
||||
viewModel.confirmCancel()
|
||||
}
|
||||
}
|
||||
|
||||
// fun View.getParentCoordinatorLayout(): CoordinatorLayout? {
|
||||
|
|
|
@ -138,7 +138,7 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun handleOnTextChanged(action: MessageComposerAction.OnTextChanged) {
|
||||
val needsSendButtonVisibilityUpdate = currentComposerText.isEmpty() != action.text.isEmpty()
|
||||
val needsSendButtonVisibilityUpdate = currentComposerText.isBlank() != action.text.isBlank()
|
||||
currentComposerText = SpannableString(action.text)
|
||||
if (needsSendButtonVisibilityUpdate) {
|
||||
updateIsSendButtonVisibility(true)
|
||||
|
|
|
@ -229,6 +229,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
|
|||
voiceMessageViews.renderPlaying(state)
|
||||
}
|
||||
is AudioMessagePlaybackTracker.Listener.State.Paused,
|
||||
is AudioMessagePlaybackTracker.Listener.State.Error,
|
||||
is AudioMessagePlaybackTracker.Listener.State.Idle -> {
|
||||
voiceMessageViews.renderIdle()
|
||||
}
|
||||
|
|
|
@ -216,8 +216,8 @@ class MessageActionsViewModel @AssistedInject constructor(
|
|||
noticeEventFormatter.format(timelineEvent, room?.roomSummary()?.isDirect.orFalse())
|
||||
}
|
||||
in EventType.POLL_START.values -> {
|
||||
timelineEvent.root.getClearContent().toModel<MessagePollContent>(catchError = true)
|
||||
?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: ""
|
||||
(timelineEvent.getVectorLastMessageContent() as? MessagePollContent)?.getBestPollCreationInfo()?.question?.getBestQuestion()
|
||||
?: stringProvider.getString(R.string.message_reply_to_poll_preview)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
|
|
@ -83,9 +83,14 @@ class PollItemViewStateFactory @Inject constructor(
|
|||
totalVotes: Int,
|
||||
winnerVoteCount: Int?,
|
||||
): PollViewState {
|
||||
val totalVotesText = if (pollResponseSummary?.hasEncryptedRelatedEvents.orFalse()) {
|
||||
stringProvider.getString(R.string.unable_to_decrypt_some_events_in_poll)
|
||||
} else {
|
||||
stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, totalVotes, totalVotes)
|
||||
}
|
||||
return PollViewState(
|
||||
question = question,
|
||||
votesStatus = stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, totalVotes, totalVotes),
|
||||
votesStatus = totalVotesText,
|
||||
canVote = false,
|
||||
optionViewStates = pollCreationInfo?.answers?.map { answer ->
|
||||
val voteSummary = pollResponseSummary?.getVoteSummaryOfAnOption(answer.id ?: "")
|
||||
|
@ -126,9 +131,14 @@ class PollItemViewStateFactory @Inject constructor(
|
|||
pollResponseSummary: PollResponseData?,
|
||||
totalVotes: Int
|
||||
): PollViewState {
|
||||
val totalVotesText = if (pollResponseSummary?.hasEncryptedRelatedEvents.orFalse()) {
|
||||
stringProvider.getString(R.string.unable_to_decrypt_some_events_in_poll)
|
||||
} else {
|
||||
stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, totalVotes, totalVotes)
|
||||
}
|
||||
return PollViewState(
|
||||
question = question,
|
||||
votesStatus = stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, totalVotes, totalVotes),
|
||||
votesStatus = totalVotesText,
|
||||
canVote = true,
|
||||
optionViewStates = pollCreationInfo?.answers?.map { answer ->
|
||||
val isMyVote = pollResponseSummary?.myVote == answer.id
|
||||
|
@ -144,7 +154,11 @@ class PollItemViewStateFactory @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun createReadyPollViewState(question: String, pollCreationInfo: PollCreationInfo?, totalVotes: Int): PollViewState {
|
||||
private fun createReadyPollViewState(
|
||||
question: String,
|
||||
pollCreationInfo: PollCreationInfo?,
|
||||
totalVotes: Int
|
||||
): PollViewState {
|
||||
val totalVotesText = if (totalVotes == 0) {
|
||||
stringProvider.getString(R.string.poll_no_votes_cast)
|
||||
} else {
|
||||
|
|
|
@ -15,9 +15,9 @@
|
|||
*/
|
||||
package im.vector.app.features.home.room.detail.timeline.factory
|
||||
|
||||
import im.vector.app.core.error.ErrorFormatter
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
import im.vector.app.core.resources.DrawableProvider
|
||||
import im.vector.app.features.displayname.getBestName
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.VoiceBroadcastEventsGroup
|
||||
|
@ -36,7 +36,6 @@ import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
|||
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.getUserOrDefault
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -45,6 +44,7 @@ class VoiceBroadcastItemFactory @Inject constructor(
|
|||
private val avatarSizeProvider: AvatarSizeProvider,
|
||||
private val colorProvider: ColorProvider,
|
||||
private val drawableProvider: DrawableProvider,
|
||||
private val errorFormatter: ErrorFormatter,
|
||||
private val voiceBroadcastRecorder: VoiceBroadcastRecorder?,
|
||||
private val voiceBroadcastPlayer: VoiceBroadcastPlayer,
|
||||
private val playbackTracker: AudioMessagePlaybackTracker,
|
||||
|
@ -75,13 +75,14 @@ class VoiceBroadcastItemFactory @Inject constructor(
|
|||
voiceBroadcast = voiceBroadcast,
|
||||
voiceBroadcastState = voiceBroadcastContent.voiceBroadcastState,
|
||||
duration = voiceBroadcastEventsGroup.getDuration(),
|
||||
recorderName = params.event.root.stateKey?.let { session.getUserOrDefault(it) }?.toMatrixItem()?.getBestName().orEmpty(),
|
||||
recorderName = params.event.senderInfo.disambiguatedDisplayName,
|
||||
recorder = voiceBroadcastRecorder,
|
||||
player = voiceBroadcastPlayer,
|
||||
playbackTracker = playbackTracker,
|
||||
roomItem = session.getRoom(params.event.roomId)?.roomSummary()?.toMatrixItem(),
|
||||
colorProvider = colorProvider,
|
||||
drawableProvider = drawableProvider,
|
||||
errorFormatter = errorFormatter,
|
||||
)
|
||||
|
||||
return if (isRecording) {
|
||||
|
|
|
@ -27,6 +27,7 @@ import im.vector.app.core.resources.StringProvider
|
|||
import im.vector.app.features.html.EventHtmlRenderer
|
||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
||||
import im.vector.app.features.voicebroadcast.isLive
|
||||
import im.vector.app.features.voicebroadcast.isVoiceBroadcast
|
||||
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
||||
import me.gujun.android.span.image
|
||||
import me.gujun.android.span.span
|
||||
|
@ -39,6 +40,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
|||
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||
import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
|
||||
import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.getTextDisplayableContent
|
||||
|
@ -86,10 +88,16 @@ class DisplayableEventFormatter @Inject constructor(
|
|||
simpleFormat(senderName, stringProvider.getString(R.string.sent_an_image), appendAuthor)
|
||||
}
|
||||
MessageType.MSGTYPE_AUDIO -> {
|
||||
if ((messageContent as? MessageAudioContent)?.voiceMessageIndicator != null) {
|
||||
simpleFormat(senderName, stringProvider.getString(R.string.sent_a_voice_message), appendAuthor)
|
||||
} else {
|
||||
simpleFormat(senderName, stringProvider.getString(R.string.sent_an_audio_file), appendAuthor)
|
||||
when {
|
||||
(messageContent as? MessageAudioContent)?.voiceMessageIndicator == null -> {
|
||||
simpleFormat(senderName, stringProvider.getString(R.string.sent_an_audio_file), appendAuthor)
|
||||
}
|
||||
timelineEvent.root.asMessageAudioEvent().isVoiceBroadcast() -> {
|
||||
simpleFormat(senderName, stringProvider.getString(R.string.started_a_voice_broadcast), appendAuthor)
|
||||
}
|
||||
else -> {
|
||||
simpleFormat(senderName, stringProvider.getString(R.string.sent_a_voice_message), appendAuthor)
|
||||
}
|
||||
}
|
||||
}
|
||||
MessageType.MSGTYPE_VIDEO -> {
|
||||
|
@ -130,7 +138,7 @@ class DisplayableEventFormatter @Inject constructor(
|
|||
span { }
|
||||
}
|
||||
in EventType.POLL_START.values -> {
|
||||
timelineEvent.root.getClearContent().toModel<MessagePollContent>(catchError = true)?.getBestPollCreationInfo()?.question?.getBestQuestion()
|
||||
(timelineEvent.getVectorLastMessageContent() as? MessagePollContent)?.getBestPollCreationInfo()?.question?.getBestQuestion()
|
||||
?: stringProvider.getString(R.string.sent_a_poll)
|
||||
}
|
||||
in EventType.POLL_RESPONSE.values -> {
|
||||
|
|
|
@ -50,8 +50,11 @@ class AudioMessagePlaybackTracker @Inject constructor() {
|
|||
listeners.remove(id)
|
||||
}
|
||||
|
||||
fun pauseAllPlaybacks() {
|
||||
listeners.keys.forEach(::pausePlayback)
|
||||
fun unregisterListeners() {
|
||||
listeners.forEach {
|
||||
it.value.onUpdate(Listener.State.Idle)
|
||||
}
|
||||
listeners.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -84,6 +87,10 @@ class AudioMessagePlaybackTracker @Inject constructor() {
|
|||
}
|
||||
}
|
||||
|
||||
fun pauseAllPlaybacks() {
|
||||
listeners.keys.forEach(::pausePlayback)
|
||||
}
|
||||
|
||||
fun pausePlayback(id: String) {
|
||||
val state = getPlaybackState(id)
|
||||
if (state is Listener.State.Playing) {
|
||||
|
@ -94,7 +101,14 @@ class AudioMessagePlaybackTracker @Inject constructor() {
|
|||
}
|
||||
|
||||
fun stopPlayback(id: String) {
|
||||
setState(id, Listener.State.Idle)
|
||||
val state = getPlaybackState(id)
|
||||
if (state !is Listener.State.Error) {
|
||||
setState(id, Listener.State.Idle)
|
||||
}
|
||||
}
|
||||
|
||||
fun onError(id: String, error: Throwable) {
|
||||
setState(id, Listener.State.Error(error))
|
||||
}
|
||||
|
||||
fun updatePlayingAtPlaybackTime(id: String, time: Int, percentage: Float) {
|
||||
|
@ -116,6 +130,7 @@ class AudioMessagePlaybackTracker @Inject constructor() {
|
|||
is Listener.State.Playing -> state.playbackTime
|
||||
is Listener.State.Paused -> state.playbackTime
|
||||
is Listener.State.Recording,
|
||||
is Listener.State.Error,
|
||||
Listener.State.Idle,
|
||||
null -> null
|
||||
}
|
||||
|
@ -126,18 +141,12 @@ class AudioMessagePlaybackTracker @Inject constructor() {
|
|||
is Listener.State.Playing -> state.percentage
|
||||
is Listener.State.Paused -> state.percentage
|
||||
is Listener.State.Recording,
|
||||
is Listener.State.Error,
|
||||
Listener.State.Idle,
|
||||
null -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun unregisterListeners() {
|
||||
listeners.forEach {
|
||||
it.value.onUpdate(Listener.State.Idle)
|
||||
}
|
||||
listeners.clear()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val RECORDING_ID = "RECORDING_ID"
|
||||
}
|
||||
|
@ -148,6 +157,7 @@ class AudioMessagePlaybackTracker @Inject constructor() {
|
|||
|
||||
sealed class State {
|
||||
object Idle : State()
|
||||
data class Error(val failure: Throwable) : State()
|
||||
data class Playing(val playbackTime: Int, val percentage: Float) : State()
|
||||
data class Paused(val playbackTime: Int, val percentage: Float) : State()
|
||||
data class Recording(val amplitudeList: List<Int>) : State()
|
||||
|
|
|
@ -44,7 +44,8 @@ class PollResponseDataFactory @Inject constructor(
|
|||
)
|
||||
},
|
||||
winnerVoteCount = it.aggregatedContent?.winnerVoteCount ?: 0,
|
||||
totalVotes = it.aggregatedContent?.totalVotes ?: 0
|
||||
totalVotes = it.aggregatedContent?.totalVotes ?: 0,
|
||||
hasEncryptedRelatedEvents = it.encryptedRelatedEventIds.isNotEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import androidx.annotation.IdRes
|
|||
import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.error.ErrorFormatter
|
||||
import im.vector.app.core.extensions.tintBackground
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
import im.vector.app.core.resources.DrawableProvider
|
||||
|
@ -48,6 +49,7 @@ abstract class AbsMessageVoiceBroadcastItem<H : AbsMessageVoiceBroadcastItem.Hol
|
|||
protected val colorProvider get() = voiceBroadcastAttributes.colorProvider
|
||||
protected val drawableProvider get() = voiceBroadcastAttributes.drawableProvider
|
||||
protected val avatarRenderer get() = attributes.avatarRenderer
|
||||
protected val errorFormatter get() = voiceBroadcastAttributes.errorFormatter
|
||||
protected val callback get() = attributes.callback
|
||||
|
||||
override fun isCacheable(): Boolean = false
|
||||
|
@ -107,5 +109,6 @@ abstract class AbsMessageVoiceBroadcastItem<H : AbsMessageVoiceBroadcastItem.Hol
|
|||
val roomItem: MatrixItem?,
|
||||
val colorProvider: ColorProvider,
|
||||
val drawableProvider: DrawableProvider,
|
||||
val errorFormatter: ErrorFormatter,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -142,6 +142,7 @@ abstract class MessageAudioItem : AbsMessageItem<MessageAudioItem.Holder>() {
|
|||
private fun renderStateBasedOnAudioPlayback(holder: Holder) {
|
||||
audioMessagePlaybackTracker.track(attributes.informationData.eventId) { state ->
|
||||
when (state) {
|
||||
is AudioMessagePlaybackTracker.Listener.State.Error,
|
||||
is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder)
|
||||
is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state)
|
||||
is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state)
|
||||
|
|
|
@ -90,7 +90,8 @@ data class PollResponseData(
|
|||
val votes: Map<String, PollVoteSummaryData>?,
|
||||
val totalVotes: Int = 0,
|
||||
val winnerVoteCount: Int = 0,
|
||||
val isClosed: Boolean = false
|
||||
val isClosed: Boolean = false,
|
||||
val hasEncryptedRelatedEvents: Boolean = false,
|
||||
) : Parcelable {
|
||||
|
||||
fun getVoteSummaryOfAnOption(optionId: String) = votes?.get(optionId)
|
||||
|
|
|
@ -20,11 +20,13 @@ import android.text.format.DateUtils
|
|||
import android.widget.ImageButton
|
||||
import android.widget.SeekBar
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.Group
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.onClick
|
||||
import im.vector.app.core.extensions.setTextOrHide
|
||||
import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker.Listener.State
|
||||
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
|
||||
|
@ -54,6 +56,16 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
|
|||
}
|
||||
}
|
||||
player.addListener(voiceBroadcast, playerListener)
|
||||
|
||||
playbackTracker.track(voiceBroadcast.voiceBroadcastId) { playbackState ->
|
||||
renderBackwardForwardButtons(holder, playbackState)
|
||||
renderPlaybackError(holder, playbackState)
|
||||
renderLiveIndicator(holder)
|
||||
if (!isUserSeeking) {
|
||||
holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
bindSeekBar(holder)
|
||||
bindButtons(holder)
|
||||
}
|
||||
|
@ -63,10 +75,11 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
|
|||
playPauseButton.setOnClickListener {
|
||||
if (player.currentVoiceBroadcast == voiceBroadcast) {
|
||||
when (player.playingState) {
|
||||
VoiceBroadcastPlayer.State.PLAYING,
|
||||
VoiceBroadcastPlayer.State.BUFFERING -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause)
|
||||
VoiceBroadcastPlayer.State.PAUSED,
|
||||
VoiceBroadcastPlayer.State.IDLE -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast))
|
||||
VoiceBroadcastPlayer.State.Playing,
|
||||
VoiceBroadcastPlayer.State.Buffering -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause)
|
||||
VoiceBroadcastPlayer.State.Paused,
|
||||
is VoiceBroadcastPlayer.State.Error,
|
||||
VoiceBroadcastPlayer.State.Idle -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast))
|
||||
}
|
||||
} else {
|
||||
callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast))
|
||||
|
@ -100,17 +113,18 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
|
|||
|
||||
private fun renderPlayingState(holder: Holder, state: VoiceBroadcastPlayer.State) {
|
||||
with(holder) {
|
||||
bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING
|
||||
voiceBroadcastMetadata.isVisible = state != VoiceBroadcastPlayer.State.BUFFERING
|
||||
bufferingView.isVisible = state == VoiceBroadcastPlayer.State.Buffering
|
||||
voiceBroadcastMetadata.isVisible = state != VoiceBroadcastPlayer.State.Buffering
|
||||
|
||||
when (state) {
|
||||
VoiceBroadcastPlayer.State.PLAYING,
|
||||
VoiceBroadcastPlayer.State.BUFFERING -> {
|
||||
VoiceBroadcastPlayer.State.Playing,
|
||||
VoiceBroadcastPlayer.State.Buffering -> {
|
||||
playPauseButton.setImageResource(R.drawable.ic_play_pause_pause)
|
||||
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast)
|
||||
}
|
||||
VoiceBroadcastPlayer.State.IDLE,
|
||||
VoiceBroadcastPlayer.State.PAUSED -> {
|
||||
is VoiceBroadcastPlayer.State.Error,
|
||||
VoiceBroadcastPlayer.State.Idle,
|
||||
VoiceBroadcastPlayer.State.Paused -> {
|
||||
playPauseButton.setImageResource(R.drawable.ic_play_pause_play)
|
||||
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast)
|
||||
}
|
||||
|
@ -120,6 +134,18 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
|
|||
}
|
||||
}
|
||||
|
||||
private fun renderPlaybackError(holder: Holder, playbackState: State) {
|
||||
with(holder) {
|
||||
if (playbackState is State.Error) {
|
||||
controlsGroup.isVisible = false
|
||||
errorView.setTextOrHide(errorFormatter.toHumanReadable(playbackState.failure))
|
||||
} else {
|
||||
errorView.isVisible = false
|
||||
controlsGroup.isVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindSeekBar(holder: Holder) {
|
||||
with(holder) {
|
||||
remainingTimeView.text = formatRemainingTime(duration)
|
||||
|
@ -141,13 +167,6 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
|
|||
}
|
||||
})
|
||||
}
|
||||
playbackTracker.track(voiceBroadcast.voiceBroadcastId) { playbackState ->
|
||||
renderBackwardForwardButtons(holder, playbackState)
|
||||
renderLiveIndicator(holder)
|
||||
if (!isUserSeeking) {
|
||||
holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) ?: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderBackwardForwardButtons(holder: Holder, playbackState: State) {
|
||||
|
@ -187,6 +206,8 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
|
|||
val broadcasterNameMetadata by bind<VoiceBroadcastMetadataView>(R.id.broadcasterNameMetadata)
|
||||
val voiceBroadcastMetadata by bind<VoiceBroadcastMetadataView>(R.id.voiceBroadcastMetadata)
|
||||
val listenersCountMetadata by bind<VoiceBroadcastMetadataView>(R.id.listenersCountMetadata)
|
||||
val errorView by bind<TextView>(R.id.errorView)
|
||||
val controlsGroup by bind<Group>(R.id.controlsGroup)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -124,6 +124,7 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
|
|||
|
||||
audioMessagePlaybackTracker.track(attributes.informationData.eventId) { state ->
|
||||
when (state) {
|
||||
is AudioMessagePlaybackTracker.Listener.State.Error,
|
||||
is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder, waveformColorIdle, waveformColorPlayed)
|
||||
is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state, waveformColorIdle, waveformColorPlayed)
|
||||
is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state, waveformColorIdle, waveformColorPlayed)
|
||||
|
|
|
@ -22,41 +22,33 @@ import com.airbnb.mvrx.Loading
|
|||
import im.vector.app.R
|
||||
import im.vector.app.core.date.DateFormatKind
|
||||
import im.vector.app.core.date.VectorDateFormatter
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.app.core.error.ErrorFormatter
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.RoomListDisplayMode
|
||||
import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter
|
||||
import im.vector.app.features.home.room.list.usecase.GetLatestPreviewableEventUseCase
|
||||
import im.vector.app.features.home.room.typing.TypingHelper
|
||||
import im.vector.app.features.voicebroadcast.isLive
|
||||
import im.vector.app.features.voicebroadcast.isVoiceBroadcast
|
||||
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
||||
import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase
|
||||
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.room.getTimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
|
||||
import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomSummaryItemFactory @Inject constructor(
|
||||
private val sessionHolder: ActiveSessionHolder,
|
||||
private val displayableEventFormatter: DisplayableEventFormatter,
|
||||
private val dateFormatter: VectorDateFormatter,
|
||||
private val stringProvider: StringProvider,
|
||||
private val typingHelper: TypingHelper,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val errorFormatter: ErrorFormatter,
|
||||
private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase,
|
||||
private val getLatestPreviewableEventUseCase: GetLatestPreviewableEventUseCase,
|
||||
) {
|
||||
|
||||
fun create(
|
||||
|
@ -142,7 +134,7 @@ class RoomSummaryItemFactory @Inject constructor(
|
|||
val showSelected = selectedRoomIds.contains(roomSummary.roomId)
|
||||
var latestFormattedEvent: CharSequence = ""
|
||||
var latestEventTime = ""
|
||||
val latestEvent = roomSummary.getVectorLatestPreviewableEvent()
|
||||
val latestEvent = getLatestPreviewableEventUseCase.execute(roomSummary.roomId)
|
||||
if (latestEvent != null) {
|
||||
latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect, roomSummary.isDirect.not())
|
||||
latestEventTime = dateFormatter.format(latestEvent.root.originServerTs, DateFormatKind.ROOM_LIST)
|
||||
|
@ -150,7 +142,8 @@ class RoomSummaryItemFactory @Inject constructor(
|
|||
|
||||
val typingMessage = typingHelper.getTypingMessage(roomSummary.typingUsers)
|
||||
// Skip typing while there is a live voice broadcast
|
||||
.takeUnless { latestEvent?.root?.asVoiceBroadcastEvent()?.isLive.orFalse() }.orEmpty()
|
||||
.takeUnless { latestEvent?.root?.asVoiceBroadcastEvent()?.isLive.orFalse() }
|
||||
.orEmpty()
|
||||
|
||||
return if (subtitle.isBlank() && displayMode == RoomListDisplayMode.FILTERED) {
|
||||
createCenteredRoomSummaryItem(roomSummary, displayMode, showSelected, unreadCount, onClick, onLongClick)
|
||||
|
@ -240,14 +233,4 @@ class RoomSummaryItemFactory @Inject constructor(
|
|||
else -> stringProvider.getQuantityString(R.plurals.search_space_multiple_parents, size - 1, directParentNames[0], size - 1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun RoomSummary.getVectorLatestPreviewableEvent(): TimelineEvent? {
|
||||
val room = sessionHolder.getSafeActiveSession()?.getRoom(roomId) ?: return latestPreviewableEvent
|
||||
val liveVoiceBroadcastTimelineEvent = getRoomLiveVoiceBroadcastsUseCase.execute(roomId).lastOrNull()
|
||||
?.root?.eventId?.let { room.getTimelineEvent(it) }
|
||||
return latestPreviewableEvent?.takeIf { it.root.getClearType() == EventType.CALL_INVITE }
|
||||
?: liveVoiceBroadcastTimelineEvent
|
||||
?: latestPreviewableEvent
|
||||
?.takeUnless { it.root.asMessageAudioEvent()?.isVoiceBroadcast().orFalse() } // Skip voice messages related to voice broadcast
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.home.room.list.usecase
|
||||
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.features.voicebroadcast.isLive
|
||||
import im.vector.app.features.voicebroadcast.isVoiceBroadcast
|
||||
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
||||
import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase
|
||||
import im.vector.app.features.voicebroadcast.voiceBroadcastId
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.room.Room
|
||||
import org.matrix.android.sdk.api.session.room.getTimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetLatestPreviewableEventUseCase @Inject constructor(
|
||||
private val sessionHolder: ActiveSessionHolder,
|
||||
private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase,
|
||||
) {
|
||||
|
||||
fun execute(roomId: String): TimelineEvent? {
|
||||
val room = sessionHolder.getSafeActiveSession()?.getRoom(roomId) ?: return null
|
||||
val roomSummary = room.roomSummary() ?: return null
|
||||
return getCallEvent(roomSummary)
|
||||
?: getLiveVoiceBroadcastEvent(room)
|
||||
?: getDefaultLatestEvent(room, roomSummary)
|
||||
}
|
||||
|
||||
private fun getCallEvent(roomSummary: RoomSummary): TimelineEvent? {
|
||||
return roomSummary.latestPreviewableEvent
|
||||
?.takeIf { it.root.getClearType() == EventType.CALL_INVITE }
|
||||
}
|
||||
|
||||
private fun getLiveVoiceBroadcastEvent(room: Room): TimelineEvent? {
|
||||
return getRoomLiveVoiceBroadcastsUseCase.execute(room.roomId)
|
||||
.lastOrNull()
|
||||
?.voiceBroadcastId
|
||||
?.let { room.getTimelineEvent(it) }
|
||||
}
|
||||
|
||||
private fun getDefaultLatestEvent(room: Room, roomSummary: RoomSummary): TimelineEvent? {
|
||||
val latestPreviewableEvent = roomSummary.latestPreviewableEvent
|
||||
|
||||
// If the default latest event is a live voice broadcast (paused or resumed), rely to the started event
|
||||
val liveVoiceBroadcastEventId = latestPreviewableEvent?.root?.asVoiceBroadcastEvent()?.takeIf { it.isLive }?.voiceBroadcastId
|
||||
if (liveVoiceBroadcastEventId != null) {
|
||||
return room.getTimelineEvent(liveVoiceBroadcastEventId)
|
||||
}
|
||||
|
||||
return latestPreviewableEvent
|
||||
?.takeUnless { it.root.asMessageAudioEvent()?.isVoiceBroadcast().orFalse() } // Skip voice messages related to voice broadcast
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.app.features.notifications
|
||||
|
||||
import im.vector.app.ActiveSessionDataSource
|
||||
import im.vector.app.features.voicebroadcast.isVoiceBroadcast
|
||||
import im.vector.app.features.voicebroadcast.sequence
|
||||
import org.matrix.android.sdk.api.session.events.model.isVoiceMessage
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.room.getTimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import javax.inject.Inject
|
||||
|
||||
class FilteredEventDetector @Inject constructor(
|
||||
private val activeSessionDataSource: ActiveSessionDataSource
|
||||
) {
|
||||
|
||||
/**
|
||||
* Returns true if the given event should be ignored.
|
||||
* Used to skip notifications if a non expected message is received.
|
||||
*/
|
||||
fun shouldBeIgnored(notifiableEvent: NotifiableEvent): Boolean {
|
||||
val session = activeSessionDataSource.currentValue?.orNull() ?: return false
|
||||
|
||||
if (notifiableEvent is NotifiableMessageEvent) {
|
||||
val room = session.getRoom(notifiableEvent.roomId) ?: return false
|
||||
val timelineEvent = room.getTimelineEvent(notifiableEvent.eventId) ?: return false
|
||||
return timelineEvent.shouldBeIgnored()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the timeline event should be ignored.
|
||||
*/
|
||||
private fun TimelineEvent.shouldBeIgnored(): Boolean {
|
||||
if (root.isVoiceMessage()) {
|
||||
val audioEvent = root.asMessageAudioEvent()
|
||||
// if the event is a voice message related to a voice broadcast, only show the event on the first chunk.
|
||||
return audioEvent.isVoiceBroadcast() && audioEvent?.sequence != 1
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -47,6 +47,7 @@ class NotificationDrawerManager @Inject constructor(
|
|||
private val notifiableEventProcessor: NotifiableEventProcessor,
|
||||
private val notificationRenderer: NotificationRenderer,
|
||||
private val notificationEventPersistence: NotificationEventPersistence,
|
||||
private val filteredEventDetector: FilteredEventDetector,
|
||||
private val buildMeta: BuildMeta,
|
||||
) {
|
||||
|
||||
|
@ -100,6 +101,11 @@ class NotificationDrawerManager @Inject constructor(
|
|||
Timber.d("onNotifiableEventReceived(): is push: ${notifiableEvent.canBeReplaced}")
|
||||
}
|
||||
|
||||
if (filteredEventDetector.shouldBeIgnored(notifiableEvent)) {
|
||||
Timber.d("onNotifiableEventReceived(): ignore the event")
|
||||
return
|
||||
}
|
||||
|
||||
add(notifiableEvent)
|
||||
}
|
||||
|
||||
|
|
|
@ -76,6 +76,8 @@ class RoomProfileActivity :
|
|||
return ActivitySimpleBinding.inflate(layoutInflater)
|
||||
}
|
||||
|
||||
override fun getCoordinatorLayout() = views.coordinatorLayout
|
||||
|
||||
override fun initUiAndData() {
|
||||
sharedActionViewModel = viewModelProvider.get(RoomProfileSharedActionViewModel::class.java)
|
||||
roomProfileArgs = intent?.extras?.getParcelableCompat(Mavericks.KEY_ARG) ?: return
|
||||
|
|
|
@ -18,4 +18,6 @@ package im.vector.app.features.roomprofile.polls
|
|||
|
||||
import im.vector.app.core.platform.VectorViewModelAction
|
||||
|
||||
sealed interface RoomPollsAction : VectorViewModelAction
|
||||
sealed interface RoomPollsAction : VectorViewModelAction {
|
||||
object LoadMorePolls : RoomPollsAction
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
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.hiltMavericksViewModelFactory
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.features.roomprofile.polls.list.domain.GetLoadedPollsStatusUseCase
|
||||
import im.vector.app.features.roomprofile.polls.list.domain.GetPollsUseCase
|
||||
import im.vector.app.features.roomprofile.polls.list.domain.LoadMorePollsUseCase
|
||||
import im.vector.app.features.roomprofile.polls.list.domain.SyncPollsUseCase
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class RoomPollsViewModel @AssistedInject constructor(
|
||||
@Assisted initialState: RoomPollsViewState,
|
||||
private val getPollsUseCase: GetPollsUseCase,
|
||||
private val getLoadedPollsStatusUseCase: GetLoadedPollsStatusUseCase,
|
||||
private val loadMorePollsUseCase: LoadMorePollsUseCase,
|
||||
private val syncPollsUseCase: SyncPollsUseCase,
|
||||
) : VectorViewModel<RoomPollsViewState, RoomPollsAction, RoomPollsViewEvent>(initialState) {
|
||||
|
||||
@AssistedFactory
|
||||
|
@ -39,16 +47,63 @@ class RoomPollsViewModel @AssistedInject constructor(
|
|||
companion object : MavericksViewModelFactory<RoomPollsViewModel, RoomPollsViewState> by hiltMavericksViewModelFactory()
|
||||
|
||||
init {
|
||||
observePolls()
|
||||
val roomId = initialState.roomId
|
||||
updateLoadedPollStatus(roomId)
|
||||
syncPolls(roomId)
|
||||
observePolls(roomId)
|
||||
}
|
||||
|
||||
private fun observePolls() {
|
||||
getPollsUseCase.execute()
|
||||
private fun updateLoadedPollStatus(roomId: String) {
|
||||
val loadedPollsStatus = getLoadedPollsStatusUseCase.execute(roomId)
|
||||
setState {
|
||||
copy(
|
||||
canLoadMore = loadedPollsStatus.canLoadMore,
|
||||
nbLoadedDays = loadedPollsStatus.nbLoadedDays
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun syncPolls(roomId: String) {
|
||||
viewModelScope.launch {
|
||||
setState { copy(isSyncing = true) }
|
||||
val result = runCatching {
|
||||
syncPollsUseCase.execute(roomId)
|
||||
}
|
||||
if (result.isFailure) {
|
||||
_viewEvents.post(RoomPollsViewEvent.LoadingError)
|
||||
}
|
||||
setState { copy(isSyncing = false) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun observePolls(roomId: String) {
|
||||
getPollsUseCase.execute(roomId)
|
||||
.onEach { setState { copy(polls = it) } }
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override fun handle(action: RoomPollsAction) {
|
||||
// do nothing for now
|
||||
when (action) {
|
||||
RoomPollsAction.LoadMorePolls -> handleLoadMore()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLoadMore() = withState { viewState ->
|
||||
viewModelScope.launch {
|
||||
setState { copy(isLoadingMore = true) }
|
||||
val result = runCatching {
|
||||
val status = loadMorePollsUseCase.execute(viewState.roomId)
|
||||
setState {
|
||||
copy(
|
||||
canLoadMore = status.canLoadMore,
|
||||
nbLoadedDays = status.nbLoadedDays,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (result.isFailure) {
|
||||
_viewEvents.post(RoomPollsViewEvent.LoadingError)
|
||||
}
|
||||
setState { copy(isLoadingMore = false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,11 +18,19 @@ package im.vector.app.features.roomprofile.polls
|
|||
|
||||
import com.airbnb.mvrx.MavericksState
|
||||
import im.vector.app.features.roomprofile.RoomProfileArgs
|
||||
import im.vector.app.features.roomprofile.polls.list.ui.PollSummary
|
||||
|
||||
data class RoomPollsViewState(
|
||||
val roomId: String,
|
||||
val polls: List<PollSummary> = emptyList(),
|
||||
val isLoadingMore: Boolean = false,
|
||||
val canLoadMore: Boolean = true,
|
||||
val nbLoadedDays: Int = 0,
|
||||
val isSyncing: Boolean = false,
|
||||
) : MavericksState {
|
||||
|
||||
constructor(roomProfileArgs: RoomProfileArgs) : this(roomId = roomProfileArgs.roomId)
|
||||
|
||||
fun hasNoPolls() = polls.isEmpty()
|
||||
fun hasNoPollsAndCanLoadMore() = !isSyncing && hasNoPolls() && canLoadMore
|
||||
}
|
||||
|
|
|
@ -19,13 +19,17 @@ package im.vector.app.features.roomprofile.polls.active
|
|||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import im.vector.app.R
|
||||
import im.vector.app.features.roomprofile.polls.RoomPollsType
|
||||
import im.vector.app.features.roomprofile.polls.list.RoomPollsListFragment
|
||||
import im.vector.app.features.roomprofile.polls.list.ui.RoomPollsListFragment
|
||||
|
||||
@AndroidEntryPoint
|
||||
class RoomActivePollsFragment : RoomPollsListFragment() {
|
||||
|
||||
override fun getEmptyListTitle(): String {
|
||||
return getString(R.string.room_polls_active_no_item)
|
||||
override fun getEmptyListTitle(canLoadMore: Boolean, nbLoadedDays: Int): String {
|
||||
return if (canLoadMore) {
|
||||
stringProvider.getQuantityString(R.plurals.room_polls_active_no_item_for_loaded_period, nbLoadedDays, nbLoadedDays)
|
||||
} else {
|
||||
getString(R.string.room_polls_active_no_item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRoomPollsType(): RoomPollsType {
|
||||
|
|
|
@ -19,13 +19,17 @@ package im.vector.app.features.roomprofile.polls.ended
|
|||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import im.vector.app.R
|
||||
import im.vector.app.features.roomprofile.polls.RoomPollsType
|
||||
import im.vector.app.features.roomprofile.polls.list.RoomPollsListFragment
|
||||
import im.vector.app.features.roomprofile.polls.list.ui.RoomPollsListFragment
|
||||
|
||||
@AndroidEntryPoint
|
||||
class RoomEndedPollsFragment : RoomPollsListFragment() {
|
||||
|
||||
override fun getEmptyListTitle(): String {
|
||||
return getString(R.string.room_polls_ended_no_item)
|
||||
override fun getEmptyListTitle(canLoadMore: Boolean, nbLoadedDays: Int): String {
|
||||
return if (canLoadMore) {
|
||||
stringProvider.getQuantityString(R.plurals.room_polls_ended_no_item_for_loaded_period, nbLoadedDays, nbLoadedDays)
|
||||
} else {
|
||||
getString(R.string.room_polls_ended_no_item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRoomPollsType(): RoomPollsType {
|
||||
|
|
|
@ -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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -14,23 +14,60 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.roomprofile.polls
|
||||
package im.vector.app.features.roomprofile.polls.list.data
|
||||
|
||||
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState
|
||||
import im.vector.app.features.roomprofile.polls.list.ui.PollSummary
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
class GetPollsUseCase @Inject constructor() {
|
||||
@Singleton
|
||||
class RoomPollDataSource @Inject constructor() {
|
||||
|
||||
fun execute(): Flow<List<PollSummary>> {
|
||||
// TODO unmock and add unit tests
|
||||
return flowOf(getActivePolls() + getEndedPolls())
|
||||
.map { it.sortedByDescending { poll -> poll.creationTimestamp } }
|
||||
private val pollsFlow = MutableSharedFlow<List<PollSummary>>(replay = 1)
|
||||
private val polls = mutableListOf<PollSummary>()
|
||||
private var fakeLoadCounter = 0
|
||||
|
||||
// TODO
|
||||
// unmock using SDK service + add unit tests
|
||||
// after unmock, expose domain layer model (entity) and do the mapping to PollSummary in the UI layer
|
||||
fun getPolls(roomId: String): Flow<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(
|
||||
PollSummary.ActivePoll(
|
||||
id = "id1",
|
||||
|
@ -44,6 +81,11 @@ class GetPollsUseCase @Inject constructor() {
|
|||
creationTimestamp = 1656194400000,
|
||||
title = "Which sport should the pupils do this year?"
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getActivePollsPart2(): List<PollSummary.ActivePoll> {
|
||||
return listOf(
|
||||
PollSummary.ActivePoll(
|
||||
id = "id3",
|
||||
// 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(
|
||||
PollSummary.EndedPoll(
|
||||
id = "id1-ended",
|
||||
|
@ -77,6 +119,11 @@ class GetPollsUseCase @Inject constructor() {
|
|||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getEndedPollsPart2(): List<PollSummary.EndedPoll> {
|
||||
return listOf(
|
||||
PollSummary.EndedPoll(
|
||||
id = "id2-ended",
|
||||
// 2022/06/26 UTC+1
|
||||
|
@ -111,4 +158,17 @@ class GetPollsUseCase @Inject constructor() {
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun syncPolls(roomId: String) {
|
||||
Timber.d("roomId=$roomId")
|
||||
// TODO
|
||||
// unmock using SDK service + add unit tests
|
||||
if (fakeLoadCounter == 0) {
|
||||
// fake first load
|
||||
loadMorePolls(roomId)
|
||||
} else {
|
||||
// fake sync
|
||||
delay(3000)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.roomprofile.polls
|
||||
package im.vector.app.features.roomprofile.polls.list.ui
|
||||
|
||||
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.roomprofile.polls.list
|
||||
package im.vector.app.features.roomprofile.polls.list.ui
|
||||
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
|
@ -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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -14,38 +14,45 @@
|
|||
* 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 im.vector.app.R
|
||||
import im.vector.app.core.date.DateFormatKind
|
||||
import im.vector.app.core.date.VectorDateFormatter
|
||||
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
|
||||
|
||||
class RoomPollsController @Inject constructor(
|
||||
val dateFormatter: VectorDateFormatter,
|
||||
val stringProvider: StringProvider,
|
||||
) : TypedEpoxyController<List<PollSummary>>() {
|
||||
) : TypedEpoxyController<RoomPollsViewState>() {
|
||||
|
||||
interface Listener {
|
||||
fun onPollClicked(pollId: String)
|
||||
fun onLoadMoreClicked()
|
||||
}
|
||||
|
||||
var listener: Listener? = null
|
||||
|
||||
override fun buildModels(data: List<PollSummary>?) {
|
||||
if (data.isNullOrEmpty()) {
|
||||
override fun buildModels(viewState: RoomPollsViewState?) {
|
||||
val polls = viewState?.polls
|
||||
if (polls.isNullOrEmpty() || viewState.isSyncing) {
|
||||
return
|
||||
}
|
||||
|
||||
for (poll in data) {
|
||||
for (poll in polls) {
|
||||
when (poll) {
|
||||
is PollSummary.ActivePoll -> buildActivePollItem(poll)
|
||||
is PollSummary.EndedPoll -> buildEndedPollItem(poll)
|
||||
}
|
||||
}
|
||||
|
||||
if (viewState.canLoadMore) {
|
||||
buildLoadMoreItem(viewState.isLoadingMore)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -16,10 +16,21 @@
|
|||
|
||||
package im.vector.app.features.voicebroadcast
|
||||
|
||||
import android.media.MediaPlayer
|
||||
|
||||
sealed class VoiceBroadcastFailure : Throwable() {
|
||||
sealed class RecordingError : VoiceBroadcastFailure() {
|
||||
object NoPermission : RecordingError()
|
||||
object BlockedBySomeoneElse : RecordingError()
|
||||
object UserAlreadyBroadcasting : RecordingError()
|
||||
}
|
||||
|
||||
sealed class ListeningError : VoiceBroadcastFailure() {
|
||||
/**
|
||||
* @property what the type of error that has occurred, see [MediaPlayer.OnErrorListener.onError].
|
||||
* @property extra an extra code, specific to the error, see [MediaPlayer.OnErrorListener.onError].
|
||||
*/
|
||||
data class UnableToPlay(val what: Int, val extra: Int) : ListeningError()
|
||||
data class DownloadError(override val cause: Throwable?) : ListeningError()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package im.vector.app.features.voicebroadcast.listening
|
||||
|
||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
|
||||
|
||||
interface VoiceBroadcastPlayer {
|
||||
|
@ -26,7 +27,7 @@ interface VoiceBroadcastPlayer {
|
|||
val currentVoiceBroadcast: VoiceBroadcast?
|
||||
|
||||
/**
|
||||
* The current playing [State], [State.IDLE] by default.
|
||||
* The current playing [State], [State.Idle] by default.
|
||||
*/
|
||||
val playingState: State
|
||||
|
||||
|
@ -68,11 +69,12 @@ interface VoiceBroadcastPlayer {
|
|||
/**
|
||||
* Player states.
|
||||
*/
|
||||
enum class State {
|
||||
PLAYING,
|
||||
PAUSED,
|
||||
BUFFERING,
|
||||
IDLE
|
||||
sealed interface State {
|
||||
object Playing : State
|
||||
object Paused : State
|
||||
object Buffering : State
|
||||
data class Error(val failure: VoiceBroadcastFailure.ListeningError) : State
|
||||
object Idle : State
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -24,7 +24,7 @@ import im.vector.app.core.di.ActiveSessionHolder
|
|||
import im.vector.app.core.extensions.onFirst
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
|
||||
import im.vector.app.features.session.coroutineScope
|
||||
import im.vector.app.features.voice.VoiceFailure
|
||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure
|
||||
import im.vector.app.features.voicebroadcast.isLive
|
||||
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Listener
|
||||
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.State
|
||||
|
@ -79,7 +79,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override var playingState = State.IDLE
|
||||
override var playingState: State = State.Idle
|
||||
@MainThread
|
||||
set(value) {
|
||||
if (field != value) {
|
||||
|
@ -96,7 +96,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
val hasChanged = currentVoiceBroadcast != voiceBroadcast
|
||||
when {
|
||||
hasChanged -> startPlayback(voiceBroadcast)
|
||||
playingState == State.PAUSED -> resumePlayback()
|
||||
playingState == State.Paused -> resumePlayback()
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
@ -107,7 +107,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
|
||||
override fun stop() {
|
||||
// Update state
|
||||
playingState = State.IDLE
|
||||
playingState = State.Idle
|
||||
|
||||
// Stop and release media players
|
||||
stopPlayer()
|
||||
|
@ -129,7 +129,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
listeners[voiceBroadcast.voiceBroadcastId]?.add(listener) ?: run {
|
||||
listeners[voiceBroadcast.voiceBroadcastId] = CopyOnWriteArrayList<Listener>().apply { add(listener) }
|
||||
}
|
||||
listener.onPlayingStateChanged(if (voiceBroadcast == currentVoiceBroadcast) playingState else State.IDLE)
|
||||
listener.onPlayingStateChanged(if (voiceBroadcast == currentVoiceBroadcast) playingState else State.Idle)
|
||||
listener.onLiveModeChanged(voiceBroadcast == currentVoiceBroadcast)
|
||||
}
|
||||
|
||||
|
@ -139,11 +139,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
|
||||
private fun startPlayback(voiceBroadcast: VoiceBroadcast) {
|
||||
// Stop listening previous voice broadcast if any
|
||||
if (playingState != State.IDLE) stop()
|
||||
if (playingState != State.Idle) stop()
|
||||
|
||||
currentVoiceBroadcast = voiceBroadcast
|
||||
|
||||
playingState = State.BUFFERING
|
||||
playingState = State.Buffering
|
||||
|
||||
observeVoiceBroadcastStateEvent(voiceBroadcast)
|
||||
}
|
||||
|
@ -175,13 +175,13 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
|
||||
private fun onPlaylistUpdated() {
|
||||
when (playingState) {
|
||||
State.PLAYING,
|
||||
State.PAUSED -> {
|
||||
State.Playing,
|
||||
State.Paused -> {
|
||||
if (nextMediaPlayer == null && !isPreparingNextPlayer) {
|
||||
prepareNextMediaPlayer()
|
||||
}
|
||||
}
|
||||
State.BUFFERING -> {
|
||||
State.Buffering -> {
|
||||
val nextItem = if (isLiveListening && playlist.currentSequence == null) {
|
||||
// live listening, jump to the last item if playback has not started
|
||||
playlist.lastOrNull()
|
||||
|
@ -193,7 +193,8 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
startPlayback(nextItem.startTime)
|
||||
}
|
||||
}
|
||||
State.IDLE -> Unit // Should not happen
|
||||
is State.Error -> Unit
|
||||
State.Idle -> Unit // Should not happen
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -213,18 +214,17 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
if (sequencePosition > 0) {
|
||||
mp.seekTo(sequencePosition)
|
||||
}
|
||||
playingState = State.PLAYING
|
||||
playingState = State.Playing
|
||||
prepareNextMediaPlayer()
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure, "## Voice Broadcast | Unable to start playback: $failure")
|
||||
throw VoiceFailure.UnableToPlay(failure)
|
||||
} catch (failure: VoiceBroadcastFailure.ListeningError.DownloadError) {
|
||||
playingState = State.Error(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun pausePlayback() {
|
||||
playingState = State.PAUSED // This will trigger a playing state update and save the current position
|
||||
playingState = State.Paused // This will trigger a playing state update and save the current position
|
||||
if (currentMediaPlayer != null) {
|
||||
currentMediaPlayer?.pause()
|
||||
} else {
|
||||
|
@ -234,7 +234,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
|
||||
private fun resumePlayback() {
|
||||
if (currentMediaPlayer != null) {
|
||||
playingState = State.PLAYING
|
||||
playingState = State.Playing
|
||||
currentMediaPlayer?.start()
|
||||
} else {
|
||||
val savedPosition = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPlaybackTime(it) } ?: 0
|
||||
|
@ -247,11 +247,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
voiceBroadcast != currentVoiceBroadcast -> {
|
||||
playbackTracker.updatePausedAtPlaybackTime(voiceBroadcast.voiceBroadcastId, positionMillis, positionMillis.toFloat() / duration)
|
||||
}
|
||||
playingState == State.PLAYING || playingState == State.BUFFERING -> {
|
||||
playingState == State.Playing || playingState == State.Buffering -> {
|
||||
updateLiveListeningMode(positionMillis)
|
||||
startPlayback(positionMillis)
|
||||
}
|
||||
playingState == State.IDLE || playingState == State.PAUSED -> {
|
||||
playingState == State.Idle || playingState == State.Paused -> {
|
||||
stopPlayer()
|
||||
playbackTracker.updatePausedAtPlaybackTime(voiceBroadcast.voiceBroadcastId, positionMillis, positionMillis.toFloat() / duration)
|
||||
}
|
||||
|
@ -263,19 +263,29 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
if (nextItem != null) {
|
||||
isPreparingNextPlayer = true
|
||||
sessionScope.launch {
|
||||
prepareMediaPlayer(nextItem.audioEvent.content) { mp ->
|
||||
try {
|
||||
prepareMediaPlayer(nextItem.audioEvent.content) { mp ->
|
||||
isPreparingNextPlayer = false
|
||||
nextMediaPlayer = mp
|
||||
when (playingState) {
|
||||
State.Playing,
|
||||
State.Paused -> {
|
||||
currentMediaPlayer?.setNextMediaPlayer(mp)
|
||||
}
|
||||
State.Buffering -> {
|
||||
mp.start()
|
||||
onNextMediaPlayerStarted(mp)
|
||||
}
|
||||
is State.Error,
|
||||
State.Idle -> stopPlayer()
|
||||
}
|
||||
}
|
||||
} catch (failure: VoiceBroadcastFailure.ListeningError.DownloadError) {
|
||||
isPreparingNextPlayer = false
|
||||
nextMediaPlayer = mp
|
||||
when (playingState) {
|
||||
State.PLAYING,
|
||||
State.PAUSED -> {
|
||||
currentMediaPlayer?.setNextMediaPlayer(mp)
|
||||
}
|
||||
State.BUFFERING -> {
|
||||
mp.start()
|
||||
onNextMediaPlayerStarted(mp)
|
||||
}
|
||||
State.IDLE -> stopPlayer()
|
||||
// Do not change the playingState if the current player is still valid,
|
||||
// the error will be thrown again when switching to the next player
|
||||
if (playingState == State.Buffering || tryOrNull { currentMediaPlayer?.isPlaying } != true) {
|
||||
playingState = State.Error(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -288,11 +298,12 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
session.fileService().downloadFile(messageAudioContent)
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure, "Voice Broadcast | Download has failed: $failure")
|
||||
throw VoiceFailure.UnableToPlay(failure)
|
||||
throw VoiceBroadcastFailure.ListeningError.DownloadError(failure)
|
||||
}
|
||||
|
||||
return audioFile.inputStream().use { fis ->
|
||||
MediaPlayer().apply {
|
||||
setOnErrorListener(mediaPlayerListener)
|
||||
setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
// Do not use CONTENT_TYPE_SPEECH / USAGE_VOICE_COMMUNICATION because we want to play loud here
|
||||
|
@ -302,10 +313,9 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
)
|
||||
setDataSource(fis.fd)
|
||||
setOnInfoListener(mediaPlayerListener)
|
||||
setOnErrorListener(mediaPlayerListener)
|
||||
setOnPreparedListener(onPreparedListener)
|
||||
setOnCompletionListener(mediaPlayerListener)
|
||||
prepare()
|
||||
prepareAsync()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -327,11 +337,18 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
currentVoiceBroadcast?.voiceBroadcastId?.let { voiceBroadcastId ->
|
||||
// Start or stop playback ticker
|
||||
when (playingState) {
|
||||
State.PLAYING -> playbackTicker.startPlaybackTicker(voiceBroadcastId)
|
||||
State.PAUSED,
|
||||
State.BUFFERING,
|
||||
State.IDLE -> playbackTicker.stopPlaybackTicker(voiceBroadcastId)
|
||||
State.Playing -> playbackTicker.startPlaybackTicker(voiceBroadcastId)
|
||||
State.Paused,
|
||||
State.Buffering,
|
||||
is State.Error,
|
||||
State.Idle -> playbackTicker.stopPlaybackTicker(voiceBroadcastId)
|
||||
}
|
||||
|
||||
// Notify playback tracker about error
|
||||
if (playingState is State.Error) {
|
||||
playbackTracker.onError(voiceBroadcastId, playingState.failure)
|
||||
}
|
||||
|
||||
// Notify state change to all the listeners attached to the current voice broadcast id
|
||||
listeners[voiceBroadcastId]?.forEach { listener -> listener.onPlayingStateChanged(playingState) }
|
||||
}
|
||||
|
@ -348,7 +365,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
// the current voice broadcast is not live (ended)
|
||||
mostRecentVoiceBroadcastEvent?.isLive != true -> false
|
||||
// the player is stopped or paused
|
||||
playingState == State.IDLE || playingState == State.PAUSED -> false
|
||||
playingState == State.Idle || playingState == State.Paused -> false
|
||||
seekPosition != null -> {
|
||||
val seekDirection = seekPosition.compareTo(getCurrentPlaybackPosition() ?: 0)
|
||||
val newSequence = playlist.findByPosition(seekPosition)?.sequence
|
||||
|
@ -374,13 +391,14 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
|
||||
private fun onLiveListeningChanged(isLiveListening: Boolean) {
|
||||
// Live has ended and last chunk has been reached, we can stop the playback
|
||||
if (!isLiveListening && playingState == State.BUFFERING && playlist.currentSequence == mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence) {
|
||||
val hasReachedLastChunk = playlist.currentSequence == mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence
|
||||
if (!isLiveListening && playingState == State.Buffering && hasReachedLastChunk) {
|
||||
stop()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onNextMediaPlayerStarted(mp: MediaPlayer) {
|
||||
playingState = State.PLAYING
|
||||
playingState = State.Playing
|
||||
playlist.currentSequence = playlist.currentSequence?.inc()
|
||||
currentMediaPlayer = mp
|
||||
nextMediaPlayer = null
|
||||
|
@ -389,16 +407,16 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
|
||||
private fun getCurrentPlaybackPosition(): Int? {
|
||||
val voiceBroadcastId = currentVoiceBroadcast?.voiceBroadcastId ?: return null
|
||||
val computedPosition = currentMediaPlayer?.currentPosition?.let { playlist.currentItem?.startTime?.plus(it) }
|
||||
val computedPosition = tryOrNull { currentMediaPlayer?.currentPosition }?.let { playlist.currentItem?.startTime?.plus(it) }
|
||||
val savedPosition = playbackTracker.getPlaybackTime(voiceBroadcastId)
|
||||
return computedPosition ?: savedPosition
|
||||
}
|
||||
|
||||
private fun getCurrentPlaybackPercentage(): Float? {
|
||||
val playlistPosition = playlist.currentItem?.startTime
|
||||
val computedPosition = currentMediaPlayer?.currentPosition?.let { playlistPosition?.plus(it) } ?: playlistPosition
|
||||
val duration = playlist.duration.takeIf { it > 0 }
|
||||
val computedPercentage = if (computedPosition != null && duration != null) computedPosition.toFloat() / duration else null
|
||||
val computedPosition = tryOrNull { currentMediaPlayer?.currentPosition }?.let { playlistPosition?.plus(it) } ?: playlistPosition
|
||||
val duration = playlist.duration
|
||||
val computedPercentage = if (computedPosition != null && duration > 0) computedPosition.toFloat() / duration else null
|
||||
val savedPercentage = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPercentage(it) }
|
||||
return computedPercentage ?: savedPercentage
|
||||
}
|
||||
|
@ -416,6 +434,14 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onCompletion(mp: MediaPlayer) {
|
||||
// Release media player as soon as it completed
|
||||
mp.release()
|
||||
if (currentMediaPlayer == mp) {
|
||||
currentMediaPlayer = null
|
||||
} else {
|
||||
error("The media player which has completed mismatches the current media player instance.")
|
||||
}
|
||||
|
||||
// Next media player is already attached to this player and will start playing automatically
|
||||
if (nextMediaPlayer != null) return
|
||||
|
||||
|
@ -426,15 +452,18 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
// We'll not receive new chunks anymore so we can stop the live listening
|
||||
stop()
|
||||
} else {
|
||||
// Enter in buffering mode and release current media player
|
||||
playingState = State.BUFFERING
|
||||
currentMediaPlayer?.release()
|
||||
currentMediaPlayer = null
|
||||
playingState = State.Buffering
|
||||
prepareNextMediaPlayer()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(mp: MediaPlayer, what: Int, extra: Int): Boolean {
|
||||
stop()
|
||||
Timber.d("## Voice Broadcast | onError: what=$what, extra=$extra")
|
||||
// Do not change the playingState if the current player is still valid,
|
||||
// the error will be thrown again when switching to the next player
|
||||
if (playingState == State.Buffering || tryOrNull { currentMediaPlayer?.isPlaying } != true) {
|
||||
playingState = State.Error(VoiceBroadcastFailure.ListeningError.UnableToPlay(what, extra))
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -462,24 +491,25 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
val playbackTime = getCurrentPlaybackPosition()
|
||||
val percentage = getCurrentPlaybackPercentage()
|
||||
when (playingState) {
|
||||
State.PLAYING -> {
|
||||
State.Playing -> {
|
||||
if (playbackTime != null && percentage != null) {
|
||||
playbackTracker.updatePlayingAtPlaybackTime(id, playbackTime, percentage)
|
||||
}
|
||||
}
|
||||
State.PAUSED,
|
||||
State.BUFFERING -> {
|
||||
State.Paused,
|
||||
State.Buffering -> {
|
||||
if (playbackTime != null && percentage != null) {
|
||||
playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage)
|
||||
}
|
||||
}
|
||||
State.IDLE -> {
|
||||
State.Idle -> {
|
||||
if (playbackTime == null || percentage == null || (playlist.duration - playbackTime) < 50) {
|
||||
playbackTracker.stopPlayback(id)
|
||||
} else {
|
||||
playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage)
|
||||
}
|
||||
}
|
||||
is State.Error -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,14 +19,20 @@ package im.vector.app.features.voicebroadcast.usecase
|
|||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
||||
import im.vector.app.features.voicebroadcast.isLive
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
|
||||
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
||||
import im.vector.app.features.voicebroadcast.voiceBroadcastId
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Get the list of live (not ended) voice broadcast events in the given room.
|
||||
*/
|
||||
class GetRoomLiveVoiceBroadcastsUseCase @Inject constructor(
|
||||
private val activeSessionHolder: ActiveSessionHolder,
|
||||
private val getVoiceBroadcastStateEventUseCase: GetVoiceBroadcastStateEventUseCase,
|
||||
) {
|
||||
|
||||
fun execute(roomId: String): List<VoiceBroadcastEvent> {
|
||||
|
@ -37,7 +43,8 @@ class GetRoomLiveVoiceBroadcastsUseCase @Inject constructor(
|
|||
setOf(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO),
|
||||
QueryStringValue.IsNotEmpty
|
||||
)
|
||||
.mapNotNull { it.asVoiceBroadcastEvent() }
|
||||
.mapNotNull { stateEvent -> stateEvent.asVoiceBroadcastEvent()?.voiceBroadcastId }
|
||||
.mapNotNull { voiceBroadcastId -> getVoiceBroadcastStateEventUseCase.execute(VoiceBroadcast(voiceBroadcastId, roomId)) }
|
||||
.filter { it.isLive }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,6 @@ import kotlinx.coroutines.flow.onStart
|
|||
import kotlinx.coroutines.flow.transformWhile
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.room.Room
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
|
@ -44,6 +43,7 @@ import javax.inject.Inject
|
|||
|
||||
class GetVoiceBroadcastStateEventLiveUseCase @Inject constructor(
|
||||
private val session: Session,
|
||||
private val getVoiceBroadcastStateEventUseCase: GetVoiceBroadcastStateEventUseCase,
|
||||
) {
|
||||
|
||||
fun execute(voiceBroadcast: VoiceBroadcast): Flow<Optional<VoiceBroadcastEvent>> {
|
||||
|
@ -93,7 +93,7 @@ class GetVoiceBroadcastStateEventLiveUseCase @Inject constructor(
|
|||
* Get a flow of the most recent related event.
|
||||
*/
|
||||
private fun getMostRecentRelatedEventFlow(room: Room, voiceBroadcast: VoiceBroadcast): Flow<Optional<VoiceBroadcastEvent>> {
|
||||
val mostRecentEvent = getMostRecentRelatedEvent(room, voiceBroadcast).toOptional()
|
||||
val mostRecentEvent = getVoiceBroadcastStateEventUseCase.execute(voiceBroadcast).toOptional()
|
||||
return if (mostRecentEvent.hasValue()) {
|
||||
val stateKey = mostRecentEvent.get().root.stateKey.orEmpty()
|
||||
// observe incoming voice broadcast state events
|
||||
|
@ -141,15 +141,6 @@ class GetVoiceBroadcastStateEventLiveUseCase @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the most recent event related to the given voice broadcast.
|
||||
*/
|
||||
private fun getMostRecentRelatedEvent(room: Room, voiceBroadcast: VoiceBroadcast): VoiceBroadcastEvent? {
|
||||
return room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId)
|
||||
.mapNotNull { timelineEvent -> timelineEvent.root.asVoiceBroadcastEvent()?.takeUnless { it.root.isRedacted() } }
|
||||
.maxByOrNull { it.root.originServerTs ?: 0 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a flow of the given voice broadcast event changes.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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.voicebroadcast.usecase
|
||||
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
|
||||
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
||||
import im.vector.app.features.voicebroadcast.voiceBroadcastId
|
||||
import org.matrix.android.sdk.api.extensions.orTrue
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.room.Room
|
||||
import org.matrix.android.sdk.api.session.room.getTimelineEvent
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetVoiceBroadcastStateEventUseCase @Inject constructor(
|
||||
private val session: Session,
|
||||
) {
|
||||
|
||||
fun execute(voiceBroadcast: VoiceBroadcast): VoiceBroadcastEvent? {
|
||||
val room = session.getRoom(voiceBroadcast.roomId) ?: error("Unknown roomId: ${voiceBroadcast.roomId}")
|
||||
return getMostRecentRelatedEvent(room, voiceBroadcast)
|
||||
.also { event ->
|
||||
Timber.d(
|
||||
"## VoiceBroadcast | " +
|
||||
"voiceBroadcastId=${event?.voiceBroadcastId}, " +
|
||||
"state=${event?.content?.voiceBroadcastState}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the most recent event related to the given voice broadcast.
|
||||
*/
|
||||
private fun getMostRecentRelatedEvent(room: Room, voiceBroadcast: VoiceBroadcast): VoiceBroadcastEvent? {
|
||||
val startedEvent = room.getTimelineEvent(voiceBroadcast.voiceBroadcastId)
|
||||
return if (startedEvent?.root?.isRedacted().orTrue()) {
|
||||
null
|
||||
} else {
|
||||
room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId)
|
||||
.mapNotNull { timelineEvent -> timelineEvent.root.asVoiceBroadcastEvent() }
|
||||
.filterNot { it.root.isRedacted() }
|
||||
.maxByOrNull { it.root.originServerTs ?: 0 }
|
||||
}
|
||||
}
|
||||
}
|
10
vector/src/main/res/drawable/ic_voice_broadcast_error.xml
Normal file
10
vector/src/main/res/drawable/ic_voice_broadcast_error.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:pathData="M8,16C12.418,16 16,12.418 16,8C16,3.582 12.418,0 8,0C3.582,0 0,3.582 0,8C0,12.418 3.582,16 8,16ZM6.777,4.135C6.717,3.451 7.221,2.851 7.905,2.803C8.577,2.755 9.177,3.259 9.249,3.943V4.135L8.865,8.935C8.829,9.379 8.457,9.715 8.013,9.715H7.941C7.521,9.679 7.197,9.355 7.161,8.935L6.777,4.135ZM9.056,12.067C9.056,12.651 8.583,13.123 8,13.123C7.417,13.123 6.944,12.651 6.944,12.067C6.944,11.484 7.417,11.011 8,11.011C8.583,11.011 9.056,11.484 9.056,12.067Z"
|
||||
android:fillColor="#FF5B55"
|
||||
android:fillType="evenOdd" />
|
||||
</vector>
|
|
@ -17,6 +17,34 @@
|
|||
tools:itemCount="5"
|
||||
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
|
||||
android:id="@+id/roomPollsEmptyTitle"
|
||||
android:layout_width="0dp"
|
||||
|
@ -26,14 +54,39 @@
|
|||
android:gravity="center"
|
||||
android:textAppearance="@style/TextAppearance.Vector.Body"
|
||||
android:textColor="?vctr_content_secondary"
|
||||
android:textSize="17sp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="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" />
|
||||
|
||||
<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
|
||||
android:id="@+id/roomPollsEmptyGuideline"
|
||||
android:id="@+id/roomPollsTitleGuideline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
|
|
|
@ -85,6 +85,7 @@
|
|||
app:layout_constraintCircle="@id/breadcrumbsImageView"
|
||||
app:layout_constraintCircleAngle="225"
|
||||
app:layout_constraintCircleRadius="28dp"
|
||||
app:tint="?vctr_content_primary"
|
||||
tools:ignore="MissingConstraints"
|
||||
tools:visibility="visible" />
|
||||
|
||||
|
|
30
vector/src/main/res/layout/item_poll_load_more.xml
Normal file
30
vector/src/main/res/layout/item_poll_load_more.xml
Normal file
|
@ -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>
|
|
@ -176,4 +176,27 @@
|
|||
tools:ignore="NegativeMargin"
|
||||
tools:text="-0:12" />
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/controlsGroup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:constraint_referenced_ids="controllerButtonsFlow,seekBar,elapsedTime,remainingTime" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/errorView"
|
||||
style="@style/Widget.Vector.TextView.Caption"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:drawablePadding="4dp"
|
||||
android:text="@string/error_voice_broadcast_unable_to_play"
|
||||
android:textColor="?colorError"
|
||||
android:visibility="gone"
|
||||
app:drawableStartCompat="@drawable/ic_voice_broadcast_error"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/headerBottomBarrier"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</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
|
||||
fun `given a sent poll when undisclosed poll type is selected then poll is votable and option states are PollUndisclosed`() {
|
||||
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
|
||||
fun `given a sent poll when poll type is disclosed then poll is votable and option view states are PollReady`() {
|
||||
val stringProvider = FakeStringProvider()
|
||||
|
|
|
@ -0,0 +1,196 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.home.room.list.usecase
|
||||
|
||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants.VOICE_BROADCAST_CHUNK_KEY
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
||||
import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase
|
||||
import im.vector.app.test.fakes.FakeActiveSessionHolder
|
||||
import im.vector.app.test.fakes.FakeRoom
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.amshove.kluent.shouldBe
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.amshove.kluent.shouldBeNull
|
||||
import org.junit.Before
|
||||
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.getRoom
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
|
||||
private const val A_ROOM_ID = "a-room-id"
|
||||
|
||||
internal class GetLatestPreviewableEventUseCaseTest {
|
||||
|
||||
private val fakeRoom = FakeRoom()
|
||||
private val fakeSessionHolder = FakeActiveSessionHolder()
|
||||
private val fakeRoomSummary = mockk<RoomSummary>()
|
||||
private val fakeGetRoomLiveVoiceBroadcastsUseCase = mockk<GetRoomLiveVoiceBroadcastsUseCase>()
|
||||
|
||||
private val getLatestPreviewableEventUseCase = GetLatestPreviewableEventUseCase(
|
||||
fakeSessionHolder.instance,
|
||||
fakeGetRoomLiveVoiceBroadcastsUseCase,
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
every { fakeSessionHolder.instance.getSafeActiveSession()?.getRoom(A_ROOM_ID) } returns fakeRoom
|
||||
every { fakeRoom.roomSummary() } returns fakeRoomSummary
|
||||
every { fakeRoom.roomId } returns A_ROOM_ID
|
||||
every { fakeRoom.timelineService().getTimelineEvent(any()) } answers {
|
||||
mockk(relaxed = true) {
|
||||
every { eventId } returns firstArg()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given the latest event is a call invite and there is a live broadcast, when execute, returns the call event`() {
|
||||
// Given
|
||||
val aLatestPreviewableEvent = mockk<TimelineEvent> {
|
||||
every { root.type } returns EventType.MESSAGE
|
||||
every { root.getClearType() } returns EventType.CALL_INVITE
|
||||
}
|
||||
every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent
|
||||
every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns listOf(
|
||||
givenAVoiceBroadcastEvent("id1", VoiceBroadcastState.STARTED, "id1"),
|
||||
givenAVoiceBroadcastEvent("id2", VoiceBroadcastState.RESUMED, "id1"),
|
||||
).mapNotNull { it.asVoiceBroadcastEvent() }
|
||||
|
||||
// When
|
||||
val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID)
|
||||
|
||||
// Then
|
||||
result shouldBe aLatestPreviewableEvent
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given the latest event is not a call invite and there is a live broadcast, when execute, returns the latest broadcast event`() {
|
||||
// Given
|
||||
val aLatestPreviewableEvent = mockk<TimelineEvent> {
|
||||
every { root.type } returns EventType.MESSAGE
|
||||
every { root.getClearType() } returns EventType.MESSAGE
|
||||
}
|
||||
every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent
|
||||
every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns listOf(
|
||||
givenAVoiceBroadcastEvent("id1", VoiceBroadcastState.STARTED, "vb_id1"),
|
||||
givenAVoiceBroadcastEvent("id2", VoiceBroadcastState.RESUMED, "vb_id2"),
|
||||
).mapNotNull { it.asVoiceBroadcastEvent() }
|
||||
|
||||
// When
|
||||
val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID)
|
||||
|
||||
// Then
|
||||
result?.eventId shouldBeEqualTo "vb_id2"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given there is no live broadcast, when execute, returns the latest event`() {
|
||||
// Given
|
||||
val aLatestPreviewableEvent = mockk<TimelineEvent> {
|
||||
every { root.type } returns EventType.MESSAGE
|
||||
every { root.getClearType() } returns EventType.MESSAGE
|
||||
}
|
||||
every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent
|
||||
every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns emptyList()
|
||||
|
||||
// When
|
||||
val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID)
|
||||
|
||||
// Then
|
||||
result shouldBe aLatestPreviewableEvent
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given there is no live broadcast and the latest event is a vb message, when execute, returns null`() {
|
||||
// Given
|
||||
val aLatestPreviewableEvent = mockk<TimelineEvent> {
|
||||
every { root.type } returns EventType.MESSAGE
|
||||
every { root.getClearType() } returns EventType.MESSAGE
|
||||
every { root.getClearContent() } returns mapOf(
|
||||
MessageContent.MSG_TYPE_JSON_KEY to "m.audio",
|
||||
VOICE_BROADCAST_CHUNK_KEY to "1",
|
||||
"body" to "",
|
||||
)
|
||||
}
|
||||
every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent
|
||||
every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns emptyList()
|
||||
|
||||
// When
|
||||
val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID)
|
||||
|
||||
// Then
|
||||
result.shouldBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given the latest event is an ended vb, when execute, returns the stopped event`() {
|
||||
// Given
|
||||
val aLatestPreviewableEvent = mockk<TimelineEvent> {
|
||||
every { eventId } returns "id1"
|
||||
every { root } returns givenAVoiceBroadcastEvent("id1", VoiceBroadcastState.STOPPED, "vb_id1")
|
||||
}
|
||||
every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent
|
||||
every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns emptyList()
|
||||
|
||||
// When
|
||||
val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID)
|
||||
|
||||
// Then
|
||||
result?.eventId shouldBeEqualTo "id1"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given the latest event is a resumed vb, when execute, returns the started event`() {
|
||||
// Given
|
||||
val aLatestPreviewableEvent = mockk<TimelineEvent> {
|
||||
every { eventId } returns "id1"
|
||||
every { root } returns givenAVoiceBroadcastEvent("id1", VoiceBroadcastState.RESUMED, "vb_id1")
|
||||
}
|
||||
every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent
|
||||
every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns emptyList()
|
||||
|
||||
// When
|
||||
val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID)
|
||||
|
||||
// Then
|
||||
result?.eventId shouldBeEqualTo "vb_id1"
|
||||
}
|
||||
|
||||
private fun givenAVoiceBroadcastEvent(
|
||||
eventId: String,
|
||||
state: VoiceBroadcastState,
|
||||
voiceBroadcastId: String,
|
||||
): Event = mockk {
|
||||
every { this@mockk.eventId } returns eventId
|
||||
every { getClearType() } returns VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO
|
||||
every { type } returns VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO
|
||||
every { content } returns mapOf(
|
||||
"state" to state.value,
|
||||
"m.relates_to" to mapOf(
|
||||
"rel_type" to RelationType.REFERENCE,
|
||||
"event_id" to voiceBroadcastId
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -17,8 +17,17 @@
|
|||
package im.vector.app.features.roomprofile.polls
|
||||
|
||||
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.testDispatcher
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coJustRun
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
|
@ -26,7 +35,7 @@ import kotlinx.coroutines.flow.flowOf
|
|||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
private const val ROOM_ID = "room-id"
|
||||
private const val A_ROOM_ID = "room-id"
|
||||
|
||||
class RoomPollsViewModelTest {
|
||||
|
||||
|
@ -34,21 +43,33 @@ class RoomPollsViewModelTest {
|
|||
val mavericksTestRule = MavericksTestRule(testDispatcher = testDispatcher)
|
||||
|
||||
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 {
|
||||
return RoomPollsViewModel(
|
||||
initialState = initialState,
|
||||
getPollsUseCase = fakeGetPollsUseCase,
|
||||
getLoadedPollsStatusUseCase = fakeGetLoadedPollsStatusUseCase,
|
||||
loadMorePollsUseCase = fakeLoadMorePollsUseCase,
|
||||
syncPollsUseCase = fakeSyncPollsUseCase,
|
||||
)
|
||||
}
|
||||
|
||||
@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
|
||||
val loadedPollsStatus = givenGetLoadedPollsStatusSuccess()
|
||||
givenSyncPollsWithSuccess()
|
||||
val polls = listOf(givenAPollSummary())
|
||||
every { fakeGetPollsUseCase.execute() } returns flowOf(polls)
|
||||
val expectedViewState = initialState.copy(polls = polls)
|
||||
every { fakeGetPollsUseCase.execute(A_ROOM_ID) } returns flowOf(polls)
|
||||
val expectedViewState = initialState.copy(
|
||||
polls = polls,
|
||||
canLoadMore = loadedPollsStatus.canLoadMore,
|
||||
nbLoadedDays = loadedPollsStatus.nbLoadedDays,
|
||||
)
|
||||
|
||||
// When
|
||||
val viewModel = createViewModel()
|
||||
|
@ -59,11 +80,88 @@ class RoomPollsViewModelTest {
|
|||
.assertLatestState(expectedViewState)
|
||||
.finish()
|
||||
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 {
|
||||
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) }
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue