mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-01-31 04:03:59 +03:00
Merge branch 'develop' into feature/bca/rust_flavor
This commit is contained in:
commit
c0397875f0
72 changed files with 1201 additions and 152 deletions
2
.github/workflows/danger.yml
vendored
2
.github/workflows/danger.yml
vendored
|
@ -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:
|
||||||
|
|
2
.github/workflows/quality.yml
vendored
2
.github/workflows/quality.yml
vendored
|
@ -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:
|
||||||
|
|
27
CHANGES.md
27
CHANGES.md
|
@ -1,3 +1,30 @@
|
||||||
|
Changes in Element v1.5.20 (2023-01-10)
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
Features ✨
|
||||||
|
----------
|
||||||
|
- "[Rich text editor] Add list formatting buttons to the rich text editor" ([#7887](https://github.com/vector-im/element-android/issues/7887))
|
||||||
|
|
||||||
|
Bugfixes 🐛
|
||||||
|
----------
|
||||||
|
- ReplyTo are not updated if the original message is edited or deleted. ([#5546](https://github.com/vector-im/element-android/issues/5546))
|
||||||
|
- Observe ViewEvents only when resumed and ensure ViewEvents are not lost. ([#7724](https://github.com/vector-im/element-android/issues/7724))
|
||||||
|
- [Session manager] Missing info when a session does not support encryption ([#7853](https://github.com/vector-im/element-android/issues/7853))
|
||||||
|
- Reduce number of crypto database transactions when handling the sync response ([#7879](https://github.com/vector-im/element-android/issues/7879))
|
||||||
|
- [Voice Broadcast] Stop listening if we reach the last received chunk and there is no last sequence number ([#7899](https://github.com/vector-im/element-android/issues/7899))
|
||||||
|
- Handle network error on API `rooms/{roomId}/threads` ([#7913](https://github.com/vector-im/element-android/issues/7913))
|
||||||
|
|
||||||
|
In development 🚧
|
||||||
|
----------------
|
||||||
|
- [Poll] Render active polls list of a room
|
||||||
|
- [Poll] Render past polls list of a room ([#7864](https://github.com/vector-im/element-android/issues/7864))
|
||||||
|
|
||||||
|
Other changes
|
||||||
|
-------------
|
||||||
|
- fix: increase font size for messages ([#5717](https://github.com/vector-im/element-android/issues/5717))
|
||||||
|
- Add trim to username input on the app side and SDK side when sign-in ([#7111](https://github.com/vector-im/element-android/issues/7111))
|
||||||
|
|
||||||
|
|
||||||
Changes in Element v1.5.18 (2023-01-02)
|
Changes in Element v1.5.18 (2023-01-02)
|
||||||
=======================================
|
=======================================
|
||||||
|
|
||||||
|
|
|
@ -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
5
SECURITY.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# Reporting a Vulnerability
|
||||||
|
|
||||||
|
**If you've found a security vulnerability, please report it to security@matrix.org**
|
||||||
|
|
||||||
|
For more information on our security disclosure policy, visit https://www.matrix.org/security-disclosure-policy/
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
ReplyTo are not updated if the original message is edited or deleted.
|
|
|
@ -1 +0,0 @@
|
||||||
Observe ViewEvents only when resumed and ensure ViewEvents are not lost.
|
|
1
changelog.d/7832.bugfix
Normal file
1
changelog.d/7832.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[Voice Broadcast] Fix unexpected "live broadcast" in the room list
|
|
@ -1 +0,0 @@
|
||||||
[Session manager] Missing info when a session does not support encryption
|
|
|
@ -1,2 +0,0 @@
|
||||||
[Poll] Render active polls list of a room
|
|
||||||
[Poll] Render past polls list of a room
|
|
|
@ -1 +0,0 @@
|
||||||
Reduce number of crypto database transactions when handling the sync response
|
|
|
@ -1 +0,0 @@
|
||||||
"[Rich text editor] Add list formatting buttons to the rich text editor"
|
|
|
@ -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
1
changelog.d/7900.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Render ended polls
|
|
@ -1 +0,0 @@
|
||||||
Handle network error on API `rooms/{roomId}/threads`
|
|
1
changelog.d/7930.feature
Normal file
1
changelog.d/7930.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"[Rich text editor] Update list item bullet appearance"
|
1
changelog.d/7936.misc
Normal file
1
changelog.d/7936.misc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Upgrade to Kotlin 1.8
|
|
@ -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']
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
2
fastlane/metadata/android/en-US/changelogs/40105200.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40105200.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Main changes in this version: Mainly bugfixing!
|
||||||
|
Full changelog: https://github.com/vector-im/element-android/releases
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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".
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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 ->
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() -> {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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,9 +67,9 @@
|
||||||
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" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]}"
|
||||||
|
|
|
@ -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) }
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue