diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml
index 8752f339bd..4901a84070 100644
--- a/.github/workflows/danger.yml
+++ b/.github/workflows/danger.yml
@@ -11,7 +11,7 @@ jobs:
- run: |
npm install --save-dev @babel/plugin-transform-flow-strip-types
- name: Danger
- uses: danger/danger-js@11.2.0
+ uses: danger/danger-js@11.2.1
with:
args: "--dangerfile ./tools/danger/dangerfile.js"
env:
diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml
index fae8d97688..c32cb65c42 100644
--- a/.github/workflows/quality.yml
+++ b/.github/workflows/quality.yml
@@ -66,7 +66,7 @@ jobs:
yarn add danger-plugin-lint-report --dev
- name: Danger lint
if: always()
- uses: danger/danger-js@11.2.0
+ uses: danger/danger-js@11.2.1
with:
args: "--dangerfile ./tools/danger/dangerfile-lint.js"
env:
diff --git a/CHANGES.md b/CHANGES.md
index e742d79c1e..15b0a76b23 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,30 @@
+Changes in Element v1.5.20 (2023-01-10)
+=======================================
+
+Features ✨
+----------
+ - "[Rich text editor] Add list formatting buttons to the rich text editor" ([#7887](https://github.com/vector-im/element-android/issues/7887))
+
+Bugfixes 🐛
+----------
+ - ReplyTo are not updated if the original message is edited or deleted. ([#5546](https://github.com/vector-im/element-android/issues/5546))
+ - Observe ViewEvents only when resumed and ensure ViewEvents are not lost. ([#7724](https://github.com/vector-im/element-android/issues/7724))
+ - [Session manager] Missing info when a session does not support encryption ([#7853](https://github.com/vector-im/element-android/issues/7853))
+ - Reduce number of crypto database transactions when handling the sync response ([#7879](https://github.com/vector-im/element-android/issues/7879))
+ - [Voice Broadcast] Stop listening if we reach the last received chunk and there is no last sequence number ([#7899](https://github.com/vector-im/element-android/issues/7899))
+ - Handle network error on API `rooms/{roomId}/threads` ([#7913](https://github.com/vector-im/element-android/issues/7913))
+
+In development 🚧
+----------------
+ - [Poll] Render active polls list of a room
+ - [Poll] Render past polls list of a room ([#7864](https://github.com/vector-im/element-android/issues/7864))
+
+Other changes
+-------------
+ - fix: increase font size for messages ([#5717](https://github.com/vector-im/element-android/issues/5717))
+ - Add trim to username input on the app side and SDK side when sign-in ([#7111](https://github.com/vector-im/element-android/issues/7111))
+
+
Changes in Element v1.5.18 (2023-01-02)
=======================================
diff --git a/Gemfile.lock b/Gemfile.lock
index 276f4ae66a..33ebbc1b70 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -127,7 +127,8 @@ GEM
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
gh_inspector (1.1.3)
- git (1.11.0)
+ git (1.13.0)
+ addressable (~> 2.8)
rchardet (~> 1.8)
google-apis-androidpublisher_v3 (0.25.0)
google-apis-core (>= 0.7, < 2.a)
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000000..3126b47a07
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,5 @@
+# Reporting a Vulnerability
+
+**If you've found a security vulnerability, please report it to security@matrix.org**
+
+For more information on our security disclosure policy, visit https://www.matrix.org/security-disclosure-policy/
diff --git a/build.gradle b/build.gradle
index a425f6f02e..70d146e8e0 100644
--- a/build.gradle
+++ b/build.gradle
@@ -27,8 +27,8 @@ buildscript {
classpath 'com.google.firebase:firebase-appdistribution-gradle:3.1.1'
classpath 'com.google.gms:google-services:4.3.14'
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.5.0.2730'
- classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5'
- classpath "com.likethesalad.android:stem-plugin:2.2.3"
+ classpath 'com.google.android.gms:oss-licenses-plugin:0.10.6'
+ classpath "com.likethesalad.android:stem-plugin:2.3.0"
classpath 'org.owasp:dependency-check-gradle:7.4.4'
classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.7.20"
classpath "org.jetbrains.kotlinx:kotlinx-knit:0.4.0"
@@ -45,10 +45,10 @@ 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.17.0"
+ id 'com.autonomousapps.dependency-analysis' version "1.18.0"
// Gradle doctor
id "com.osacky.doctor" version "0.8.1"
}
diff --git a/changelog.d/5546.bugfix b/changelog.d/5546.bugfix
deleted file mode 100644
index a3ff48a4a2..0000000000
--- a/changelog.d/5546.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-ReplyTo are not updated if the original message is edited or deleted.
diff --git a/changelog.d/7724.bugfix b/changelog.d/7724.bugfix
deleted file mode 100644
index 685f7ad4e2..0000000000
--- a/changelog.d/7724.bugfix
+++ /dev/null
@@ -1 +0,0 @@
- Observe ViewEvents only when resumed and ensure ViewEvents are not lost.
diff --git a/changelog.d/7832.bugfix b/changelog.d/7832.bugfix
new file mode 100644
index 0000000000..871f9aabb9
--- /dev/null
+++ b/changelog.d/7832.bugfix
@@ -0,0 +1 @@
+[Voice Broadcast] Fix unexpected "live broadcast" in the room list
diff --git a/changelog.d/7853.bugfix b/changelog.d/7853.bugfix
deleted file mode 100644
index 885233553e..0000000000
--- a/changelog.d/7853.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-[Session manager] Missing info when a session does not support encryption
diff --git a/changelog.d/7864.wip b/changelog.d/7864.wip
deleted file mode 100644
index e1187ee1e7..0000000000
--- a/changelog.d/7864.wip
+++ /dev/null
@@ -1,2 +0,0 @@
-[Poll] Render active polls list of a room
-[Poll] Render past polls list of a room
diff --git a/changelog.d/7879.bugfix b/changelog.d/7879.bugfix
deleted file mode 100644
index be828ec2cc..0000000000
--- a/changelog.d/7879.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-Reduce number of crypto database transactions when handling the sync response
diff --git a/changelog.d/7887.feature b/changelog.d/7887.feature
deleted file mode 100644
index 1f1c29761a..0000000000
--- a/changelog.d/7887.feature
+++ /dev/null
@@ -1 +0,0 @@
-"[Rich text editor] Add list formatting buttons to the rich text editor"
\ No newline at end of file
diff --git a/changelog.d/7899.bugfix b/changelog.d/7899.bugfix
deleted file mode 100644
index d95af29d8d..0000000000
--- a/changelog.d/7899.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-[Voice Broadcast] Stop listening if we reach the last received chunk and there is no last sequence number
diff --git a/changelog.d/7900.feature b/changelog.d/7900.feature
new file mode 100644
index 0000000000..c3cce1e0e6
--- /dev/null
+++ b/changelog.d/7900.feature
@@ -0,0 +1 @@
+Render ended polls
diff --git a/changelog.d/7913.bugfix b/changelog.d/7913.bugfix
deleted file mode 100644
index 32b821f14d..0000000000
--- a/changelog.d/7913.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-Handle network error on API `rooms/{roomId}/threads`
diff --git a/changelog.d/7930.feature b/changelog.d/7930.feature
new file mode 100644
index 0000000000..7eb779e6ec
--- /dev/null
+++ b/changelog.d/7930.feature
@@ -0,0 +1 @@
+"[Rich text editor] Update list item bullet appearance"
\ No newline at end of file
diff --git a/changelog.d/7936.misc b/changelog.d/7936.misc
new file mode 100644
index 0000000000..8480d9a6bf
--- /dev/null
+++ b/changelog.d/7936.misc
@@ -0,0 +1 @@
+Upgrade to Kotlin 1.8
diff --git a/coverage.gradle b/coverage.gradle
index 94a6d097e5..869611ce07 100644
--- a/coverage.gradle
+++ b/coverage.gradle
@@ -80,12 +80,12 @@ task generateCoverageReport(type: JacocoReport) {
task unitTestsWithCoverage(type: GradleBuild) {
// the 7.1.3 android gradle plugin has a bug where enableTestCoverage generates invalid coverage
- startParameter.projectProperties.coverage = [enableTestCoverage: false]
+ startParameter.projectProperties.coverage = "false"
tasks = ['testDebugUnitTest']
}
task instrumentationTestsWithCoverage(type: GradleBuild) {
- startParameter.projectProperties.coverage = [enableTestCoverage: true]
+ startParameter.projectProperties.coverage = "true"
startParameter.projectProperties['android.testInstrumentationRunnerArguments.notPackage'] = 'im.vector.app.ui'
tasks = [':vector-app:connectedGplayKotlinCryptoDebugAndroidTest', ':vector:connectedKotlinCryptoDebugAndroidTest', 'matrix-sdk-android:connectedKotlinCryptoDebugAndroidTest']
}
diff --git a/dependencies.gradle b/dependencies.gradle
index e970457e7c..8b0933b943 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -8,7 +8,7 @@ ext.versions = [
def gradle = "7.3.1"
// Ref: https://kotlinlang.org/releases.html
-def kotlin = "1.7.22"
+def kotlin = "1.8.0"
def kotlinCoroutines = "1.6.4"
def dagger = "2.44.2"
def firebaseBom = "31.1.1"
@@ -18,7 +18,7 @@ def markwon = "4.6.2"
def moshi = "1.14.0"
def lifecycle = "2.5.1"
def flowBinding = "1.2.0"
-def flipper = "0.176.0"
+def flipper = "0.176.1"
def epoxy = "5.0.0"
def mavericks = "3.0.1"
def glide = "4.14.2"
@@ -27,12 +27,13 @@ def jjwt = "0.11.5"
// Temporary version to unblock #6929. Once 0.16.0 is released we should use it, and revert
// the whole commit which set version 0.16.0-SNAPSHOT
def vanniktechEmoji = "0.16.0-SNAPSHOT"
-def sentry = "6.9.2"
-def fragment = "1.5.5"
+def sentry = "6.11.0"
+// 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"
@@ -56,11 +57,12 @@ ext.libs = [
'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",
'preferenceKtx' : "androidx.preference:preference-ktx:1.2.0",
- 'junit' : "androidx.test.ext:junit:1.1.3",
+ 'junit' : "androidx.test.ext:junit:1.1.5",
'lifecycleCommon' : "androidx.lifecycle:lifecycle-common:$lifecycle",
'lifecycleLivedata' : "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle",
'lifecycleProcess' : "androidx.lifecycle:lifecycle-process:$lifecycle",
@@ -86,7 +88,7 @@ ext.libs = [
'appdistributionApi' : "com.google.firebase:firebase-appdistribution-api-ktx:$appDistribution",
'appdistribution' : "com.google.firebase:firebase-appdistribution:$appDistribution",
// Phone number https://github.com/google/libphonenumber
- 'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.13.3"
+ 'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.13.4"
],
dagger : [
'dagger' : "com.google.dagger:dagger:$dagger",
@@ -101,7 +103,7 @@ ext.libs = [
],
element : [
'opusencoder' : "io.element.android:opusencoder:1.1.0",
- 'wysiwyg' : "io.element.android:wysiwyg:0.14.0"
+ 'wysiwyg' : "io.element.android:wysiwyg:0.15.0"
],
squareup : [
'moshi' : "com.squareup.moshi:moshi:$moshi",
diff --git a/fastlane/metadata/android/en-US/changelogs/40105200.txt b/fastlane/metadata/android/en-US/changelogs/40105200.txt
new file mode 100644
index 0000000000..6f549d094a
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/40105200.txt
@@ -0,0 +1,2 @@
+Main changes in this version: Mainly bugfixing!
+Full changelog: https://github.com/vector-im/element-android/releases
diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index 15d9e5c9e6..907940414f 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -3187,7 +3187,8 @@
- Final result based on %1$d votes
End poll
- winner option
+
+ winner option
End this poll?
This will stop people from being able to vote and will display the final results of the poll.
End poll
@@ -3201,6 +3202,7 @@
Voters see results as soon as they have voted
Closed poll
Results are only revealed when you end the poll
+ Ended the poll.
Active polls
There are no active polls in this room
Past polls
@@ -3518,6 +3520,9 @@
sent a video.
sent a sticker.
created a poll.
+ ended a poll.
+ Poll
+ Ended poll
Access Token
Your access token gives full access to your account. Do not share it with anyone.
diff --git a/library/ui-styles/src/main/res/values/styles_edit_text.xml b/library/ui-styles/src/main/res/values/styles_edit_text.xml
index 94f4d86160..6b282a7674 100644
--- a/library/ui-styles/src/main/res/values/styles_edit_text.xml
+++ b/library/ui-styles/src/main/res/values/styles_edit_text.xml
@@ -22,6 +22,7 @@
- false
- 15sp
- ?vctr_message_text_color
+ - 20sp
diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index 8332385beb..e74fe0a452 100644
--- a/matrix-sdk-android/build.gradle
+++ b/matrix-sdk-android/build.gradle
@@ -63,7 +63,7 @@ android {
// that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true'
- buildConfigField "String", "SDK_VERSION", "\"1.5.20\""
+ buildConfigField "String", "SDK_VERSION", "\"1.5.22\""
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""
@@ -82,7 +82,7 @@ android {
buildTypes {
debug {
if (project.hasProperty("coverage")) {
- testCoverageEnabled = coverage.enableTestCoverage
+ testCoverageEnabled = coverage == "true"
}
// Set to true to log privacy or sensible data, such as token
buildConfigField "boolean", "LOG_PRIVATE_DATA", project.property("vector.debugPrivateData")
diff --git a/matrix-sdk-android/src/androidTest/AndroidManifest.xml b/matrix-sdk-android/src/androidTest/AndroidManifest.xml
index 40360fcd19..859ebbd238 100644
--- a/matrix-sdk-android/src/androidTest/AndroidManifest.xml
+++ b/matrix-sdk-android/src/androidTest/AndroidManifest.xml
@@ -1,6 +1,5 @@
+ xmlns:tools="http://schemas.android.com/tools">
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
index 9a928c61fb..40c69ceb66 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
@@ -248,7 +248,7 @@ data class Event(
if (isRedacted()) return "Message removed"
val text = getDecryptedValue() ?: run {
if (isPoll()) {
- return getPollQuestion() ?: "created a poll."
+ return getTextSummaryForPoll()
}
return null
}
@@ -261,13 +261,23 @@ data class Event(
isImageMessage() -> "sent an image."
isVideoMessage() -> "sent a video."
isSticker() -> "sent a sticker."
- isPoll() -> getPollQuestion() ?: "created a poll."
+ isPoll() -> getTextSummaryForPoll()
isLiveLocation() -> "Live location."
isLocationMessage() -> "has shared their location."
else -> text
}
}
+ private fun getTextSummaryForPoll(): String? {
+ val pollQuestion = getPollQuestion()
+ return when {
+ pollQuestion != null -> pollQuestion
+ isPollStart() -> "created a poll."
+ isPollEnd() -> "ended a poll."
+ else -> null
+ }
+ }
+
private fun Event.isQuote(): Boolean {
if (isReplyRenderedInThread()) return false
return getDecryptedValue("formatted_body")?.contains("") ?: false
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageEndPollContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageEndPollContent.kt
index f0511903d0..6e31320b13 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageEndPollContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageEndPollContent.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.room.model.message
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
/**
@@ -25,5 +26,12 @@ import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultCon
*/
@JsonClass(generateAdapter = true)
data class MessageEndPollContent(
- @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? = null
-)
+ /**
+ * Local message type, not from server.
+ */
+ @Transient
+ override val msgType: String = MessageType.MSGTYPE_POLL_END,
+ @Json(name = "body") override val body: String = "",
+ @Json(name = "m.new_content") override val newContent: Content? = null,
+ @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null
+) : MessageContent
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt
index 647deef95e..18daa579ed 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt
@@ -38,6 +38,7 @@ object MessageType {
// Because poll events are not message events and they don't have msgtype field
const val MSGTYPE_POLL_START = "org.matrix.android.sdk.poll.start"
const val MSGTYPE_POLL_RESPONSE = "org.matrix.android.sdk.poll.response"
+ const val MSGTYPE_POLL_END = "org.matrix.android.sdk.poll.end"
const val MSGTYPE_CONFETTI = "nic.custom.confetti"
const val MSGTYPE_SNOWFALL = "io.element.effect.snowfall"
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
index 9053425a39..6320ea964d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
@@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoCo
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody
+import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollContent
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
@@ -148,6 +149,7 @@ fun TimelineEvent.getLastMessageContent(): MessageContent? {
// so toModel won't parse them correctly
// It's discriminated on event type instead. Maybe it shouldn't be MessageContent at all to avoid confusion?
in EventType.POLL_START.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel()
+ in EventType.POLL_END.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel()
in EventType.STATE_ROOM_BEACON_INFO.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel()
in EventType.BEACON_LOCATION_DATA.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel()
else -> (getLastEditNewContent() ?: root.getClearContent()).toModel()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt
index 468e998407..0a8c58de16 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt
@@ -69,7 +69,7 @@ internal class DefaultLoginWizard(
)
} else {
PasswordLoginParams.userIdentifier(
- user = login,
+ user = login.trim(),
password = password,
deviceDisplayName = initialDeviceName,
deviceId = deviceId
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushRulesResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushRulesResponse.kt
index 5f35c919fc..e359410f17 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushRulesResponse.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushRulesResponse.kt
@@ -30,10 +30,4 @@ internal data class GetPushRulesResponse(
*/
@Json(name = "global")
val global: RuleSet,
-
- /**
- * Device specific rules, apply only to current device.
- */
- @Json(name = "device")
- val device: RuleSet? = null
)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/SavePushRulesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/SavePushRulesTask.kt
index 88c78aa460..4a46f56a70 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/SavePushRulesTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/SavePushRulesTask.kt
@@ -42,7 +42,6 @@ internal class DefaultSavePushRulesTask @Inject constructor(@SessionDatabase pri
.findAll()
.forEach { it.deleteOnCascade() }
- // Save only global rules for the moment
val globalRules = params.pushRules.global
val content = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.CONTENT }
diff --git a/tools/release/releaseScript.sh b/tools/release/releaseScript.sh
index 553c02101c..f9f5303546 100755
--- a/tools/release/releaseScript.sh
+++ b/tools/release/releaseScript.sh
@@ -359,9 +359,9 @@ adb -d install ${apkPath}
read -p "Please run the APK on your phone to check that the upgrade went well (no init sync, etc.). Press enter when it's done."
printf "\n================================================================================\n"
-githubCreateReleaseLink="https://github.com/vector-im/element-android/releases/new?tag=v${version}&title=Element%%20Android%%20v${version}&body=${changelogUrlEncoded}"
+githubCreateReleaseLink="https://github.com/vector-im/element-android/releases/new?tag=v${version}&title=Element%20Android%20v${version}&body=${changelogUrlEncoded}"
printf "Creating the release on gitHub.\n"
-printf "Open this link: ${githubCreateReleaseLink}\n"
+printf -- "Open this link: %s\n" ${githubCreateReleaseLink}
printf "Then\n"
printf " - click on the 'Generate releases notes' button\n"
printf " - Add the 4 signed APKs to the GitHub release. They are located at ${targetPath}\n"
@@ -369,7 +369,7 @@ read -p ". Press enter when it's done. "
printf "\n================================================================================\n"
printf "Message for the Android internal room:\n\n"
-message="@room Element Android ${version} is ready to be tested. You can get if from https://github.com/vector-im/element-android/releases/tag/v${version}. Please report any feedback here. Thanks!"
+message="@room Element Android ${version} is ready to be tested. You can get it from https://github.com/vector-im/element-android/releases/tag/v${version}. Please report any feedback here. Thanks!"
printf "${message}\n\n"
if [[ -z "${elementBotToken}" ]]; then
diff --git a/vector-app/build.gradle b/vector-app/build.gradle
index f0961ac54f..06dce76873 100644
--- a/vector-app/build.gradle
+++ b/vector-app/build.gradle
@@ -37,7 +37,7 @@ ext.versionMinor = 5
// Note: even values are reserved for regular release, odd values for hotfix release.
// When creating a hotfix, you should decrease the value, since the current value
// is the value for the next regular release.
-ext.versionPatch = 20
+ext.versionPatch = 22
static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct'
@@ -251,7 +251,7 @@ android {
signingConfig signingConfigs.debug
if (project.hasProperty("coverage")) {
- testCoverageEnabled = coverage.enableTestCoverage
+ testCoverageEnabled = coverage == "true"
}
}
@@ -448,7 +448,7 @@ dependencies {
androidTestImplementation libs.mockk.mockkAndroid
androidTestUtil libs.androidx.orchestrator
androidTestImplementation libs.androidx.fragmentTesting
- androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.22"
- debugImplementation libs.androidx.fragmentTesting
+ androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.8.0"
+ debugImplementation libs.androidx.fragmentTestingManifest
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
}
diff --git a/vector/build.gradle b/vector/build.gradle
index bc289481e6..fa11034fea 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -69,7 +69,7 @@ android {
buildTypes {
debug {
if (project.hasProperty("coverage")) {
- testCoverageEnabled = coverage.enableTestCoverage
+ testCoverageEnabled = coverage == "true"
}
}
}
@@ -135,7 +135,7 @@ dependencies {
implementation libs.androidx.biometric
api "org.threeten:threetenbp:1.4.0:no-tzdb"
- api "com.gabrielittner.threetenbp:lazythreetenbp:0.12.0"
+ api "com.gabrielittner.threetenbp:lazythreetenbp:0.13.0"
implementation libs.squareup.moshi
kapt libs.squareup.moshiKotlin
@@ -333,6 +333,7 @@ dependencies {
}
androidTestImplementation libs.mockk.mockkAndroid
androidTestUtil libs.androidx.orchestrator
- debugImplementation libs.androidx.fragmentTesting
- androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.22"
+ debugImplementation libs.androidx.fragmentTestingManifest
+ androidTestImplementation libs.androidx.fragmentTesting
+ androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.8.0"
}
diff --git a/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt b/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt
index c94f9cd921..89bd28fc93 100644
--- a/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt
+++ b/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt
@@ -27,7 +27,7 @@ import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
fun TimelineEvent.canReact(): Boolean {
// Only event of type EventType.MESSAGE, EventType.STICKER and EventType.POLL_START are supported for the moment
- return root.getClearType() in listOf(EventType.MESSAGE, EventType.STICKER) + EventType.POLL_START.values &&
+ return root.getClearType() in listOf(EventType.MESSAGE, EventType.STICKER) + EventType.POLL_START.values + EventType.POLL_END.values &&
root.sendState == SendState.SYNCED &&
!root.isRedacted()
}
diff --git a/vector/src/main/java/im/vector/app/core/utils/SharedEvent.kt b/vector/src/main/java/im/vector/app/core/utils/SharedEvent.kt
index e712769c48..081a4f6192 100644
--- a/vector/src/main/java/im/vector/app/core/utils/SharedEvent.kt
+++ b/vector/src/main/java/im/vector/app/core/utils/SharedEvent.kt
@@ -16,16 +16,18 @@
package im.vector.app.core.utils
+import im.vector.app.core.platform.VectorViewEvents
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.transform
import java.util.concurrent.CopyOnWriteArraySet
-interface SharedEvents {
+interface SharedEvents {
fun stream(consumerId: String): Flow
}
-class EventQueue(capacity: Int) : SharedEvents {
+class EventQueue(capacity: Int) : SharedEvents {
private val innerQueue = MutableSharedFlow>(replay = capacity)
@@ -33,7 +35,12 @@ class EventQueue(capacity: Int) : SharedEvents {
innerQueue.tryEmit(OneTimeEvent(event))
}
- override fun stream(consumerId: String): Flow = innerQueue.filterNotHandledBy(consumerId)
+ override fun stream(consumerId: String): Flow = innerQueue
+ .onEach {
+ // Ensure that buffered Events will not be sent again to new subscribers.
+ innerQueue.resetReplayCache()
+ }
+ .filterNotHandledBy(consumerId)
}
/**
@@ -42,7 +49,7 @@ class EventQueue(capacity: Int) : SharedEvents {
*
* Keeps track of who has already handled its content.
*/
-private class OneTimeEvent(private val content: T) {
+private class OneTimeEvent(private val content: T) {
private val handlers = CopyOnWriteArraySet()
@@ -53,6 +60,6 @@ private class OneTimeEvent(private val content: T) {
fun getIfNotHandled(asker: String): T? = if (handlers.add(asker)) content else null
}
-private fun Flow>.filterNotHandledBy(consumerId: String): Flow = transform { event ->
+private fun Flow>.filterNotHandledBy(consumerId: String): Flow = transform { event ->
event.getIfNotHandled(consumerId)?.let { emit(it) }
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt
index 8f4dd9b71d..cf127d834f 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt
@@ -44,6 +44,7 @@ import org.commonmark.parser.Parser
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
+import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollContent
import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
@@ -181,6 +182,7 @@ class PlainTextComposerLayout @JvmOverloads constructor(
is MessageAudioContent -> getAudioContentBodyText(messageContent)
is MessagePollContent -> messageContent.getBestPollCreationInfo()?.question?.getBestQuestion()
is MessageBeaconInfoContent -> resources.getString(R.string.live_location_description)
+ is MessageEndPollContent -> resources.getString(R.string.message_reply_to_ended_poll_preview)
else -> messageContent?.body.orEmpty()
}
var formattedBody: CharSequence? = null
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanReplyEventUseCase.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanReplyEventUseCase.kt
index a9df059cc1..fdd94d1559 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanReplyEventUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanReplyEventUseCase.kt
@@ -25,8 +25,14 @@ import javax.inject.Inject
class CheckIfCanReplyEventUseCase @Inject constructor() {
fun execute(event: TimelineEvent, messageContent: MessageContent?, actionPermissions: ActionPermissions): Boolean {
- // Only EventType.MESSAGE, EventType.POLL_START and EventType.STATE_ROOM_BEACON_INFO event types are supported for the moment
- if (event.root.getClearType() !in EventType.STATE_ROOM_BEACON_INFO.values + EventType.POLL_START.values + EventType.MESSAGE) return false
+ // Only EventType.MESSAGE, EventType.POLL_START, EventType.POLL_END and EventType.STATE_ROOM_BEACON_INFO event types are supported for the moment
+ if (event.root.getClearType() !in
+ EventType.STATE_ROOM_BEACON_INFO.values +
+ EventType.POLL_START.values +
+ EventType.POLL_END.values +
+ EventType.MESSAGE
+ ) return false
+
if (!actionPermissions.canSendMessage) return false
return when (messageContent?.msgType) {
MessageType.MSGTYPE_TEXT,
@@ -37,6 +43,7 @@ class CheckIfCanReplyEventUseCase @Inject constructor() {
MessageType.MSGTYPE_AUDIO,
MessageType.MSGTYPE_FILE,
MessageType.MSGTYPE_POLL_START,
+ MessageType.MSGTYPE_POLL_END,
MessageType.MSGTYPE_BEACON_INFO,
MessageType.MSGTYPE_LOCATION -> true
else -> false
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
index e52c20c4d7..649de1acba 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
@@ -499,6 +499,7 @@ class MessageActionsViewModel @AssistedInject constructor(
MessageType.MSGTYPE_AUDIO,
MessageType.MSGTYPE_FILE,
MessageType.MSGTYPE_POLL_START,
+ MessageType.MSGTYPE_POLL_END,
MessageType.MSGTYPE_STICKER_LOCAL -> event.root.threadDetails?.isRootThread ?: false
else -> false
}
@@ -530,8 +531,8 @@ class MessageActionsViewModel @AssistedInject constructor(
}
private fun canViewReactions(event: TimelineEvent): Boolean {
- // Only event of type EventType.MESSAGE, EventType.STICKER and EventType.POLL_START are supported for the moment
- if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER) + EventType.POLL_START.values) return false
+ // Only event of type EventType.MESSAGE, EventType.STICKER, EventType.POLL_START, EventType.POLL_END are supported for the moment
+ if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER) + EventType.POLL_START.values + EventType.POLL_END.values) return false
return event.annotations?.reactionsSummary?.isNotEmpty() ?: false
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
index 42e031a3c4..219ccbe11c 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
@@ -91,11 +91,13 @@ 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.isThread
import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody
import org.matrix.android.sdk.api.session.room.model.message.MessageEmoteContent
+import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollContent
import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent
import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent
@@ -109,8 +111,10 @@ import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl
import org.matrix.android.sdk.api.session.room.model.relation.ReplyToContent
+import org.matrix.android.sdk.api.session.room.timeline.getRelationContent
import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
import org.matrix.android.sdk.api.util.MimeTypes
+import timber.log.Timber
import javax.inject.Inject
class MessageItemFactory @Inject constructor(
@@ -202,7 +206,8 @@ class MessageItemFactory @Inject constructor(
is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes)
is MessageAudioContent -> buildAudioContent(params, messageContent, informationData, highlight, attributes)
is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes)
- is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes)
+ is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes, isEnded = false)
+ is MessageEndPollContent -> buildEndedPollItem(event.getRelationContent()?.eventId, informationData, highlight, callback, attributes)
is MessageLocationContent -> buildLocationItem(messageContent, informationData, highlight, attributes)
is MessageBeaconInfoContent -> liveLocationShareMessageItemFactory.create(event, highlight, attributes)
is MessageVoiceBroadcastInfoContent -> voiceBroadcastItemFactory.create(params, messageContent, highlight, attributes)
@@ -245,6 +250,7 @@ class MessageItemFactory @Inject constructor(
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes,
+ isEnded: Boolean,
): PollItem {
val pollViewState = pollItemViewStateFactory.create(pollContent, informationData)
@@ -256,11 +262,35 @@ class MessageItemFactory @Inject constructor(
.votesStatus(pollViewState.votesStatus)
.optionViewStates(pollViewState.optionViewStates.orEmpty())
.edited(informationData.hasBeenEdited)
+ .ended(isEnded)
.highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline)
.callback(callback)
}
+ private fun buildEndedPollItem(
+ pollStartEventId: String?,
+ informationData: MessageInformationData,
+ highlight: Boolean,
+ callback: TimelineEventController.Callback?,
+ attributes: AbsMessageItem.Attributes,
+ ): PollItem? {
+ pollStartEventId ?: return null.also {
+ Timber.e("### buildEndedPollItem. Cannot render poll end event because poll start event id is null")
+ }
+ val pollStartEvent = session.roomService().getRoom(roomId)?.getTimelineEvent(pollStartEventId)
+ val pollContent = pollStartEvent?.root?.getClearContent()?.toModel() ?: return null
+
+ return buildPollItem(
+ pollContent,
+ informationData,
+ highlight,
+ callback,
+ attributes,
+ isEnded = true
+ )
+ }
+
private fun createPollQuestion(
informationData: MessageInformationData,
question: String,
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
index ae3ea143a7..61b2385d1d 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
@@ -102,6 +102,7 @@ class TimelineItemFactory @Inject constructor(
// Message itemsX
EventType.STICKER,
in EventType.POLL_START.values,
+ in EventType.POLL_END.values,
EventType.MESSAGE -> messageItemFactory.create(params)
EventType.REDACTION,
EventType.KEY_VERIFICATION_ACCEPT,
@@ -114,8 +115,7 @@ class TimelineItemFactory @Inject constructor(
EventType.CALL_SELECT_ANSWER,
EventType.CALL_NEGOTIATE,
EventType.REACTION,
- in EventType.POLL_RESPONSE.values,
- in EventType.POLL_END.values -> noticeItemFactory.create(params)
+ in EventType.POLL_RESPONSE.values -> noticeItemFactory.create(params)
in EventType.BEACON_LOCATION_DATA.values -> {
if (event.root.isRedacted()) {
messageItemFactory.create(params)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/EventDetailsFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/EventDetailsFormatter.kt
index 2233a53eda..1d3f016951 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/EventDetailsFormatter.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/EventDetailsFormatter.kt
@@ -17,11 +17,14 @@
package im.vector.app.features.home.room.detail.timeline.format
import android.content.Context
+import im.vector.app.R
import im.vector.app.core.utils.TextUtils
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.isAudioMessage
import org.matrix.android.sdk.api.session.events.model.isFileMessage
import org.matrix.android.sdk.api.session.events.model.isImageMessage
+import org.matrix.android.sdk.api.session.events.model.isPollEnd
+import org.matrix.android.sdk.api.session.events.model.isPollStart
import org.matrix.android.sdk.api.session.events.model.isVideoMessage
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
@@ -51,10 +54,16 @@ class EventDetailsFormatter @Inject constructor(
event.isVideoMessage() -> formatForVideoMessage(event)
event.isAudioMessage() -> formatForAudioMessage(event)
event.isFileMessage() -> formatForFileMessage(event)
+ event.isPollStart() -> formatPollMessage()
+ event.isPollEnd() -> formatPollEndMessage()
else -> null
}
}
+ private fun formatPollMessage() = context.getString(R.string.message_reply_to_poll_preview)
+
+ private fun formatPollEndMessage() = context.getString(R.string.message_reply_to_ended_poll_preview)
+
/**
* Example: "1024 x 720 - 670 kB".
*/
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
index c7686c913b..0fc07ef0e1 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
@@ -23,8 +23,6 @@ import im.vector.app.core.extensions.localDateTime
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactoryParams
import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
-import im.vector.app.features.home.room.detail.timeline.item.PollResponseData
-import im.vector.app.features.home.room.detail.timeline.item.PollVoteSummaryData
import im.vector.app.features.home.room.detail.timeline.item.ReferencesInfoData
import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayoutFactory
@@ -55,7 +53,8 @@ class MessageInformationDataFactory @Inject constructor(
private val session: Session,
private val dateFormatter: VectorDateFormatter,
private val messageLayoutFactory: TimelineMessageLayoutFactory,
- private val reactionsSummaryFactory: ReactionsSummaryFactory
+ private val reactionsSummaryFactory: ReactionsSummaryFactory,
+ private val pollResponseDataFactory: PollResponseDataFactory,
) {
fun create(params: TimelineItemFactoryParams): MessageInformationData {
@@ -102,20 +101,7 @@ class MessageInformationDataFactory @Inject constructor(
memberName = event.senderInfo.disambiguatedDisplayName,
messageLayout = messageLayout,
reactionsSummary = reactionsSummaryFactory.create(event),
- pollResponseAggregatedSummary = event.annotations?.pollResponseSummary?.let {
- PollResponseData(
- myVote = it.aggregatedContent?.myVote,
- isClosed = it.closedTime != null,
- votes = it.aggregatedContent?.votesSummary?.mapValues { votesSummary ->
- PollVoteSummaryData(
- total = votesSummary.value.total,
- percentage = votesSummary.value.percentage
- )
- },
- winnerVoteCount = it.aggregatedContent?.winnerVoteCount ?: 0,
- totalVotes = it.aggregatedContent?.totalVotes ?: 0
- )
- },
+ pollResponseAggregatedSummary = pollResponseDataFactory.create(event),
hasBeenEdited = event.hasBeenEdited(),
hasPendingEdits = event.annotations?.editSummary?.localEchos?.any() ?: false,
referencesInfoData = event.annotations?.referencesAggregatedSummary?.let { referencesAggregatedSummary ->
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/PollResponseDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/PollResponseDataFactory.kt
new file mode 100644
index 0000000000..533397b4d8
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/PollResponseDataFactory.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.detail.timeline.helper
+
+import im.vector.app.core.di.ActiveSessionHolder
+import im.vector.app.features.home.room.detail.timeline.item.PollResponseData
+import im.vector.app.features.home.room.detail.timeline.item.PollVoteSummaryData
+import org.matrix.android.sdk.api.session.events.model.getRelationContent
+import org.matrix.android.sdk.api.session.events.model.isPollEnd
+import org.matrix.android.sdk.api.session.room.getTimelineEvent
+import org.matrix.android.sdk.api.session.room.model.PollResponseAggregatedSummary
+import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+import timber.log.Timber
+import javax.inject.Inject
+
+class PollResponseDataFactory @Inject constructor(
+ private val activeSessionHolder: ActiveSessionHolder,
+) {
+
+ fun create(event: TimelineEvent): PollResponseData? {
+ val pollResponseSummary = getPollResponseSummary(event)
+ return pollResponseSummary?.let {
+ PollResponseData(
+ myVote = it.aggregatedContent?.myVote,
+ isClosed = it.closedTime != null,
+ votes = it.aggregatedContent?.votesSummary?.mapValues { votesSummary ->
+ PollVoteSummaryData(
+ total = votesSummary.value.total,
+ percentage = votesSummary.value.percentage
+ )
+ },
+ winnerVoteCount = it.aggregatedContent?.winnerVoteCount ?: 0,
+ totalVotes = it.aggregatedContent?.totalVotes ?: 0
+ )
+ }
+ }
+
+ private fun getPollResponseSummary(event: TimelineEvent): PollResponseAggregatedSummary? {
+ return if (event.root.isPollEnd()) {
+ val pollStartEventId = event.root.getRelationContent()?.eventId
+ if (pollStartEventId.isNullOrEmpty()) {
+ Timber.e("### Cannot render poll end event because poll start event id is null")
+ null
+ } else {
+ activeSessionHolder
+ .getSafeActiveSession()
+ ?.roomService()
+ ?.getRoom(event.roomId)
+ ?.getTimelineEvent(pollStartEventId)
+ ?.annotations
+ ?.pollResponseSummary
+ }
+ } else {
+ event.annotations?.pollResponseSummary
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt
index 51e961f247..2dcb6cc6d8 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt
@@ -55,6 +55,7 @@ object TimelineDisplayableEvents {
VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO,
) +
EventType.POLL_START.values +
+ EventType.POLL_END.values +
EventType.STATE_ROOM_BEACON_INFO.values +
EventType.BEACON_LOCATION_DATA.values
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt
index 3a9d21dfc4..072c3dcd27 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt
@@ -85,7 +85,7 @@ abstract class MessageTextItem : AbsMessageItem() {
if (useBigFont) {
holder.messageView.textSize = 44F
} else {
- holder.messageView.textSize = 14F
+ holder.messageView.textSize = 15.5F
}
if (searchForPills) {
message?.charSequence?.findPillsAndProcess(coroutineScope) {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt
index 54be4092ed..6fe19e9762 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt
@@ -19,6 +19,7 @@ package im.vector.app.features.home.room.detail.timeline.item
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.view.children
+import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
@@ -50,6 +51,9 @@ abstract class PollItem : AbsMessageItem() {
@EpoxyAttribute
lateinit var optionViewStates: List
+ @EpoxyAttribute
+ var ended: Boolean = false
+
override fun getViewStubId() = STUB_ID
override fun bind(holder: Holder) {
@@ -75,6 +79,8 @@ abstract class PollItem : AbsMessageItem() {
it.setOnClickListener { onPollItemClick(optionViewState) }
}
}
+
+ holder.endedPollTextView.isVisible = ended
}
private fun onPollItemClick(optionViewState: PollOptionViewState) {
@@ -89,6 +95,7 @@ abstract class PollItem : AbsMessageItem() {
val questionTextView by bind(R.id.questionTextView)
val optionsContainer by bind(R.id.optionsContainer)
val votesStatusTextView by bind(R.id.optionsVotesStatusTextView)
+ val endedPollTextView by bind(R.id.endedPollTextView)
}
companion object {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt
index 20aa6e3af2..e8d636e20b 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt
@@ -25,6 +25,7 @@ import androidx.core.view.isVisible
import im.vector.app.R
import im.vector.app.core.extensions.setAttributeTintedImageResource
import im.vector.app.databinding.ItemPollOptionBinding
+import im.vector.app.features.themes.ThemeUtils
class PollOptionView @JvmOverloads constructor(
context: Context,
@@ -53,35 +54,40 @@ class PollOptionView @JvmOverloads constructor(
private fun renderPollSending() {
views.optionCheckImageView.isVisible = false
- views.optionWinnerImageView.isVisible = false
+ views.optionVoteCountTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
hideVotes()
renderVoteSelection(false)
}
private fun renderPollEnded(state: PollOptionViewState.PollEnded) {
views.optionCheckImageView.isVisible = false
- views.optionWinnerImageView.isVisible = state.isWinner
+ val drawableStart = if (state.isWinner) R.drawable.ic_poll_winner else 0
+ views.optionVoteCountTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(drawableStart, 0, 0, 0)
+ views.optionVoteCountTextView.setTextColor(
+ if (state.isWinner) ThemeUtils.getColor(context, R.attr.colorPrimary)
+ else ThemeUtils.getColor(context, R.attr.vctr_content_secondary)
+ )
showVotes(state.voteCount, state.votePercentage)
renderVoteSelection(state.isWinner)
}
private fun renderPollReady() {
views.optionCheckImageView.isVisible = true
- views.optionWinnerImageView.isVisible = false
+ views.optionVoteCountTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
hideVotes()
renderVoteSelection(false)
}
private fun renderPollVoted(state: PollOptionViewState.PollVoted) {
views.optionCheckImageView.isVisible = true
- views.optionWinnerImageView.isVisible = false
+ views.optionVoteCountTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
showVotes(state.voteCount, state.votePercentage)
renderVoteSelection(state.isSelected)
}
private fun renderPollUndisclosed(state: PollOptionViewState.PollUndisclosed) {
views.optionCheckImageView.isVisible = true
- views.optionWinnerImageView.isVisible = false
+ views.optionVoteCountTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
hideVotes()
renderVoteSelection(state.isSelected)
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCase.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCase.kt
index 2197d89a2c..ff814d4cbc 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCase.kt
@@ -25,6 +25,8 @@ import org.matrix.android.sdk.api.session.events.model.isFileMessage
import org.matrix.android.sdk.api.session.events.model.isImageMessage
import org.matrix.android.sdk.api.session.events.model.isLiveLocation
import org.matrix.android.sdk.api.session.events.model.isPoll
+import org.matrix.android.sdk.api.session.events.model.isPollEnd
+import org.matrix.android.sdk.api.session.events.model.isPollStart
import org.matrix.android.sdk.api.session.events.model.isSticker
import org.matrix.android.sdk.api.session.events.model.isVideoMessage
import org.matrix.android.sdk.api.session.events.model.isVoiceMessage
@@ -93,10 +95,15 @@ class ProcessBodyOfReplyToEventUseCase @Inject constructor(
)
}
repliedToEvent.isPoll() -> {
+ val fallbackText = when {
+ repliedToEvent.isPollStart() -> stringProvider.getString(R.string.message_reply_to_sender_created_poll)
+ repliedToEvent.isPollEnd() -> stringProvider.getString(R.string.message_reply_to_sender_ended_poll)
+ else -> ""
+ }
matrixFormattedBody.replaceRange(
afterBreakingLineIndex,
endOfBlockQuoteIndex,
- repliedToEvent.getPollQuestion() ?: stringProvider.getString(R.string.message_reply_to_sender_created_poll)
+ repliedToEvent.getPollQuestion() ?: fallbackText
)
}
repliedToEvent.isLiveLocation() -> {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt
index c207a5f67e..6e34aeeca2 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt
@@ -50,6 +50,7 @@ class TimelineMessageLayoutFactory @Inject constructor(
EventType.STICKER,
) +
EventType.POLL_START.values +
+ EventType.POLL_END.values +
EventType.STATE_ROOM_BEACON_INFO.values
// Can't be rendered in bubbles, so get back to default layout
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt
index a55900a5c4..18c8ea3bde 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt
@@ -22,41 +22,33 @@ import com.airbnb.mvrx.Loading
import im.vector.app.R
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter
-import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.RoomListDisplayMode
import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter
+import im.vector.app.features.home.room.list.usecase.GetLatestPreviewableEventUseCase
import im.vector.app.features.home.room.typing.TypingHelper
import im.vector.app.features.voicebroadcast.isLive
-import im.vector.app.features.voicebroadcast.isVoiceBroadcast
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
-import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import org.matrix.android.sdk.api.extensions.orFalse
-import org.matrix.android.sdk.api.session.events.model.EventType
-import org.matrix.android.sdk.api.session.getRoom
-import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
-import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
-import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class RoomSummaryItemFactory @Inject constructor(
- private val sessionHolder: ActiveSessionHolder,
private val displayableEventFormatter: DisplayableEventFormatter,
private val dateFormatter: VectorDateFormatter,
private val stringProvider: StringProvider,
private val typingHelper: TypingHelper,
private val avatarRenderer: AvatarRenderer,
private val errorFormatter: ErrorFormatter,
- private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase,
+ private val getLatestPreviewableEventUseCase: GetLatestPreviewableEventUseCase,
) {
fun create(
@@ -142,7 +134,7 @@ class RoomSummaryItemFactory @Inject constructor(
val showSelected = selectedRoomIds.contains(roomSummary.roomId)
var latestFormattedEvent: CharSequence = ""
var latestEventTime = ""
- val latestEvent = roomSummary.getVectorLatestPreviewableEvent()
+ val latestEvent = getLatestPreviewableEventUseCase.execute(roomSummary.roomId)
if (latestEvent != null) {
latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect, roomSummary.isDirect.not())
latestEventTime = dateFormatter.format(latestEvent.root.originServerTs, DateFormatKind.ROOM_LIST)
@@ -150,7 +142,8 @@ class RoomSummaryItemFactory @Inject constructor(
val typingMessage = typingHelper.getTypingMessage(roomSummary.typingUsers)
// Skip typing while there is a live voice broadcast
- .takeUnless { latestEvent?.root?.asVoiceBroadcastEvent()?.isLive.orFalse() }.orEmpty()
+ .takeUnless { latestEvent?.root?.asVoiceBroadcastEvent()?.isLive.orFalse() }
+ .orEmpty()
return if (subtitle.isBlank() && displayMode == RoomListDisplayMode.FILTERED) {
createCenteredRoomSummaryItem(roomSummary, displayMode, showSelected, unreadCount, onClick, onLongClick)
@@ -240,14 +233,4 @@ class RoomSummaryItemFactory @Inject constructor(
else -> stringProvider.getQuantityString(R.plurals.search_space_multiple_parents, size - 1, directParentNames[0], size - 1)
}
}
-
- private fun RoomSummary.getVectorLatestPreviewableEvent(): TimelineEvent? {
- val room = sessionHolder.getSafeActiveSession()?.getRoom(roomId) ?: return latestPreviewableEvent
- val liveVoiceBroadcastTimelineEvent = getRoomLiveVoiceBroadcastsUseCase.execute(roomId).lastOrNull()
- ?.root?.eventId?.let { room.getTimelineEvent(it) }
- return latestPreviewableEvent?.takeIf { it.root.getClearType() == EventType.CALL_INVITE }
- ?: liveVoiceBroadcastTimelineEvent
- ?: latestPreviewableEvent
- ?.takeUnless { it.root.asMessageAudioEvent()?.isVoiceBroadcast().orFalse() } // Skip voice messages related to voice broadcast
- }
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCase.kt b/vector/src/main/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCase.kt
new file mode 100644
index 0000000000..6a50e87562
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCase.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.home.room.list.usecase
+
+import im.vector.app.core.di.ActiveSessionHolder
+import im.vector.app.features.voicebroadcast.isLive
+import im.vector.app.features.voicebroadcast.isVoiceBroadcast
+import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
+import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase
+import im.vector.app.features.voicebroadcast.voiceBroadcastId
+import org.matrix.android.sdk.api.extensions.orFalse
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.getRoom
+import org.matrix.android.sdk.api.session.room.Room
+import org.matrix.android.sdk.api.session.room.getTimelineEvent
+import org.matrix.android.sdk.api.session.room.model.RoomSummary
+import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
+import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+import javax.inject.Inject
+
+class GetLatestPreviewableEventUseCase @Inject constructor(
+ private val sessionHolder: ActiveSessionHolder,
+ private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase,
+) {
+
+ fun execute(roomId: String): TimelineEvent? {
+ val room = sessionHolder.getSafeActiveSession()?.getRoom(roomId) ?: return null
+ val roomSummary = room.roomSummary() ?: return null
+ return getCallEvent(roomSummary)
+ ?: getLiveVoiceBroadcastEvent(room)
+ ?: getDefaultLatestEvent(room, roomSummary)
+ }
+
+ private fun getCallEvent(roomSummary: RoomSummary): TimelineEvent? {
+ return roomSummary.latestPreviewableEvent
+ ?.takeIf { it.root.getClearType() == EventType.CALL_INVITE }
+ }
+
+ private fun getLiveVoiceBroadcastEvent(room: Room): TimelineEvent? {
+ return getRoomLiveVoiceBroadcastsUseCase.execute(room.roomId)
+ .lastOrNull()
+ ?.voiceBroadcastId
+ ?.let { room.getTimelineEvent(it) }
+ }
+
+ private fun getDefaultLatestEvent(room: Room, roomSummary: RoomSummary): TimelineEvent? {
+ val latestPreviewableEvent = roomSummary.latestPreviewableEvent
+
+ // If the default latest event is a live voice broadcast (paused or resumed), rely to the started event
+ val liveVoiceBroadcastEventId = latestPreviewableEvent?.root?.asVoiceBroadcastEvent()?.takeIf { it.isLive }?.voiceBroadcastId
+ if (liveVoiceBroadcastEventId != null) {
+ return room.getTimelineEvent(liveVoiceBroadcastEventId)
+ }
+
+ return latestPreviewableEvent
+ ?.takeUnless { it.root.asMessageAudioEvent()?.isVoiceBroadcast().orFalse() } // Skip voice messages related to voice broadcast
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt
index b46f22c58f..8d520628f0 100644
--- a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt
@@ -685,7 +685,7 @@ class LoginViewModel @AssistedInject constructor(
currentJob = viewModelScope.launch {
try {
safeLoginWizard.login(
- action.username,
+ action.username.trim(),
action.password,
action.initialDeviceName
)
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetRoomLiveVoiceBroadcastsUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetRoomLiveVoiceBroadcastsUseCase.kt
index fa5f06bfe6..fb48328305 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetRoomLiveVoiceBroadcastsUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetRoomLiveVoiceBroadcastsUseCase.kt
@@ -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 {
@@ -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 }
}
}
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventLiveUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventLiveUseCase.kt
index b3bbdad635..22fb0df6f9 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventLiveUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventLiveUseCase.kt
@@ -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> {
@@ -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> {
- 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.
*/
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCase.kt
new file mode 100644
index 0000000000..e821e09119
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCase.kt
@@ -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 }
+ }
+ }
+}
diff --git a/vector/src/main/res/layout/composer_rich_text_layout.xml b/vector/src/main/res/layout/composer_rich_text_layout.xml
index 7cc2d48cda..8992b632c0 100644
--- a/vector/src/main/res/layout/composer_rich_text_layout.xml
+++ b/vector/src/main/res/layout/composer_rich_text_layout.xml
@@ -124,6 +124,8 @@
app:layout_constraintEnd_toStartOf="@id/composerFullScreenButton"
app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder"
app:layout_constraintTop_toBottomOf="@id/composerModeBarrier"
+ app:bulletRadius="4sp"
+ app:bulletGap="8sp"
tools:text="@tools:sample/lorem/random" />
-
-
@@ -78,9 +67,9 @@
android:layout_marginBottom="8dp"
android:progressDrawable="@drawable/poll_option_progressbar_checked"
app:layout_constraintBottom_toBottomOf="@id/optionBorderImageView"
- app:layout_constraintEnd_toStartOf="@id/optionVoteCountTextView"
+ app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/optionNameTextView"
tools:progress="60" />
-
\ No newline at end of file
+
diff --git a/vector/src/main/res/layout/item_timeline_event_poll.xml b/vector/src/main/res/layout/item_timeline_event_poll.xml
index 393b736260..9151fc68cf 100644
--- a/vector/src/main/res/layout/item_timeline_event_poll.xml
+++ b/vector/src/main/res/layout/item_timeline_event_poll.xml
@@ -2,9 +2,21 @@
+ android:layout_height="wrap_content"
+ android:minWidth="@dimen/chat_bubble_fixed_size">
+
+
val event = givenAnEvent(eventType)
@@ -78,6 +78,7 @@ class CheckIfCanReplyEventUseCaseTest {
MessageType.MSGTYPE_AUDIO,
MessageType.MSGTYPE_FILE,
MessageType.MSGTYPE_POLL_START,
+ MessageType.MSGTYPE_POLL_END,
MessageType.MSGTYPE_BEACON_INFO,
MessageType.MSGTYPE_LOCATION
)
diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCaseTest.kt
index f612861511..c38afe20ec 100644
--- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCaseTest.kt
@@ -29,6 +29,7 @@ import org.junit.After
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.getPollQuestion
import org.matrix.android.sdk.api.session.events.model.isAudioMessage
import org.matrix.android.sdk.api.session.events.model.isFileMessage
@@ -158,6 +159,7 @@ class ProcessBodyOfReplyToEventUseCaseTest {
// Given
givenTypeOfRepliedEvent(isPollMessage = true)
givenNewContentForId(R.string.message_reply_to_sender_created_poll)
+ every { fakeRepliedEvent.getClearType() } returns EventType.POLL_START.unstable
every { fakeRepliedEvent.getPollQuestion() } returns null
executeAndAssertResult()
@@ -168,11 +170,23 @@ class ProcessBodyOfReplyToEventUseCaseTest {
// Given
givenTypeOfRepliedEvent(isPollMessage = true)
givenNewContentForId(R.string.message_reply_to_sender_created_poll)
+ every { fakeRepliedEvent.getClearType() } returns EventType.POLL_START.unstable
every { fakeRepliedEvent.getPollQuestion() } returns A_NEW_CONTENT
executeAndAssertResult()
}
+ @Test
+ fun `given a replied event of type poll end message when process the formatted body then content is replaced by correct string`() {
+ // Given
+ givenTypeOfRepliedEvent(isPollMessage = true)
+ givenNewContentForId(R.string.message_reply_to_sender_ended_poll)
+ every { fakeRepliedEvent.getClearType() } returns EventType.POLL_END.unstable
+ every { fakeRepliedEvent.getPollQuestion() } returns null
+
+ executeAndAssertResult()
+ }
+
@Test
fun `given a replied event of type live location message when process the formatted body then content is replaced by correct string`() {
// Given
diff --git a/vector/src/test/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCaseTest.kt
new file mode 100644
index 0000000000..5d526c783b
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCaseTest.kt
@@ -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()
+ private val fakeGetRoomLiveVoiceBroadcastsUseCase = mockk()
+
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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
+ )
+ )
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCaseTest.kt
new file mode 100644
index 0000000000..00b04aea81
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCaseTest.kt
@@ -0,0 +1,125 @@
+/*
+ * 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.voicebroadcast.usecase
+
+import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
+import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
+import im.vector.app.test.fakes.FakeSession
+import io.mockk.every
+import io.mockk.mockk
+import org.amshove.kluent.shouldBeEqualTo
+import org.amshove.kluent.shouldBeNull
+import org.amshove.kluent.shouldNotBeNull
+import org.junit.Test
+import org.matrix.android.sdk.api.session.getRoom
+import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+
+private const val A_ROOM_ID = "A_ROOM_ID"
+private const val A_VOICE_BROADCAST_ID = "A_VOICE_BROADCAST_ID"
+
+internal class GetVoiceBroadcastStateEventUseCaseTest {
+
+ private val fakeSession = FakeSession()
+ private val getVoiceBroadcastStateEventUseCase = GetVoiceBroadcastStateEventUseCase(fakeSession)
+
+ @Test
+ fun `given there is no event related to the given vb, when execute, then return null`() {
+ // Given
+ val aVoiceBroadcast = VoiceBroadcast(A_VOICE_BROADCAST_ID, A_ROOM_ID)
+ every { fakeSession.getRoom(A_ROOM_ID)?.timelineService()?.getTimelineEvent(A_VOICE_BROADCAST_ID) } returns null
+ every { fakeSession.getRoom(A_ROOM_ID)?.timelineService()?.getTimelineEventsRelatedTo(any(), any()) } returns emptyList()
+
+ // When
+ val result = getVoiceBroadcastStateEventUseCase.execute(aVoiceBroadcast)
+
+ // Then
+ result.shouldBeNull()
+ }
+
+ @Test
+ fun `given there are several related events related to the given vb, when execute, then return the most recent one`() {
+ // Given
+ val aVoiceBroadcast = VoiceBroadcast(A_VOICE_BROADCAST_ID, A_ROOM_ID)
+ val aListOfTimelineEvents = listOf(
+ givenAVoiceBroadcastEvent(eventId = A_VOICE_BROADCAST_ID, state = VoiceBroadcastState.STARTED, isRedacted = false, timestamp = 1L),
+ givenAVoiceBroadcastEvent(eventId = "event_id_3", state = VoiceBroadcastState.STOPPED, isRedacted = false, timestamp = 3L),
+ givenAVoiceBroadcastEvent(eventId = "event_id_2", state = VoiceBroadcastState.PAUSED, isRedacted = false, timestamp = 2L),
+ )
+ every { fakeSession.getRoom(A_ROOM_ID)?.timelineService()?.getTimelineEventsRelatedTo(any(), any()) } returns aListOfTimelineEvents
+
+ // When
+ val result = getVoiceBroadcastStateEventUseCase.execute(aVoiceBroadcast)
+
+ // Then
+ result.shouldNotBeNull()
+ result.root.eventId shouldBeEqualTo "event_id_3"
+ }
+
+ @Test
+ fun `given there are several related events related to the given vb, when execute, then return the most recent one which is not redacted`() {
+ // Given
+ val aVoiceBroadcast = VoiceBroadcast(A_VOICE_BROADCAST_ID, A_ROOM_ID)
+ val aListOfTimelineEvents = listOf(
+ givenAVoiceBroadcastEvent(eventId = A_VOICE_BROADCAST_ID, state = VoiceBroadcastState.STARTED, isRedacted = false, timestamp = 1L),
+ givenAVoiceBroadcastEvent(eventId = "event_id_2", state = VoiceBroadcastState.STOPPED, isRedacted = true, timestamp = 2L),
+ )
+ every { fakeSession.getRoom(A_ROOM_ID)?.timelineService()?.getTimelineEventsRelatedTo(any(), any()) } returns aListOfTimelineEvents
+
+ // When
+ val result = getVoiceBroadcastStateEventUseCase.execute(aVoiceBroadcast)
+
+ // Then
+ result.shouldNotBeNull()
+ result.root.eventId shouldBeEqualTo A_VOICE_BROADCAST_ID
+ }
+
+ @Test
+ fun `given a not ended voice broadcast with a redacted start event, when execute, then return null`() {
+ // Given
+ val aVoiceBroadcast = VoiceBroadcast(A_VOICE_BROADCAST_ID, A_ROOM_ID)
+ val aListOfTimelineEvents = listOf(
+ givenAVoiceBroadcastEvent(eventId = A_VOICE_BROADCAST_ID, state = VoiceBroadcastState.STARTED, isRedacted = true, timestamp = 1L),
+ givenAVoiceBroadcastEvent(eventId = "event_id_2", state = VoiceBroadcastState.PAUSED, isRedacted = false, timestamp = 2L),
+ givenAVoiceBroadcastEvent(eventId = "event_id_3", state = VoiceBroadcastState.RESUMED, isRedacted = false, timestamp = 3L),
+ )
+ every { fakeSession.getRoom(A_ROOM_ID)?.timelineService()?.getTimelineEventsRelatedTo(any(), any()) } returns aListOfTimelineEvents
+
+ // When
+ val result = getVoiceBroadcastStateEventUseCase.execute(aVoiceBroadcast)
+
+ // Then
+ result.shouldBeNull()
+ }
+
+ private fun givenAVoiceBroadcastEvent(
+ eventId: String,
+ state: VoiceBroadcastState,
+ isRedacted: Boolean,
+ timestamp: Long,
+ ): TimelineEvent {
+ val timelineEvent = mockk {
+ every { root.eventId } returns eventId
+ every { root.type } returns VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO
+ every { root.content } returns mapOf("state" to state.value)
+ every { root.isRedacted() } returns isRedacted
+ every { root.originServerTs } returns timestamp
+ }
+ every { fakeSession.getRoom(A_ROOM_ID)?.timelineService()?.getTimelineEvent(eventId) } returns timelineEvent
+ return timelineEvent
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeDrawableProvider.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeDrawableProvider.kt
new file mode 100644
index 0000000000..26fa7af3f5
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeDrawableProvider.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.test.fakes
+
+import im.vector.app.core.resources.DrawableProvider
+import io.mockk.every
+import io.mockk.mockk
+
+class FakeDrawableProvider {
+ val instance = mockk()
+
+ init {
+ every { instance.getDrawable(any()) } returns mockk()
+ every { instance.getDrawable(any(), any()) } returns mockk()
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeHomeLayoutPreferencesStore.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeHomeLayoutPreferencesStore.kt
new file mode 100644
index 0000000000..bd5dd20d37
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeHomeLayoutPreferencesStore.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.test.fakes
+
+import im.vector.app.features.home.room.list.home.HomeLayoutPreferencesStore
+import io.mockk.every
+import io.mockk.mockk
+import kotlinx.coroutines.flow.MutableSharedFlow
+
+class FakeHomeLayoutPreferencesStore {
+
+ private val _areRecentsEnabledFlow = MutableSharedFlow()
+ private val _areFiltersEnabledFlow = MutableSharedFlow()
+ private val _isAZOrderingEnabledFlow = MutableSharedFlow()
+
+ val instance = mockk(relaxed = true) {
+ every { areRecentsEnabledFlow } returns _areRecentsEnabledFlow
+ every { areFiltersEnabledFlow } returns _areFiltersEnabledFlow
+ every { isAZOrderingEnabledFlow } returns _isAZOrderingEnabledFlow
+ }
+
+ suspend fun givenRecentsEnabled(enabled: Boolean) {
+ _areRecentsEnabledFlow.emit(enabled)
+ }
+
+ suspend fun givenFiltersEnabled(enabled: Boolean) {
+ _areFiltersEnabledFlow.emit(enabled)
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeRoomService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeRoomService.kt
index 506e96ba11..e957266383 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeRoomService.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeRoomService.kt
@@ -30,4 +30,8 @@ class FakeRoomService(
fun getRoomSummaryReturns(roomSummary: RoomSummary?) {
every { getRoomSummary(any()) } returns roomSummary
}
+
+ fun set(roomSummary: RoomSummary?) {
+ every { getRoomSummary(any()) } returns roomSummary
+ }
}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt
index 420d16e82f..9155f58dd5 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt
@@ -42,6 +42,7 @@ class FakeSession(
val fakeSharedSecretStorageService: FakeSharedSecretStorageService = FakeSharedSecretStorageService(),
val fakeRoomService: FakeRoomService = FakeRoomService(),
val fakePushersService: FakePushersService = FakePushersService(),
+ val fakeUserService: FakeUserService = FakeUserService(),
private val fakeEventService: FakeEventService = FakeEventService(),
val fakeSessionAccountDataService: FakeSessionAccountDataService = FakeSessionAccountDataService()
) : Session by mockk(relaxed = true) {
@@ -62,6 +63,7 @@ class FakeSession(
override fun eventService() = fakeEventService
override fun pushersService() = fakePushersService
override fun accountDataService() = fakeSessionAccountDataService
+ override fun userService() = fakeUserService
fun givenVectorStore(vectorSessionStore: VectorSessionStore) {
coEvery {
@@ -92,8 +94,10 @@ class FakeSession(
/**
* Do not forget to call mockkStatic("org.matrix.android.sdk.flow.FlowSessionKt") in the setup method of the tests.
*/
+ @SuppressWarnings("all")
fun givenFlowSession(): FlowSession {
val fakeFlowSession = mockk()
+
every { flow() } returns fakeFlowSession
return fakeFlowSession
}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeStringProvider.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeStringProvider.kt
index 28d9f7c732..83f8607261 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeStringProvider.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeStringProvider.kt
@@ -17,6 +17,7 @@
package im.vector.app.test.fakes
import im.vector.app.core.resources.StringProvider
+import io.mockk.InternalPlatformDsl.toStr
import io.mockk.every
import io.mockk.mockk
@@ -27,6 +28,9 @@ class FakeStringProvider {
every { instance.getString(any()) } answers {
"test-${args[0]}"
}
+ every { instance.getString(any(), any()) } answers {
+ "test-${args[0]}-${args[1].toStr()}"
+ }
every { instance.getQuantityString(any(), any(), any()) } answers {
"test-${args[0]}-${args[1]}"
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeUserService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeUserService.kt
new file mode 100644
index 0000000000..065796934c
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeUserService.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.test.fakes
+
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.slot
+import org.matrix.android.sdk.api.session.user.UserService
+import org.matrix.android.sdk.api.session.user.model.User
+
+class FakeUserService : UserService by mockk() {
+
+ private val userIdSlot = slot()
+
+ init {
+ every { getUser(capture(userIdSlot)) } answers { User(userId = userIdSlot.captured) }
+ }
+}