Merge branch 'develop' into feature/bca/rust_flavor

This commit is contained in:
valere 2023-01-12 14:51:03 +01:00
commit c0397875f0
72 changed files with 1201 additions and 152 deletions

View File

@ -11,7 +11,7 @@ jobs:
- run: | - run: |
npm install --save-dev @babel/plugin-transform-flow-strip-types npm install --save-dev @babel/plugin-transform-flow-strip-types
- name: Danger - name: Danger
uses: danger/danger-js@11.2.0 uses: danger/danger-js@11.2.1
with: with:
args: "--dangerfile ./tools/danger/dangerfile.js" args: "--dangerfile ./tools/danger/dangerfile.js"
env: env:

View File

@ -66,7 +66,7 @@ jobs:
yarn add danger-plugin-lint-report --dev yarn add danger-plugin-lint-report --dev
- name: Danger lint - name: Danger lint
if: always() if: always()
uses: danger/danger-js@11.2.0 uses: danger/danger-js@11.2.1
with: with:
args: "--dangerfile ./tools/danger/dangerfile-lint.js" args: "--dangerfile ./tools/danger/dangerfile-lint.js"
env: env:

View File

@ -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) Changes in Element v1.5.18 (2023-01-02)
======================================= =======================================

View File

@ -127,7 +127,8 @@ GEM
xcpretty (~> 0.3.0) xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3) xcpretty-travis-formatter (>= 0.0.3)
gh_inspector (1.1.3) gh_inspector (1.1.3)
git (1.11.0) git (1.13.0)
addressable (~> 2.8)
rchardet (~> 1.8) rchardet (~> 1.8)
google-apis-androidpublisher_v3 (0.25.0) google-apis-androidpublisher_v3 (0.25.0)
google-apis-core (>= 0.7, < 2.a) google-apis-core (>= 0.7, < 2.a)

5
SECURITY.md Normal file
View File

@ -0,0 +1,5 @@
# Reporting a Vulnerability
**If you've found a security vulnerability, please report it to security@matrix.org**
For more information on our security disclosure policy, visit https://www.matrix.org/security-disclosure-policy/

View File

@ -27,8 +27,8 @@ buildscript {
classpath 'com.google.firebase:firebase-appdistribution-gradle:3.1.1' classpath 'com.google.firebase:firebase-appdistribution-gradle:3.1.1'
classpath 'com.google.gms:google-services:4.3.14' classpath 'com.google.gms:google-services:4.3.14'
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.5.0.2730' 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.google.android.gms:oss-licenses-plugin:0.10.6'
classpath "com.likethesalad.android:stem-plugin:2.2.3" classpath "com.likethesalad.android:stem-plugin:2.3.0"
classpath 'org.owasp:dependency-check-gradle:7.4.4' classpath 'org.owasp:dependency-check-gradle:7.4.4'
classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.7.20" classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.7.20"
classpath "org.jetbrains.kotlinx:kotlinx-knit:0.4.0" classpath "org.jetbrains.kotlinx:kotlinx-knit:0.4.0"
@ -45,10 +45,10 @@ plugins {
// Detekt // Detekt
id "io.gitlab.arturbosch.detekt" version "1.22.0" id "io.gitlab.arturbosch.detekt" version "1.22.0"
// Ksp // 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 // Dependency Analysis
id 'com.autonomousapps.dependency-analysis' version "1.17.0" id 'com.autonomousapps.dependency-analysis' version "1.18.0"
// Gradle doctor // Gradle doctor
id "com.osacky.doctor" version "0.8.1" id "com.osacky.doctor" version "0.8.1"
} }

View File

@ -1 +0,0 @@
ReplyTo are not updated if the original message is edited or deleted.

View File

@ -1 +0,0 @@
Observe ViewEvents only when resumed and ensure ViewEvents are not lost.

1
changelog.d/7832.bugfix Normal file
View File

@ -0,0 +1 @@
[Voice Broadcast] Fix unexpected "live broadcast" in the room list

View File

@ -1 +0,0 @@
[Session manager] Missing info when a session does not support encryption

View File

@ -1,2 +0,0 @@
[Poll] Render active polls list of a room
[Poll] Render past polls list of a room

View File

@ -1 +0,0 @@
Reduce number of crypto database transactions when handling the sync response

View File

@ -1 +0,0 @@
"[Rich text editor] Add list formatting buttons to the rich text editor"

View File

@ -1 +0,0 @@
[Voice Broadcast] Stop listening if we reach the last received chunk and there is no last sequence number

1
changelog.d/7900.feature Normal file
View File

@ -0,0 +1 @@
Render ended polls

View File

@ -1 +0,0 @@
Handle network error on API `rooms/{roomId}/threads`

1
changelog.d/7930.feature Normal file
View File

@ -0,0 +1 @@
"[Rich text editor] Update list item bullet appearance"

1
changelog.d/7936.misc Normal file
View File

@ -0,0 +1 @@
Upgrade to Kotlin 1.8

View File

@ -80,12 +80,12 @@ task generateCoverageReport(type: JacocoReport) {
task unitTestsWithCoverage(type: GradleBuild) { task unitTestsWithCoverage(type: GradleBuild) {
// the 7.1.3 android gradle plugin has a bug where enableTestCoverage generates invalid coverage // 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'] tasks = ['testDebugUnitTest']
} }
task instrumentationTestsWithCoverage(type: GradleBuild) { task instrumentationTestsWithCoverage(type: GradleBuild) {
startParameter.projectProperties.coverage = [enableTestCoverage: true] startParameter.projectProperties.coverage = "true"
startParameter.projectProperties['android.testInstrumentationRunnerArguments.notPackage'] = 'im.vector.app.ui' startParameter.projectProperties['android.testInstrumentationRunnerArguments.notPackage'] = 'im.vector.app.ui'
tasks = [':vector-app:connectedGplayKotlinCryptoDebugAndroidTest', ':vector:connectedKotlinCryptoDebugAndroidTest', 'matrix-sdk-android:connectedKotlinCryptoDebugAndroidTest'] tasks = [':vector-app:connectedGplayKotlinCryptoDebugAndroidTest', ':vector:connectedKotlinCryptoDebugAndroidTest', 'matrix-sdk-android:connectedKotlinCryptoDebugAndroidTest']
} }

View File

@ -8,7 +8,7 @@ ext.versions = [
def gradle = "7.3.1" def gradle = "7.3.1"
// Ref: https://kotlinlang.org/releases.html // Ref: https://kotlinlang.org/releases.html
def kotlin = "1.7.22" def kotlin = "1.8.0"
def kotlinCoroutines = "1.6.4" def kotlinCoroutines = "1.6.4"
def dagger = "2.44.2" def dagger = "2.44.2"
def firebaseBom = "31.1.1" def firebaseBom = "31.1.1"
@ -18,7 +18,7 @@ def markwon = "4.6.2"
def moshi = "1.14.0" def moshi = "1.14.0"
def lifecycle = "2.5.1" def lifecycle = "2.5.1"
def flowBinding = "1.2.0" def flowBinding = "1.2.0"
def flipper = "0.176.0" def flipper = "0.176.1"
def epoxy = "5.0.0" def epoxy = "5.0.0"
def mavericks = "3.0.1" def mavericks = "3.0.1"
def glide = "4.14.2" def glide = "4.14.2"
@ -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 // 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 // the whole commit which set version 0.16.0-SNAPSHOT
def vanniktechEmoji = "0.16.0-SNAPSHOT" def vanniktechEmoji = "0.16.0-SNAPSHOT"
def sentry = "6.9.2" def sentry = "6.11.0"
def fragment = "1.5.5" // Use 1.6.0 alpha to fix issue with test
def fragment = "1.6.0-alpha04"
// Testing // 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 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 espresso = "3.5.1"
def androidxTest = "1.4.0" def androidxTest = "1.5.0"
def androidxOrchestrator = "1.4.2" def androidxOrchestrator = "1.4.2"
def paparazzi = "1.1.0" def paparazzi = "1.1.0"
@ -56,11 +57,12 @@ ext.libs = [
'exifinterface' : "androidx.exifinterface:exifinterface:1.3.5", 'exifinterface' : "androidx.exifinterface:exifinterface:1.3.5",
'fragmentKtx' : "androidx.fragment:fragment-ktx:$fragment", 'fragmentKtx' : "androidx.fragment:fragment-ktx:$fragment",
'fragmentTesting' : "androidx.fragment:fragment-testing:$fragment", 'fragmentTesting' : "androidx.fragment:fragment-testing:$fragment",
'fragmentTestingManifest' : "androidx.fragment:fragment-testing-manifest:$fragment",
'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.4", 'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.4",
'work' : "androidx.work:work-runtime-ktx:2.7.1", 'work' : "androidx.work:work-runtime-ktx:2.7.1",
'autoFill' : "androidx.autofill:autofill:1.1.0", 'autoFill' : "androidx.autofill:autofill:1.1.0",
'preferenceKtx' : "androidx.preference:preference-ktx:1.2.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", 'lifecycleCommon' : "androidx.lifecycle:lifecycle-common:$lifecycle",
'lifecycleLivedata' : "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle", 'lifecycleLivedata' : "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle",
'lifecycleProcess' : "androidx.lifecycle:lifecycle-process:$lifecycle", 'lifecycleProcess' : "androidx.lifecycle:lifecycle-process:$lifecycle",
@ -86,7 +88,7 @@ ext.libs = [
'appdistributionApi' : "com.google.firebase:firebase-appdistribution-api-ktx:$appDistribution", 'appdistributionApi' : "com.google.firebase:firebase-appdistribution-api-ktx:$appDistribution",
'appdistribution' : "com.google.firebase:firebase-appdistribution:$appDistribution", 'appdistribution' : "com.google.firebase:firebase-appdistribution:$appDistribution",
// Phone number https://github.com/google/libphonenumber // 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 : [
'dagger' : "com.google.dagger:dagger:$dagger", 'dagger' : "com.google.dagger:dagger:$dagger",
@ -101,7 +103,7 @@ ext.libs = [
], ],
element : [ element : [
'opusencoder' : "io.element.android:opusencoder:1.1.0", 'opusencoder' : "io.element.android:opusencoder:1.1.0",
'wysiwyg' : "io.element.android:wysiwyg:0.14.0" 'wysiwyg' : "io.element.android:wysiwyg:0.15.0"
], ],
squareup : [ squareup : [
'moshi' : "com.squareup.moshi:moshi:$moshi", 'moshi' : "com.squareup.moshi:moshi:$moshi",

View File

@ -0,0 +1,2 @@
Main changes in this version: Mainly bugfixing!
Full changelog: https://github.com/vector-im/element-android/releases

View File

@ -3187,7 +3187,8 @@
<item quantity="other">Final result based on %1$d votes</item> <item quantity="other">Final result based on %1$d votes</item>
</plurals> </plurals>
<string name="poll_end_action">End poll</string> <string name="poll_end_action">End poll</string>
<string name="a11y_poll_winner_option">winner option</string> <!-- TODO TO BE REMOVED -->
<string name="a11y_poll_winner_option" tools:ignore="UnusedResources">winner option</string>
<string name="end_poll_confirmation_title">End this poll?</string> <string name="end_poll_confirmation_title">End this poll?</string>
<string name="end_poll_confirmation_description">This will stop people from being able to vote and will display the final results of the poll.</string> <string name="end_poll_confirmation_description">This will stop people from being able to vote and will display the final results of the poll.</string>
<string name="end_poll_confirmation_approve_button">End poll</string> <string name="end_poll_confirmation_approve_button">End poll</string>
@ -3201,6 +3202,7 @@
<string name="open_poll_option_description">Voters see results as soon as they have voted</string> <string name="open_poll_option_description">Voters see results as soon as they have voted</string>
<string name="closed_poll_option_title">Closed poll</string> <string name="closed_poll_option_title">Closed poll</string>
<string name="closed_poll_option_description">Results are only revealed when you end the poll</string> <string name="closed_poll_option_description">Results are only revealed when you end the poll</string>
<string name="ended_poll_indicator">Ended the poll.</string>
<string name="room_polls_active">Active polls</string> <string name="room_polls_active">Active polls</string>
<string name="room_polls_active_no_item">There are no active polls in this room</string> <string name="room_polls_active_no_item">There are no active polls in this room</string>
<string name="room_polls_ended">Past polls</string> <string name="room_polls_ended">Past polls</string>
@ -3518,6 +3520,9 @@
<string name="message_reply_to_sender_sent_video">sent a video.</string> <string name="message_reply_to_sender_sent_video">sent a video.</string>
<string name="message_reply_to_sender_sent_sticker">sent a sticker.</string> <string name="message_reply_to_sender_sent_sticker">sent a sticker.</string>
<string name="message_reply_to_sender_created_poll">created a poll.</string> <string name="message_reply_to_sender_created_poll">created a poll.</string>
<string name="message_reply_to_sender_ended_poll">ended a poll.</string>
<string name="message_reply_to_poll_preview">Poll</string>
<string name="message_reply_to_ended_poll_preview">Ended poll</string>
<string name="settings_access_token">Access Token</string> <string name="settings_access_token">Access Token</string>
<string name="settings_access_token_summary">Your access token gives full access to your account. Do not share it with anyone.</string> <string name="settings_access_token_summary">Your access token gives full access to your account. Do not share it with anyone.</string>

View File

@ -22,6 +22,7 @@
<item name="android:clipToPadding">false</item> <item name="android:clipToPadding">false</item>
<item name="android:textSize">15sp</item> <item name="android:textSize">15sp</item>
<item name="android:textColor">?vctr_message_text_color</item> <item name="android:textColor">?vctr_message_text_color</item>
<item name="lineHeight">20sp</item>
</style> </style>
</resources> </resources>

View File

@ -63,7 +63,7 @@ android {
// that the app's state is completely cleared between tests. // that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true' 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", "\"${gitRevision()}\""
buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""
@ -82,7 +82,7 @@ android {
buildTypes { buildTypes {
debug { debug {
if (project.hasProperty("coverage")) { if (project.hasProperty("coverage")) {
testCoverageEnabled = coverage.enableTestCoverage testCoverageEnabled = coverage == "true"
} }
// Set to true to log privacy or sensible data, such as token // Set to true to log privacy or sensible data, such as token
buildConfigField "boolean", "LOG_PRIVATE_DATA", project.property("vector.debugPrivateData") buildConfigField "boolean", "LOG_PRIVATE_DATA", project.property("vector.debugPrivateData")

View File

@ -1,6 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools">
package="org.matrix.android.sdk">
<application> <application>

View File

@ -248,7 +248,7 @@ data class Event(
if (isRedacted()) return "Message removed" if (isRedacted()) return "Message removed"
val text = getDecryptedValue() ?: run { val text = getDecryptedValue() ?: run {
if (isPoll()) { if (isPoll()) {
return getPollQuestion() ?: "created a poll." return getTextSummaryForPoll()
} }
return null return null
} }
@ -261,13 +261,23 @@ data class Event(
isImageMessage() -> "sent an image." isImageMessage() -> "sent an image."
isVideoMessage() -> "sent a video." isVideoMessage() -> "sent a video."
isSticker() -> "sent a sticker." isSticker() -> "sent a sticker."
isPoll() -> getPollQuestion() ?: "created a poll." isPoll() -> getTextSummaryForPoll()
isLiveLocation() -> "Live location." isLiveLocation() -> "Live location."
isLocationMessage() -> "has shared their location." isLocationMessage() -> "has shared their location."
else -> text 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 { private fun Event.isQuote(): Boolean {
if (isReplyRenderedInThread()) return false if (isReplyRenderedInThread()) return false
return getDecryptedValue("formatted_body")?.contains("<blockquote>") ?: false return getDecryptedValue("formatted_body")?.contains("<blockquote>") ?: false

View File

@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.room.model.message
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass 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 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) @JsonClass(generateAdapter = true)
data class MessageEndPollContent( 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

View File

@ -38,6 +38,7 @@ object MessageType {
// Because poll events are not message events and they don't have msgtype field // 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_START = "org.matrix.android.sdk.poll.start"
const val MSGTYPE_POLL_RESPONSE = "org.matrix.android.sdk.poll.response" 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_CONFETTI = "nic.custom.confetti"
const val MSGTYPE_SNOWFALL = "io.element.effect.snowfall" const val MSGTYPE_SNOWFALL = "io.element.effect.snowfall"

View File

@ -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.MessageBeaconLocationDataContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContent 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.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.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
@ -148,6 +149,7 @@ fun TimelineEvent.getLastMessageContent(): MessageContent? {
// so toModel<MessageContent> won't parse them correctly // so toModel<MessageContent> won't parse them correctly
// It's discriminated on event type instead. Maybe it shouldn't be MessageContent at all to avoid confusion? // It's discriminated on event type instead. Maybe it shouldn't be MessageContent at all to avoid confusion?
in EventType.POLL_START.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel<MessagePollContent>() in EventType.POLL_START.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel<MessagePollContent>()
in EventType.POLL_END.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel<MessageEndPollContent>()
in EventType.STATE_ROOM_BEACON_INFO.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel<MessageBeaconInfoContent>() in EventType.STATE_ROOM_BEACON_INFO.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel<MessageBeaconInfoContent>()
in EventType.BEACON_LOCATION_DATA.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel<MessageBeaconLocationDataContent>() in EventType.BEACON_LOCATION_DATA.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel<MessageBeaconLocationDataContent>()
else -> (getLastEditNewContent() ?: root.getClearContent()).toModel() else -> (getLastEditNewContent() ?: root.getClearContent()).toModel()

View File

@ -69,7 +69,7 @@ internal class DefaultLoginWizard(
) )
} else { } else {
PasswordLoginParams.userIdentifier( PasswordLoginParams.userIdentifier(
user = login, user = login.trim(),
password = password, password = password,
deviceDisplayName = initialDeviceName, deviceDisplayName = initialDeviceName,
deviceId = deviceId deviceId = deviceId

View File

@ -30,10 +30,4 @@ internal data class GetPushRulesResponse(
*/ */
@Json(name = "global") @Json(name = "global")
val global: RuleSet, val global: RuleSet,
/**
* Device specific rules, apply only to current device.
*/
@Json(name = "device")
val device: RuleSet? = null
) )

View File

@ -42,7 +42,6 @@ internal class DefaultSavePushRulesTask @Inject constructor(@SessionDatabase pri
.findAll() .findAll()
.forEach { it.deleteOnCascade() } .forEach { it.deleteOnCascade() }
// Save only global rules for the moment
val globalRules = params.pushRules.global val globalRules = params.pushRules.global
val content = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.CONTENT } val content = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.CONTENT }

View File

@ -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." 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" 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 "Creating the release on gitHub.\n"
printf "Open this link: ${githubCreateReleaseLink}\n" printf -- "Open this link: %s\n" ${githubCreateReleaseLink}
printf "Then\n" printf "Then\n"
printf " - click on the 'Generate releases notes' button\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" 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 "\n================================================================================\n"
printf "Message for the Android internal room:\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" printf "${message}\n\n"
if [[ -z "${elementBotToken}" ]]; then if [[ -z "${elementBotToken}" ]]; then

View File

@ -37,7 +37,7 @@ ext.versionMinor = 5
// Note: even values are reserved for regular release, odd values for hotfix release. // 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 // When creating a hotfix, you should decrease the value, since the current value
// is the value for the next regular release. // is the value for the next regular release.
ext.versionPatch = 20 ext.versionPatch = 22
static def getGitTimestamp() { static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct' def cmd = 'git show -s --format=%ct'
@ -251,7 +251,7 @@ android {
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
if (project.hasProperty("coverage")) { if (project.hasProperty("coverage")) {
testCoverageEnabled = coverage.enableTestCoverage testCoverageEnabled = coverage == "true"
} }
} }
@ -448,7 +448,7 @@ dependencies {
androidTestImplementation libs.mockk.mockkAndroid androidTestImplementation libs.mockk.mockkAndroid
androidTestUtil libs.androidx.orchestrator androidTestUtil libs.androidx.orchestrator
androidTestImplementation libs.androidx.fragmentTesting androidTestImplementation libs.androidx.fragmentTesting
androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.22" androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.8.0"
debugImplementation libs.androidx.fragmentTesting debugImplementation libs.androidx.fragmentTestingManifest
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
} }

View File

@ -69,7 +69,7 @@ android {
buildTypes { buildTypes {
debug { debug {
if (project.hasProperty("coverage")) { if (project.hasProperty("coverage")) {
testCoverageEnabled = coverage.enableTestCoverage testCoverageEnabled = coverage == "true"
} }
} }
} }
@ -135,7 +135,7 @@ dependencies {
implementation libs.androidx.biometric implementation libs.androidx.biometric
api "org.threeten:threetenbp:1.4.0:no-tzdb" 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 implementation libs.squareup.moshi
kapt libs.squareup.moshiKotlin kapt libs.squareup.moshiKotlin
@ -333,6 +333,7 @@ dependencies {
} }
androidTestImplementation libs.mockk.mockkAndroid androidTestImplementation libs.mockk.mockkAndroid
androidTestUtil libs.androidx.orchestrator androidTestUtil libs.androidx.orchestrator
debugImplementation libs.androidx.fragmentTesting debugImplementation libs.androidx.fragmentTestingManifest
androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.22" androidTestImplementation libs.androidx.fragmentTesting
androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.8.0"
} }

View File

@ -27,7 +27,7 @@ import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
fun TimelineEvent.canReact(): Boolean { fun TimelineEvent.canReact(): Boolean {
// Only event of type EventType.MESSAGE, EventType.STICKER and EventType.POLL_START are supported for the moment // 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.sendState == SendState.SYNCED &&
!root.isRedacted() !root.isRedacted()
} }

View File

@ -16,16 +16,18 @@
package im.vector.app.core.utils package im.vector.app.core.utils
import im.vector.app.core.platform.VectorViewEvents
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.transform import kotlinx.coroutines.flow.transform
import java.util.concurrent.CopyOnWriteArraySet import java.util.concurrent.CopyOnWriteArraySet
interface SharedEvents<out T> { interface SharedEvents<out T : VectorViewEvents> {
fun stream(consumerId: String): Flow<T> fun stream(consumerId: String): Flow<T>
} }
class EventQueue<T>(capacity: Int) : SharedEvents<T> { class EventQueue<T : VectorViewEvents>(capacity: Int) : SharedEvents<T> {
private val innerQueue = MutableSharedFlow<OneTimeEvent<T>>(replay = capacity) private val innerQueue = MutableSharedFlow<OneTimeEvent<T>>(replay = capacity)
@ -33,7 +35,12 @@ class EventQueue<T>(capacity: Int) : SharedEvents<T> {
innerQueue.tryEmit(OneTimeEvent(event)) innerQueue.tryEmit(OneTimeEvent(event))
} }
override fun stream(consumerId: String): Flow<T> = innerQueue.filterNotHandledBy(consumerId) override fun stream(consumerId: String): Flow<T> = innerQueue
.onEach {
// Ensure that buffered Events will not be sent again to new subscribers.
innerQueue.resetReplayCache()
}
.filterNotHandledBy(consumerId)
} }
/** /**
@ -42,7 +49,7 @@ class EventQueue<T>(capacity: Int) : SharedEvents<T> {
* *
* Keeps track of who has already handled its content. * Keeps track of who has already handled its content.
*/ */
private class OneTimeEvent<out T>(private val content: T) { private class OneTimeEvent<out T : VectorViewEvents>(private val content: T) {
private val handlers = CopyOnWriteArraySet<String>() private val handlers = CopyOnWriteArraySet<String>()
@ -53,6 +60,6 @@ private class OneTimeEvent<out T>(private val content: T) {
fun getIfNotHandled(asker: String): T? = if (handlers.add(asker)) content else null fun getIfNotHandled(asker: String): T? = if (handlers.add(asker)) content else null
} }
private fun <T> Flow<OneTimeEvent<T>>.filterNotHandledBy(consumerId: String): Flow<T> = transform { event -> private fun <T : VectorViewEvents> Flow<OneTimeEvent<T>>.filterNotHandledBy(consumerId: String): Flow<T> = transform { event ->
event.getIfNotHandled(consumerId)?.let { emit(it) } event.getIfNotHandled(consumerId)?.let { emit(it) }
} }

View File

@ -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.MessageAudioContent
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent 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.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.MessageFormat
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
@ -181,6 +182,7 @@ class PlainTextComposerLayout @JvmOverloads constructor(
is MessageAudioContent -> getAudioContentBodyText(messageContent) is MessageAudioContent -> getAudioContentBodyText(messageContent)
is MessagePollContent -> messageContent.getBestPollCreationInfo()?.question?.getBestQuestion() is MessagePollContent -> messageContent.getBestPollCreationInfo()?.question?.getBestQuestion()
is MessageBeaconInfoContent -> resources.getString(R.string.live_location_description) 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() else -> messageContent?.body.orEmpty()
} }
var formattedBody: CharSequence? = null var formattedBody: CharSequence? = null

View File

@ -25,8 +25,14 @@ import javax.inject.Inject
class CheckIfCanReplyEventUseCase @Inject constructor() { class CheckIfCanReplyEventUseCase @Inject constructor() {
fun execute(event: TimelineEvent, messageContent: MessageContent?, actionPermissions: ActionPermissions): Boolean { 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 // 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.MESSAGE) return false 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 if (!actionPermissions.canSendMessage) return false
return when (messageContent?.msgType) { return when (messageContent?.msgType) {
MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_TEXT,
@ -37,6 +43,7 @@ class CheckIfCanReplyEventUseCase @Inject constructor() {
MessageType.MSGTYPE_AUDIO, MessageType.MSGTYPE_AUDIO,
MessageType.MSGTYPE_FILE, MessageType.MSGTYPE_FILE,
MessageType.MSGTYPE_POLL_START, MessageType.MSGTYPE_POLL_START,
MessageType.MSGTYPE_POLL_END,
MessageType.MSGTYPE_BEACON_INFO, MessageType.MSGTYPE_BEACON_INFO,
MessageType.MSGTYPE_LOCATION -> true MessageType.MSGTYPE_LOCATION -> true
else -> false else -> false

View File

@ -499,6 +499,7 @@ class MessageActionsViewModel @AssistedInject constructor(
MessageType.MSGTYPE_AUDIO, MessageType.MSGTYPE_AUDIO,
MessageType.MSGTYPE_FILE, MessageType.MSGTYPE_FILE,
MessageType.MSGTYPE_POLL_START, MessageType.MSGTYPE_POLL_START,
MessageType.MSGTYPE_POLL_END,
MessageType.MSGTYPE_STICKER_LOCAL -> event.root.threadDetails?.isRootThread ?: false MessageType.MSGTYPE_STICKER_LOCAL -> event.root.threadDetails?.isRootThread ?: false
else -> false else -> false
} }
@ -530,8 +531,8 @@ class MessageActionsViewModel @AssistedInject constructor(
} }
private fun canViewReactions(event: TimelineEvent): Boolean { private fun canViewReactions(event: TimelineEvent): Boolean {
// Only event of type EventType.MESSAGE, EventType.STICKER and EventType.POLL_START are supported for the moment // 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) return false 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 return event.annotations?.reactionsSummary?.isNotEmpty() ?: false
} }

View File

@ -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.content.EncryptedEventContent
import org.matrix.android.sdk.api.session.events.model.isThread 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.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.MessageAudioContent
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent 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.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody 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.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.MessageFileContent
import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent 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.getFileUrl
import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl 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.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.settings.LightweightSettingsStorage
import org.matrix.android.sdk.api.util.MimeTypes import org.matrix.android.sdk.api.util.MimeTypes
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class MessageItemFactory @Inject constructor( class MessageItemFactory @Inject constructor(
@ -202,7 +206,8 @@ class MessageItemFactory @Inject constructor(
is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes) is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes)
is MessageAudioContent -> buildAudioContent(params, messageContent, informationData, highlight, attributes) is MessageAudioContent -> buildAudioContent(params, messageContent, informationData, highlight, attributes)
is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, 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 MessageLocationContent -> buildLocationItem(messageContent, informationData, highlight, attributes)
is MessageBeaconInfoContent -> liveLocationShareMessageItemFactory.create(event, highlight, attributes) is MessageBeaconInfoContent -> liveLocationShareMessageItemFactory.create(event, highlight, attributes)
is MessageVoiceBroadcastInfoContent -> voiceBroadcastItemFactory.create(params, messageContent, highlight, attributes) is MessageVoiceBroadcastInfoContent -> voiceBroadcastItemFactory.create(params, messageContent, highlight, attributes)
@ -245,6 +250,7 @@ class MessageItemFactory @Inject constructor(
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes, attributes: AbsMessageItem.Attributes,
isEnded: Boolean,
): PollItem { ): PollItem {
val pollViewState = pollItemViewStateFactory.create(pollContent, informationData) val pollViewState = pollItemViewStateFactory.create(pollContent, informationData)
@ -256,11 +262,35 @@ class MessageItemFactory @Inject constructor(
.votesStatus(pollViewState.votesStatus) .votesStatus(pollViewState.votesStatus)
.optionViewStates(pollViewState.optionViewStates.orEmpty()) .optionViewStates(pollViewState.optionViewStates.orEmpty())
.edited(informationData.hasBeenEdited) .edited(informationData.hasBeenEdited)
.ended(isEnded)
.highlighted(highlight) .highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
.callback(callback) .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<MessagePollContent>() ?: return null
return buildPollItem(
pollContent,
informationData,
highlight,
callback,
attributes,
isEnded = true
)
}
private fun createPollQuestion( private fun createPollQuestion(
informationData: MessageInformationData, informationData: MessageInformationData,
question: String, question: String,

View File

@ -102,6 +102,7 @@ class TimelineItemFactory @Inject constructor(
// Message itemsX // Message itemsX
EventType.STICKER, EventType.STICKER,
in EventType.POLL_START.values, in EventType.POLL_START.values,
in EventType.POLL_END.values,
EventType.MESSAGE -> messageItemFactory.create(params) EventType.MESSAGE -> messageItemFactory.create(params)
EventType.REDACTION, EventType.REDACTION,
EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_ACCEPT,
@ -114,8 +115,7 @@ class TimelineItemFactory @Inject constructor(
EventType.CALL_SELECT_ANSWER, EventType.CALL_SELECT_ANSWER,
EventType.CALL_NEGOTIATE, EventType.CALL_NEGOTIATE,
EventType.REACTION, EventType.REACTION,
in EventType.POLL_RESPONSE.values, in EventType.POLL_RESPONSE.values -> noticeItemFactory.create(params)
in EventType.POLL_END.values -> noticeItemFactory.create(params)
in EventType.BEACON_LOCATION_DATA.values -> { in EventType.BEACON_LOCATION_DATA.values -> {
if (event.root.isRedacted()) { if (event.root.isRedacted()) {
messageItemFactory.create(params) messageItemFactory.create(params)

View File

@ -17,11 +17,14 @@
package im.vector.app.features.home.room.detail.timeline.format package im.vector.app.features.home.room.detail.timeline.format
import android.content.Context import android.content.Context
import im.vector.app.R
import im.vector.app.core.utils.TextUtils 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.Event
import org.matrix.android.sdk.api.session.events.model.isAudioMessage 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.isFileMessage
import org.matrix.android.sdk.api.session.events.model.isImageMessage 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.isVideoMessage
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
@ -51,10 +54,16 @@ class EventDetailsFormatter @Inject constructor(
event.isVideoMessage() -> formatForVideoMessage(event) event.isVideoMessage() -> formatForVideoMessage(event)
event.isAudioMessage() -> formatForAudioMessage(event) event.isAudioMessage() -> formatForAudioMessage(event)
event.isFileMessage() -> formatForFileMessage(event) event.isFileMessage() -> formatForFileMessage(event)
event.isPollStart() -> formatPollMessage()
event.isPollEnd() -> formatPollEndMessage()
else -> null 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". * Example: "1024 x 720 - 670 kB".
*/ */

View File

@ -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.factory.TimelineItemFactoryParams
import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration 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.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.ReferencesInfoData
import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayoutFactory 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 session: Session,
private val dateFormatter: VectorDateFormatter, private val dateFormatter: VectorDateFormatter,
private val messageLayoutFactory: TimelineMessageLayoutFactory, private val messageLayoutFactory: TimelineMessageLayoutFactory,
private val reactionsSummaryFactory: ReactionsSummaryFactory private val reactionsSummaryFactory: ReactionsSummaryFactory,
private val pollResponseDataFactory: PollResponseDataFactory,
) { ) {
fun create(params: TimelineItemFactoryParams): MessageInformationData { fun create(params: TimelineItemFactoryParams): MessageInformationData {
@ -102,20 +101,7 @@ class MessageInformationDataFactory @Inject constructor(
memberName = event.senderInfo.disambiguatedDisplayName, memberName = event.senderInfo.disambiguatedDisplayName,
messageLayout = messageLayout, messageLayout = messageLayout,
reactionsSummary = reactionsSummaryFactory.create(event), reactionsSummary = reactionsSummaryFactory.create(event),
pollResponseAggregatedSummary = event.annotations?.pollResponseSummary?.let { pollResponseAggregatedSummary = pollResponseDataFactory.create(event),
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
)
},
hasBeenEdited = event.hasBeenEdited(), hasBeenEdited = event.hasBeenEdited(),
hasPendingEdits = event.annotations?.editSummary?.localEchos?.any() ?: false, hasPendingEdits = event.annotations?.editSummary?.localEchos?.any() ?: false,
referencesInfoData = event.annotations?.referencesAggregatedSummary?.let { referencesAggregatedSummary -> referencesInfoData = event.annotations?.referencesAggregatedSummary?.let { referencesAggregatedSummary ->

View File

@ -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
}
}
}

View File

@ -55,6 +55,7 @@ object TimelineDisplayableEvents {
VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO,
) + ) +
EventType.POLL_START.values + EventType.POLL_START.values +
EventType.POLL_END.values +
EventType.STATE_ROOM_BEACON_INFO.values + EventType.STATE_ROOM_BEACON_INFO.values +
EventType.BEACON_LOCATION_DATA.values EventType.BEACON_LOCATION_DATA.values
} }

View File

@ -85,7 +85,7 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
if (useBigFont) { if (useBigFont) {
holder.messageView.textSize = 44F holder.messageView.textSize = 44F
} else { } else {
holder.messageView.textSize = 14F holder.messageView.textSize = 15.5F
} }
if (searchForPills) { if (searchForPills) {
message?.charSequence?.findPillsAndProcess(coroutineScope) { message?.charSequence?.findPillsAndProcess(coroutineScope) {

View File

@ -19,6 +19,7 @@ package im.vector.app.features.home.room.detail.timeline.item
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R import im.vector.app.R
@ -50,6 +51,9 @@ abstract class PollItem : AbsMessageItem<PollItem.Holder>() {
@EpoxyAttribute @EpoxyAttribute
lateinit var optionViewStates: List<PollOptionViewState> lateinit var optionViewStates: List<PollOptionViewState>
@EpoxyAttribute
var ended: Boolean = false
override fun getViewStubId() = STUB_ID override fun getViewStubId() = STUB_ID
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
@ -75,6 +79,8 @@ abstract class PollItem : AbsMessageItem<PollItem.Holder>() {
it.setOnClickListener { onPollItemClick(optionViewState) } it.setOnClickListener { onPollItemClick(optionViewState) }
} }
} }
holder.endedPollTextView.isVisible = ended
} }
private fun onPollItemClick(optionViewState: PollOptionViewState) { private fun onPollItemClick(optionViewState: PollOptionViewState) {
@ -89,6 +95,7 @@ abstract class PollItem : AbsMessageItem<PollItem.Holder>() {
val questionTextView by bind<TextView>(R.id.questionTextView) val questionTextView by bind<TextView>(R.id.questionTextView)
val optionsContainer by bind<LinearLayout>(R.id.optionsContainer) val optionsContainer by bind<LinearLayout>(R.id.optionsContainer)
val votesStatusTextView by bind<TextView>(R.id.optionsVotesStatusTextView) val votesStatusTextView by bind<TextView>(R.id.optionsVotesStatusTextView)
val endedPollTextView by bind<TextView>(R.id.endedPollTextView)
} }
companion object { companion object {

View File

@ -25,6 +25,7 @@ import androidx.core.view.isVisible
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.setAttributeTintedImageResource import im.vector.app.core.extensions.setAttributeTintedImageResource
import im.vector.app.databinding.ItemPollOptionBinding import im.vector.app.databinding.ItemPollOptionBinding
import im.vector.app.features.themes.ThemeUtils
class PollOptionView @JvmOverloads constructor( class PollOptionView @JvmOverloads constructor(
context: Context, context: Context,
@ -53,35 +54,40 @@ class PollOptionView @JvmOverloads constructor(
private fun renderPollSending() { private fun renderPollSending() {
views.optionCheckImageView.isVisible = false views.optionCheckImageView.isVisible = false
views.optionWinnerImageView.isVisible = false views.optionVoteCountTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
hideVotes() hideVotes()
renderVoteSelection(false) renderVoteSelection(false)
} }
private fun renderPollEnded(state: PollOptionViewState.PollEnded) { private fun renderPollEnded(state: PollOptionViewState.PollEnded) {
views.optionCheckImageView.isVisible = false 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) showVotes(state.voteCount, state.votePercentage)
renderVoteSelection(state.isWinner) renderVoteSelection(state.isWinner)
} }
private fun renderPollReady() { private fun renderPollReady() {
views.optionCheckImageView.isVisible = true views.optionCheckImageView.isVisible = true
views.optionWinnerImageView.isVisible = false views.optionVoteCountTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
hideVotes() hideVotes()
renderVoteSelection(false) renderVoteSelection(false)
} }
private fun renderPollVoted(state: PollOptionViewState.PollVoted) { private fun renderPollVoted(state: PollOptionViewState.PollVoted) {
views.optionCheckImageView.isVisible = true views.optionCheckImageView.isVisible = true
views.optionWinnerImageView.isVisible = false views.optionVoteCountTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
showVotes(state.voteCount, state.votePercentage) showVotes(state.voteCount, state.votePercentage)
renderVoteSelection(state.isSelected) renderVoteSelection(state.isSelected)
} }
private fun renderPollUndisclosed(state: PollOptionViewState.PollUndisclosed) { private fun renderPollUndisclosed(state: PollOptionViewState.PollUndisclosed) {
views.optionCheckImageView.isVisible = true views.optionCheckImageView.isVisible = true
views.optionWinnerImageView.isVisible = false views.optionVoteCountTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
hideVotes() hideVotes()
renderVoteSelection(state.isSelected) renderVoteSelection(state.isSelected)
} }

View File

@ -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.isImageMessage
import org.matrix.android.sdk.api.session.events.model.isLiveLocation 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.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.isSticker
import org.matrix.android.sdk.api.session.events.model.isVideoMessage import org.matrix.android.sdk.api.session.events.model.isVideoMessage
import org.matrix.android.sdk.api.session.events.model.isVoiceMessage import org.matrix.android.sdk.api.session.events.model.isVoiceMessage
@ -93,10 +95,15 @@ class ProcessBodyOfReplyToEventUseCase @Inject constructor(
) )
} }
repliedToEvent.isPoll() -> { 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( matrixFormattedBody.replaceRange(
afterBreakingLineIndex, afterBreakingLineIndex,
endOfBlockQuoteIndex, endOfBlockQuoteIndex,
repliedToEvent.getPollQuestion() ?: stringProvider.getString(R.string.message_reply_to_sender_created_poll) repliedToEvent.getPollQuestion() ?: fallbackText
) )
} }
repliedToEvent.isLiveLocation() -> { repliedToEvent.isLiveLocation() -> {

View File

@ -50,6 +50,7 @@ class TimelineMessageLayoutFactory @Inject constructor(
EventType.STICKER, EventType.STICKER,
) + ) +
EventType.POLL_START.values + EventType.POLL_START.values +
EventType.POLL_END.values +
EventType.STATE_ROOM_BEACON_INFO.values EventType.STATE_ROOM_BEACON_INFO.values
// Can't be rendered in bubbles, so get back to default layout // Can't be rendered in bubbles, so get back to default layout

View File

@ -22,41 +22,33 @@ import com.airbnb.mvrx.Loading
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.RoomListDisplayMode 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.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.home.room.typing.TypingHelper
import im.vector.app.features.voicebroadcast.isLive 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.model.asVoiceBroadcastEvent
import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import org.matrix.android.sdk.api.extensions.orFalse 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.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.Membership 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.RoomSummary
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo 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 org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject import javax.inject.Inject
class RoomSummaryItemFactory @Inject constructor( class RoomSummaryItemFactory @Inject constructor(
private val sessionHolder: ActiveSessionHolder,
private val displayableEventFormatter: DisplayableEventFormatter, private val displayableEventFormatter: DisplayableEventFormatter,
private val dateFormatter: VectorDateFormatter, private val dateFormatter: VectorDateFormatter,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val typingHelper: TypingHelper, private val typingHelper: TypingHelper,
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val errorFormatter: ErrorFormatter, private val errorFormatter: ErrorFormatter,
private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase, private val getLatestPreviewableEventUseCase: GetLatestPreviewableEventUseCase,
) { ) {
fun create( fun create(
@ -142,7 +134,7 @@ class RoomSummaryItemFactory @Inject constructor(
val showSelected = selectedRoomIds.contains(roomSummary.roomId) val showSelected = selectedRoomIds.contains(roomSummary.roomId)
var latestFormattedEvent: CharSequence = "" var latestFormattedEvent: CharSequence = ""
var latestEventTime = "" var latestEventTime = ""
val latestEvent = roomSummary.getVectorLatestPreviewableEvent() val latestEvent = getLatestPreviewableEventUseCase.execute(roomSummary.roomId)
if (latestEvent != null) { if (latestEvent != null) {
latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect, roomSummary.isDirect.not()) latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect, roomSummary.isDirect.not())
latestEventTime = dateFormatter.format(latestEvent.root.originServerTs, DateFormatKind.ROOM_LIST) latestEventTime = dateFormatter.format(latestEvent.root.originServerTs, DateFormatKind.ROOM_LIST)
@ -150,7 +142,8 @@ class RoomSummaryItemFactory @Inject constructor(
val typingMessage = typingHelper.getTypingMessage(roomSummary.typingUsers) val typingMessage = typingHelper.getTypingMessage(roomSummary.typingUsers)
// Skip typing while there is a live voice broadcast // 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) { return if (subtitle.isBlank() && displayMode == RoomListDisplayMode.FILTERED) {
createCenteredRoomSummaryItem(roomSummary, displayMode, showSelected, unreadCount, onClick, onLongClick) 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) 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
}
} }

View File

@ -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
}
}

View File

@ -685,7 +685,7 @@ class LoginViewModel @AssistedInject constructor(
currentJob = viewModelScope.launch { currentJob = viewModelScope.launch {
try { try {
safeLoginWizard.login( safeLoginWizard.login(
action.username, action.username.trim(),
action.password, action.password,
action.initialDeviceName action.initialDeviceName
) )

View File

@ -19,14 +19,20 @@ package im.vector.app.features.voicebroadcast.usecase
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
import im.vector.app.features.voicebroadcast.isLive 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.VoiceBroadcastEvent
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent 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.query.QueryStringValue
import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.getRoom
import javax.inject.Inject import javax.inject.Inject
/**
* Get the list of live (not ended) voice broadcast events in the given room.
*/
class GetRoomLiveVoiceBroadcastsUseCase @Inject constructor( class GetRoomLiveVoiceBroadcastsUseCase @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder, private val activeSessionHolder: ActiveSessionHolder,
private val getVoiceBroadcastStateEventUseCase: GetVoiceBroadcastStateEventUseCase,
) { ) {
fun execute(roomId: String): List<VoiceBroadcastEvent> { fun execute(roomId: String): List<VoiceBroadcastEvent> {
@ -37,7 +43,8 @@ class GetRoomLiveVoiceBroadcastsUseCase @Inject constructor(
setOf(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO), setOf(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO),
QueryStringValue.IsNotEmpty QueryStringValue.IsNotEmpty
) )
.mapNotNull { it.asVoiceBroadcastEvent() } .mapNotNull { stateEvent -> stateEvent.asVoiceBroadcastEvent()?.voiceBroadcastId }
.mapNotNull { voiceBroadcastId -> getVoiceBroadcastStateEventUseCase.execute(VoiceBroadcast(voiceBroadcastId, roomId)) }
.filter { it.isLive } .filter { it.isLive }
} }
} }

View File

@ -32,7 +32,6 @@ import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.transformWhile import kotlinx.coroutines.flow.transformWhile
import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session 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.getRoom
import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
@ -44,6 +43,7 @@ import javax.inject.Inject
class GetVoiceBroadcastStateEventLiveUseCase @Inject constructor( class GetVoiceBroadcastStateEventLiveUseCase @Inject constructor(
private val session: Session, private val session: Session,
private val getVoiceBroadcastStateEventUseCase: GetVoiceBroadcastStateEventUseCase,
) { ) {
fun execute(voiceBroadcast: VoiceBroadcast): Flow<Optional<VoiceBroadcastEvent>> { fun execute(voiceBroadcast: VoiceBroadcast): Flow<Optional<VoiceBroadcastEvent>> {
@ -93,7 +93,7 @@ class GetVoiceBroadcastStateEventLiveUseCase @Inject constructor(
* Get a flow of the most recent related event. * Get a flow of the most recent related event.
*/ */
private fun getMostRecentRelatedEventFlow(room: Room, voiceBroadcast: VoiceBroadcast): Flow<Optional<VoiceBroadcastEvent>> { private fun getMostRecentRelatedEventFlow(room: Room, voiceBroadcast: VoiceBroadcast): Flow<Optional<VoiceBroadcastEvent>> {
val mostRecentEvent = getMostRecentRelatedEvent(room, voiceBroadcast).toOptional() val mostRecentEvent = getVoiceBroadcastStateEventUseCase.execute(voiceBroadcast).toOptional()
return if (mostRecentEvent.hasValue()) { return if (mostRecentEvent.hasValue()) {
val stateKey = mostRecentEvent.get().root.stateKey.orEmpty() val stateKey = mostRecentEvent.get().root.stateKey.orEmpty()
// observe incoming voice broadcast state events // 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. * Get a flow of the given voice broadcast event changes.
*/ */

View File

@ -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 }
}
}
}

View File

@ -124,6 +124,8 @@
app:layout_constraintEnd_toStartOf="@id/composerFullScreenButton" app:layout_constraintEnd_toStartOf="@id/composerFullScreenButton"
app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder" app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder"
app:layout_constraintTop_toBottomOf="@id/composerModeBarrier" app:layout_constraintTop_toBottomOf="@id/composerModeBarrier"
app:bulletRadius="4sp"
app:bulletGap="8sp"
tools:text="@tools:sample/lorem/random" /> tools:text="@tools:sample/lorem/random" />
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText

View File

@ -36,34 +36,23 @@
android:layout_marginStart="12dp" android:layout_marginStart="12dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginEnd="12dp" android:layout_marginEnd="12dp"
app:layout_constraintEnd_toEndOf="@id/optionWinnerImageView" app:layout_constraintEnd_toStartOf="@id/optionVoteCountTextView"
app:layout_constraintStart_toEndOf="@id/optionCheckImageView" app:layout_constraintStart_toEndOf="@id/optionCheckImageView"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="@sample/poll.json/data/answer" /> tools:text="@sample/poll.json/data/answer" />
<ImageView
android:id="@+id/optionWinnerImageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:contentDescription="@string/a11y_poll_winner_option"
android:src="@drawable/ic_poll_winner"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<TextView <TextView
android:id="@+id/optionVoteCountTextView" android:id="@+id/optionVoteCountTextView"
style="@style/Widget.Vector.TextView.Caption" style="@style/Widget.Vector.TextView.Caption"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="8dp" android:layout_marginEnd="10dp"
android:drawablePadding="6dp"
android:visibility="gone" android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/optionVoteProgress" app:layout_constraintBottom_toBottomOf="@id/optionNameTextView"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/optionVoteProgress" app:layout_constraintTop_toTopOf="@id/optionNameTextView"
tools:drawableStartCompat="@drawable/ic_poll_winner"
tools:text="@sample/poll.json/data/votes" tools:text="@sample/poll.json/data/votes"
tools:visibility="visible" /> tools:visibility="visible" />
@ -78,7 +67,7 @@
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:progressDrawable="@drawable/poll_option_progressbar_checked" android:progressDrawable="@drawable/poll_option_progressbar_checked"
app:layout_constraintBottom_toBottomOf="@id/optionBorderImageView" app:layout_constraintBottom_toBottomOf="@id/optionBorderImageView"
app:layout_constraintEnd_toStartOf="@id/optionVoteCountTextView" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/optionNameTextView" app:layout_constraintTop_toBottomOf="@id/optionNameTextView"
tools:progress="60" /> tools:progress="60" />

View File

@ -2,9 +2,21 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:minWidth="@dimen/chat_bubble_fixed_size"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:minWidth="@dimen/chat_bubble_fixed_size">
<TextView
android:id="@+id/endedPollTextView"
style="@style/Widget.Vector.TextView.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/ended_poll_indicator"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<TextView <TextView
android:id="@+id/questionTextView" android:id="@+id/questionTextView"
@ -13,11 +25,10 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:textColor="?vctr_content_primary" android:textColor="?vctr_content_primary"
android:textStyle="bold"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toBottomOf="@id/endedPollTextView"
tools:text="@sample/poll.json/question" /> tools:text="@sample/poll.json/question" />
<LinearLayout <LinearLayout

View File

@ -0,0 +1,108 @@
/*
* 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.home
import com.airbnb.mvrx.test.MavericksTestRule
import im.vector.app.features.home.room.list.home.invites.InvitesAction
import im.vector.app.features.home.room.list.home.invites.InvitesViewEvents
import im.vector.app.features.home.room.list.home.invites.InvitesViewModel
import im.vector.app.features.home.room.list.home.invites.InvitesViewState
import im.vector.app.test.fakes.FakeDrawableProvider
import im.vector.app.test.fakes.FakeSession
import im.vector.app.test.fakes.FakeStringProvider
import im.vector.app.test.fixtures.RoomSummaryFixture
import im.vector.app.test.test
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.matrix.android.sdk.api.session.room.model.Membership
class InvitesViewModelTest {
@get:Rule
val mavericksTestRule = MavericksTestRule()
private val fakeSession = FakeSession()
private val fakeStringProvider = FakeStringProvider()
private val fakeDrawableProvider = FakeDrawableProvider()
private var initialState = InvitesViewState()
private lateinit var viewModel: InvitesViewModel
private val anInvite = RoomSummaryFixture.aRoomSummary("invite")
@Before
fun setUp() {
mockkStatic("org.matrix.android.sdk.flow.FlowSessionKt")
every {
fakeSession.fakeRoomService.getPagedRoomSummariesLive(
queryParams = match {
it.memberships == listOf(Membership.INVITE)
},
pagedListConfig = any(),
sortOrder = any()
)
} returns mockk()
viewModelWith(initialState)
}
@Test
fun `when invite accepted then membership map is updated and open event posted`() = runTest {
val test = viewModel.test()
viewModel.handle(InvitesAction.AcceptInvitation(anInvite))
test.assertEvents(
InvitesViewEvents.OpenRoom(
roomSummary = anInvite,
shouldCloseInviteView = false,
isInviteAlreadySelected = true
)
).finish()
}
@Test
fun `when invite rejected then membership map is updated and open event posted`() = runTest {
coEvery { fakeSession.roomService().leaveRoom(any(), any()) } returns Unit
viewModel.handle(InvitesAction.RejectInvitation(anInvite))
coVerify {
fakeSession.roomService().leaveRoom(anInvite.roomId)
}
}
private fun viewModelWith(state: InvitesViewState) {
InvitesViewModel(
state,
session = fakeSession,
stringProvider = fakeStringProvider.instance,
drawableProvider = fakeDrawableProvider.instance,
).also {
viewModel = it
initialState = state
}
}
}

View File

@ -0,0 +1,184 @@
/*
* 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.home
import android.widget.ImageView
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.airbnb.mvrx.test.MavericksTestRule
import im.vector.app.R
import im.vector.app.core.platform.StateView
import im.vector.app.features.displayname.getBestName
import im.vector.app.features.home.room.list.home.HomeRoomListAction
import im.vector.app.features.home.room.list.home.HomeRoomListViewModel
import im.vector.app.features.home.room.list.home.HomeRoomListViewState
import im.vector.app.features.home.room.list.home.header.HomeRoomFilter
import im.vector.app.test.fakes.FakeAnalyticsTracker
import im.vector.app.test.fakes.FakeDrawableProvider
import im.vector.app.test.fakes.FakeHomeLayoutPreferencesStore
import im.vector.app.test.fakes.FakeSession
import im.vector.app.test.fakes.FakeSpaceStateHandler
import im.vector.app.test.fakes.FakeStringProvider
import im.vector.app.test.fixtures.RoomSummaryFixture.aRoomSummary
import im.vector.app.test.test
import io.mockk.every
import io.mockk.mockkStatic
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.matrix.android.sdk.api.query.SpaceFilter
import org.matrix.android.sdk.api.session.getUserOrDefault
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.flow.FlowSession
class RoomsListViewModelTest {
@get:Rule
val mavericksTestRule = MavericksTestRule()
@get:Rule
var rule = InstantTaskExecutorRule()
private val fakeSession = FakeSession()
private val fakeAnalyticsTracker = FakeAnalyticsTracker()
private val fakeStringProvider = FakeStringProvider()
private val fakeDrawableProvider = FakeDrawableProvider()
private val fakeSpaceStateHandler = FakeSpaceStateHandler()
private val fakeHomeLayoutPreferencesStore = FakeHomeLayoutPreferencesStore()
private var initialState = HomeRoomListViewState()
private lateinit var viewModel: HomeRoomListViewModel
private lateinit var fakeFLowSession: FlowSession
@Before
fun setUp() {
mockkStatic("org.matrix.android.sdk.flow.FlowSessionKt")
fakeFLowSession = fakeSession.givenFlowSession()
every { fakeSpaceStateHandler.getSelectedSpaceFlow() } returns flowOf(Optional.empty())
every { fakeSpaceStateHandler.getCurrentSpace() } returns null
every { fakeFLowSession.liveRoomSummaries(any(), any()) } returns flowOf(emptyList())
val roomA = aRoomSummary("room_a")
val roomB = aRoomSummary("room_b")
val roomC = aRoomSummary("room_c")
val allRooms = listOf(roomA, roomB, roomC)
every {
fakeFLowSession.liveRoomSummaries(
match {
it.roomCategoryFilter == null &&
it.roomTagQueryFilter == null &&
it.memberships == listOf(Membership.JOIN) &&
it.spaceFilter is SpaceFilter.NoFilter
}, any()
)
} returns flowOf(allRooms)
viewModelWith(initialState)
}
@Test
fun `when recents are enabled then updates state`() = runTest {
val fakeFLowSession = fakeSession.givenFlowSession()
every { fakeFLowSession.liveRoomSummaries(any()) } returns flowOf(emptyList())
val test = viewModel.test()
val roomA = aRoomSummary("room_a")
val roomB = aRoomSummary("room_b")
val roomC = aRoomSummary("room_c")
val recentRooms = listOf(roomA, roomB, roomC)
every { fakeFLowSession.liveBreadcrumbs(any()) } returns flowOf(recentRooms)
fakeHomeLayoutPreferencesStore.givenRecentsEnabled(true)
val userName = fakeSession.getUserOrDefault(fakeSession.myUserId).toMatrixItem().getBestName()
val allEmptyState = StateView.State.Empty(
title = fakeStringProvider.instance.getString(R.string.home_empty_no_rooms_title, userName),
message = fakeStringProvider.instance.getString(R.string.home_empty_no_rooms_message),
image = fakeDrawableProvider.instance.getDrawable(R.drawable.ill_empty_all_chats),
isBigImage = true
)
test.assertLatestState(
initialState.copy(emptyState = allEmptyState, headersData = initialState.headersData.copy(recents = recentRooms))
)
}
@Test
fun `when filter tabs are enabled then updates state`() = runTest {
val test = viewModel.test()
fakeHomeLayoutPreferencesStore.givenFiltersEnabled(true)
val filtersData = mutableListOf(
HomeRoomFilter.ALL,
HomeRoomFilter.UNREADS
)
val userName = fakeSession.getUserOrDefault(fakeSession.myUserId).toMatrixItem().getBestName()
val allEmptyState = StateView.State.Empty(
title = fakeStringProvider.instance.getString(R.string.home_empty_no_rooms_title, userName),
message = fakeStringProvider.instance.getString(R.string.home_empty_no_rooms_message),
image = fakeDrawableProvider.instance.getDrawable(R.drawable.ill_empty_all_chats),
isBigImage = true
)
test.assertLatestState(
initialState.copy(emptyState = allEmptyState, headersData = initialState.headersData.copy(filtersList = filtersData))
)
}
@Test
fun `when filter tab is selected then updates state`() = runTest {
val test = viewModel.test()
val aFilter = HomeRoomFilter.UNREADS
viewModel.handle(HomeRoomListAction.ChangeRoomFilter(filter = aFilter))
val unreadsEmptyState = StateView.State.Empty(
title = fakeStringProvider.instance.getString(R.string.home_empty_no_unreads_title),
message = fakeStringProvider.instance.getString(R.string.home_empty_no_unreads_message),
image = fakeDrawableProvider.instance.getDrawable(R.drawable.ill_empty_unreads),
isBigImage = true,
imageScaleType = ImageView.ScaleType.CENTER_INSIDE
)
test.assertLatestState(
initialState.copy(emptyState = unreadsEmptyState, headersData = initialState.headersData.copy(currentFilter = aFilter))
)
}
private fun viewModelWith(state: HomeRoomListViewState) {
HomeRoomListViewModel(
state,
session = fakeSession,
spaceStateHandler = fakeSpaceStateHandler,
preferencesStore = fakeHomeLayoutPreferencesStore.instance,
stringProvider = fakeStringProvider.instance,
drawableProvider = fakeDrawableProvider.instance,
analyticsTracker = fakeAnalyticsTracker
).also {
viewModel = it
initialState = state
}
}
}

View File

@ -43,7 +43,7 @@ class CheckIfCanReplyEventUseCaseTest {
@Test @Test
fun `given reply is allowed for the event type when use case is executed then result is true`() { fun `given reply is allowed for the event type when use case is executed then result is true`() {
val eventTypes = EventType.STATE_ROOM_BEACON_INFO.values + EventType.POLL_START.values + EventType.MESSAGE val eventTypes = EventType.STATE_ROOM_BEACON_INFO.values + EventType.POLL_START.values + EventType.POLL_END.values + EventType.MESSAGE
eventTypes.forEach { eventType -> eventTypes.forEach { eventType ->
val event = givenAnEvent(eventType) val event = givenAnEvent(eventType)
@ -78,6 +78,7 @@ class CheckIfCanReplyEventUseCaseTest {
MessageType.MSGTYPE_AUDIO, MessageType.MSGTYPE_AUDIO,
MessageType.MSGTYPE_FILE, MessageType.MSGTYPE_FILE,
MessageType.MSGTYPE_POLL_START, MessageType.MSGTYPE_POLL_START,
MessageType.MSGTYPE_POLL_END,
MessageType.MSGTYPE_BEACON_INFO, MessageType.MSGTYPE_BEACON_INFO,
MessageType.MSGTYPE_LOCATION MessageType.MSGTYPE_LOCATION
) )

View File

@ -29,6 +29,7 @@ import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.getPollQuestion 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.isAudioMessage
import org.matrix.android.sdk.api.session.events.model.isFileMessage import org.matrix.android.sdk.api.session.events.model.isFileMessage
@ -158,6 +159,7 @@ class ProcessBodyOfReplyToEventUseCaseTest {
// Given // Given
givenTypeOfRepliedEvent(isPollMessage = true) givenTypeOfRepliedEvent(isPollMessage = true)
givenNewContentForId(R.string.message_reply_to_sender_created_poll) givenNewContentForId(R.string.message_reply_to_sender_created_poll)
every { fakeRepliedEvent.getClearType() } returns EventType.POLL_START.unstable
every { fakeRepliedEvent.getPollQuestion() } returns null every { fakeRepliedEvent.getPollQuestion() } returns null
executeAndAssertResult() executeAndAssertResult()
@ -168,11 +170,23 @@ class ProcessBodyOfReplyToEventUseCaseTest {
// Given // Given
givenTypeOfRepliedEvent(isPollMessage = true) givenTypeOfRepliedEvent(isPollMessage = true)
givenNewContentForId(R.string.message_reply_to_sender_created_poll) givenNewContentForId(R.string.message_reply_to_sender_created_poll)
every { fakeRepliedEvent.getClearType() } returns EventType.POLL_START.unstable
every { fakeRepliedEvent.getPollQuestion() } returns A_NEW_CONTENT every { fakeRepliedEvent.getPollQuestion() } returns A_NEW_CONTENT
executeAndAssertResult() 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 @Test
fun `given a replied event of type live location message when process the formatted body then content is replaced by correct string`() { fun `given a replied event of type live location message when process the formatted body then content is replaced by correct string`() {
// Given // Given

View File

@ -0,0 +1,196 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.list.usecase
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants.VOICE_BROADCAST_CHUNK_KEY
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase
import im.vector.app.test.fakes.FakeActiveSessionHolder
import im.vector.app.test.fakes.FakeRoom
import io.mockk.every
import io.mockk.mockk
import org.amshove.kluent.shouldBe
import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldBeNull
import org.junit.Before
import org.junit.Test
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
private const val A_ROOM_ID = "a-room-id"
internal class GetLatestPreviewableEventUseCaseTest {
private val fakeRoom = FakeRoom()
private val fakeSessionHolder = FakeActiveSessionHolder()
private val fakeRoomSummary = mockk<RoomSummary>()
private val fakeGetRoomLiveVoiceBroadcastsUseCase = mockk<GetRoomLiveVoiceBroadcastsUseCase>()
private val getLatestPreviewableEventUseCase = GetLatestPreviewableEventUseCase(
fakeSessionHolder.instance,
fakeGetRoomLiveVoiceBroadcastsUseCase,
)
@Before
fun setup() {
every { fakeSessionHolder.instance.getSafeActiveSession()?.getRoom(A_ROOM_ID) } returns fakeRoom
every { fakeRoom.roomSummary() } returns fakeRoomSummary
every { fakeRoom.roomId } returns A_ROOM_ID
every { fakeRoom.timelineService().getTimelineEvent(any()) } answers {
mockk(relaxed = true) {
every { eventId } returns firstArg()
}
}
}
@Test
fun `given the latest event is a call invite and there is a live broadcast, when execute, returns the call event`() {
// Given
val aLatestPreviewableEvent = mockk<TimelineEvent> {
every { root.type } returns EventType.MESSAGE
every { root.getClearType() } returns EventType.CALL_INVITE
}
every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent
every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns listOf(
givenAVoiceBroadcastEvent("id1", VoiceBroadcastState.STARTED, "id1"),
givenAVoiceBroadcastEvent("id2", VoiceBroadcastState.RESUMED, "id1"),
).mapNotNull { it.asVoiceBroadcastEvent() }
// When
val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID)
// Then
result shouldBe aLatestPreviewableEvent
}
@Test
fun `given the latest event is not a call invite and there is a live broadcast, when execute, returns the latest broadcast event`() {
// Given
val aLatestPreviewableEvent = mockk<TimelineEvent> {
every { root.type } returns EventType.MESSAGE
every { root.getClearType() } returns EventType.MESSAGE
}
every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent
every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns listOf(
givenAVoiceBroadcastEvent("id1", VoiceBroadcastState.STARTED, "vb_id1"),
givenAVoiceBroadcastEvent("id2", VoiceBroadcastState.RESUMED, "vb_id2"),
).mapNotNull { it.asVoiceBroadcastEvent() }
// When
val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID)
// Then
result?.eventId shouldBeEqualTo "vb_id2"
}
@Test
fun `given there is no live broadcast, when execute, returns the latest event`() {
// Given
val aLatestPreviewableEvent = mockk<TimelineEvent> {
every { root.type } returns EventType.MESSAGE
every { root.getClearType() } returns EventType.MESSAGE
}
every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent
every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns emptyList()
// When
val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID)
// Then
result shouldBe aLatestPreviewableEvent
}
@Test
fun `given there is no live broadcast and the latest event is a vb message, when execute, returns null`() {
// Given
val aLatestPreviewableEvent = mockk<TimelineEvent> {
every { root.type } returns EventType.MESSAGE
every { root.getClearType() } returns EventType.MESSAGE
every { root.getClearContent() } returns mapOf(
MessageContent.MSG_TYPE_JSON_KEY to "m.audio",
VOICE_BROADCAST_CHUNK_KEY to "1",
"body" to "",
)
}
every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent
every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns emptyList()
// When
val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID)
// Then
result.shouldBeNull()
}
@Test
fun `given the latest event is an ended vb, when execute, returns the stopped event`() {
// Given
val aLatestPreviewableEvent = mockk<TimelineEvent> {
every { eventId } returns "id1"
every { root } returns givenAVoiceBroadcastEvent("id1", VoiceBroadcastState.STOPPED, "vb_id1")
}
every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent
every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns emptyList()
// When
val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID)
// Then
result?.eventId shouldBeEqualTo "id1"
}
@Test
fun `given the latest event is a resumed vb, when execute, returns the started event`() {
// Given
val aLatestPreviewableEvent = mockk<TimelineEvent> {
every { eventId } returns "id1"
every { root } returns givenAVoiceBroadcastEvent("id1", VoiceBroadcastState.RESUMED, "vb_id1")
}
every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent
every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns emptyList()
// When
val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID)
// Then
result?.eventId shouldBeEqualTo "vb_id1"
}
private fun givenAVoiceBroadcastEvent(
eventId: String,
state: VoiceBroadcastState,
voiceBroadcastId: String,
): Event = mockk {
every { this@mockk.eventId } returns eventId
every { getClearType() } returns VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO
every { type } returns VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO
every { content } returns mapOf(
"state" to state.value,
"m.relates_to" to mapOf(
"rel_type" to RelationType.REFERENCE,
"event_id" to voiceBroadcastId
)
)
}
}

View File

@ -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<TimelineEvent> {
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
}
}

View File

@ -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<DrawableProvider>()
init {
every { instance.getDrawable(any()) } returns mockk()
every { instance.getDrawable(any(), any()) } returns mockk()
}
}

View File

@ -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<Boolean>()
private val _areFiltersEnabledFlow = MutableSharedFlow<Boolean>()
private val _isAZOrderingEnabledFlow = MutableSharedFlow<Boolean>()
val instance = mockk<HomeLayoutPreferencesStore>(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)
}
}

View File

@ -30,4 +30,8 @@ class FakeRoomService(
fun getRoomSummaryReturns(roomSummary: RoomSummary?) { fun getRoomSummaryReturns(roomSummary: RoomSummary?) {
every { getRoomSummary(any()) } returns roomSummary every { getRoomSummary(any()) } returns roomSummary
} }
fun set(roomSummary: RoomSummary?) {
every { getRoomSummary(any()) } returns roomSummary
}
} }

View File

@ -42,6 +42,7 @@ class FakeSession(
val fakeSharedSecretStorageService: FakeSharedSecretStorageService = FakeSharedSecretStorageService(), val fakeSharedSecretStorageService: FakeSharedSecretStorageService = FakeSharedSecretStorageService(),
val fakeRoomService: FakeRoomService = FakeRoomService(), val fakeRoomService: FakeRoomService = FakeRoomService(),
val fakePushersService: FakePushersService = FakePushersService(), val fakePushersService: FakePushersService = FakePushersService(),
val fakeUserService: FakeUserService = FakeUserService(),
private val fakeEventService: FakeEventService = FakeEventService(), private val fakeEventService: FakeEventService = FakeEventService(),
val fakeSessionAccountDataService: FakeSessionAccountDataService = FakeSessionAccountDataService() val fakeSessionAccountDataService: FakeSessionAccountDataService = FakeSessionAccountDataService()
) : Session by mockk(relaxed = true) { ) : Session by mockk(relaxed = true) {
@ -62,6 +63,7 @@ class FakeSession(
override fun eventService() = fakeEventService override fun eventService() = fakeEventService
override fun pushersService() = fakePushersService override fun pushersService() = fakePushersService
override fun accountDataService() = fakeSessionAccountDataService override fun accountDataService() = fakeSessionAccountDataService
override fun userService() = fakeUserService
fun givenVectorStore(vectorSessionStore: VectorSessionStore) { fun givenVectorStore(vectorSessionStore: VectorSessionStore) {
coEvery { 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. * Do not forget to call mockkStatic("org.matrix.android.sdk.flow.FlowSessionKt") in the setup method of the tests.
*/ */
@SuppressWarnings("all")
fun givenFlowSession(): FlowSession { fun givenFlowSession(): FlowSession {
val fakeFlowSession = mockk<FlowSession>() val fakeFlowSession = mockk<FlowSession>()
every { flow() } returns fakeFlowSession every { flow() } returns fakeFlowSession
return fakeFlowSession return fakeFlowSession
} }

View File

@ -17,6 +17,7 @@
package im.vector.app.test.fakes package im.vector.app.test.fakes
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import io.mockk.InternalPlatformDsl.toStr
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
@ -27,6 +28,9 @@ class FakeStringProvider {
every { instance.getString(any()) } answers { every { instance.getString(any()) } answers {
"test-${args[0]}" "test-${args[0]}"
} }
every { instance.getString(any(), any()) } answers {
"test-${args[0]}-${args[1].toStr()}"
}
every { instance.getQuantityString(any(), any(), any()) } answers { every { instance.getQuantityString(any(), any(), any()) } answers {
"test-${args[0]}-${args[1]}" "test-${args[0]}-${args[1]}"

View File

@ -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<String>()
init {
every { getUser(capture(userIdSlot)) } answers { User(userId = userIdSlot.captured) }
}
}