Merge tag 'v1.5.32' into sc

Note: mainly conflicts in differing implementations to fix duplicate
read receipts.

After a quick glance at the upstream implementation, my first impression
is that we likely want to keep our downstream implementation:
- Better performance by checking for the more recent receipt in the SDK
  upon receipt receival, instead on rendering the timeline
- null seems to always map to main, which caused us jumping receipts in
  the past, which we have addressed by now

Conflicts:
	matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
	vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
	vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
	vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt

Change-Id: I47ae2afe5fcbe77a4fec69374d83821b94a431de
This commit is contained in:
SpiritCroc 2023-04-20 10:50:35 +02:00
commit acdf419768
33 changed files with 227 additions and 105 deletions

View File

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

View File

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

View File

@ -23,7 +23,7 @@ jobs:
- name: Run Emoji script - name: Run Emoji script
run: ./tools/import_emojis.py run: ./tools/import_emojis.py
- name: Create Pull Request for Emojis - name: Create Pull Request for Emojis
uses: peter-evans/create-pull-request@v4 uses: peter-evans/create-pull-request@v5
with: with:
commit-message: Sync Emojis commit-message: Sync Emojis
title: Sync Emojis title: Sync Emojis
@ -49,7 +49,7 @@ jobs:
- name: Run SAS String script - name: Run SAS String script
run: ./tools/import_sas_strings.py run: ./tools/import_sas_strings.py
- name: Create Pull Request for SAS Strings - name: Create Pull Request for SAS Strings
uses: peter-evans/create-pull-request@v4 uses: peter-evans/create-pull-request@v5
with: with:
commit-message: Sync SAS Strings commit-message: Sync SAS Strings
title: Sync SAS Strings title: Sync SAS Strings
@ -68,7 +68,7 @@ jobs:
- name: Run analytics import script - name: Run analytics import script
run: ./tools/import_analytic_plan.sh run: ./tools/import_analytic_plan.sh
- name: Create Pull Request for analytics plan - name: Create Pull Request for analytics plan
uses: peter-evans/create-pull-request@v4 uses: peter-evans/create-pull-request@v5
with: with:
commit-message: Sync analytics plan commit-message: Sync analytics plan
title: Sync analytics plan title: Sync analytics plan

View File

@ -1,3 +1,13 @@
Changes in Element v1.5.32 (2023-04-19)
=======================================
Bugfixes 🐛
----------
- Fix multiple read receipts for the same user in timeline. ([#7882](https://github.com/vector-im/element-android/issues/7882))
- The new permalink rendering is not applied on permalink created with the potential clientPermalinkBaseUrl ([#8307](https://github.com/vector-im/element-android/issues/8307))
- Keep screen on while recording voicebroadcast ([#8313](https://github.com/vector-im/element-android/issues/8313))
Changes in Element v1.5.30 (2023-04-05) Changes in Element v1.5.30 (2023-04-05)
======================================= =======================================

View File

@ -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.188.0" def flipper = "0.189.0"
def epoxy = "5.0.0" def epoxy = "5.0.0"
def mavericks = "3.0.2" def mavericks = "3.0.2"
def glide = "4.14.2" def glide = "4.14.2"
@ -27,7 +27,7 @@ def jjwt = "0.11.5"
def vanniktechEmoji = "0.16.0" def vanniktechEmoji = "0.16.0"
def sentry = "6.17.0" def sentry = "6.17.0"
// Use 1.6.0 alpha to fix issue with test // Use 1.6.0 alpha to fix issue with test
def fragment = "1.6.0-alpha08" def fragment = "1.6.0-alpha09"
// 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.5.1" def espresso = "3.5.1"
@ -51,14 +51,14 @@ ext.libs = [
'activity' : "androidx.activity:activity-ktx:1.7.0", 'activity' : "androidx.activity:activity-ktx:1.7.0",
'appCompat' : "androidx.appcompat:appcompat:1.6.1", 'appCompat' : "androidx.appcompat:appcompat:1.6.1",
'biometric' : "androidx.biometric:biometric:1.1.0", 'biometric' : "androidx.biometric:biometric:1.1.0",
'core' : "androidx.core:core-ktx:1.9.0", 'core' : "androidx.core:core-ktx:1.10.0",
'recyclerview' : "androidx.recyclerview:recyclerview:1.3.0", 'recyclerview' : "androidx.recyclerview:recyclerview:1.3.0",
'exifinterface' : "androidx.exifinterface:exifinterface:1.3.6", 'exifinterface' : "androidx.exifinterface:exifinterface:1.3.6",
'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", '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.8.0", 'work' : "androidx.work:work-runtime-ktx:2.8.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.5", 'junit' : "androidx.test.ext:junit:1.1.5",
@ -87,7 +87,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.8" 'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.13.10"
], ],
dagger : [ dagger : [
'dagger' : "com.google.dagger:dagger:$dagger", 'dagger' : "com.google.dagger:dagger:$dagger",

View File

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

View File

@ -62,7 +62,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.30\"" buildConfigField "String", "SDK_VERSION", "\"1.5.32\""
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()}\""

View File

@ -48,4 +48,8 @@ class TestPermalinkService : PermalinkService {
MARKDOWN -> "[%2\$s](https://matrix.to/#/%1\$s)" MARKDOWN -> "[%2\$s](https://matrix.to/#/%1\$s)"
} }
} }
override fun isPermalinkSupported(supportedHosts: Array<String>, url: String): Boolean {
return false
}
} }

View File

@ -62,7 +62,7 @@ object MatrixPatterns {
// regex pattern to find permalink with message id. // regex pattern to find permalink with message id.
// Android does not support in URL so extract it. // Android does not support in URL so extract it.
private const val PERMALINK_BASE_REGEX = "https://matrix\\.to/#/" private const val PERMALINK_BASE_REGEX = "https://matrix\\.to/#/"
private const val APP_BASE_REGEX = "https://[A-Z0-9.-]+\\.[A-Z]{2,}/[A-Z]{3,}/#/room/" private const val APP_BASE_REGEX = "https://[A-Z0-9.-]+\\.[A-Z]{2,}/#/(room|user)/"
const val SEP_REGEX = "/" const val SEP_REGEX = "/"
private val PATTERN_CONTAIN_MATRIX_TO_PERMALINK = PERMALINK_BASE_REGEX.toRegex(RegexOption.IGNORE_CASE) private val PATTERN_CONTAIN_MATRIX_TO_PERMALINK = PERMALINK_BASE_REGEX.toRegex(RegexOption.IGNORE_CASE)

View File

@ -97,4 +97,15 @@ interface PermalinkService {
* @return the created template * @return the created template
*/ */
fun createMentionSpanTemplate(type: SpanTemplateType, forceMatrixTo: Boolean = false): String fun createMentionSpanTemplate(type: SpanTemplateType, forceMatrixTo: Boolean = false): String
/**
* Check if the url is a permalink. It must be a matrix.to link
* or a link with host provided by the string-array `permalink_supported_hosts` in the config file
*
* @param supportedHosts the list of hosts supported for permalinks
* @param url the link to check, Ex: "https://matrix.to/#/@benoit:matrix.org"
*
* @return true when url is a permalink
*/
fun isPermalinkSupported(supportedHosts: Array<String>, url: String): Boolean
} }

View File

@ -23,6 +23,7 @@ import io.realm.kotlin.createObject
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.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.read.ReadService
import org.matrix.android.sdk.api.session.room.read.ReadService.Companion.THREAD_ID_MAIN import org.matrix.android.sdk.api.session.room.read.ReadService.Companion.THREAD_ID_MAIN
import org.matrix.android.sdk.api.session.room.read.ReadService.Companion.THREAD_ID_MAIN_OR_NULL import org.matrix.android.sdk.api.session.room.read.ReadService.Companion.THREAD_ID_MAIN_OR_NULL
import org.matrix.android.sdk.internal.crypto.model.SessionInfo import org.matrix.android.sdk.internal.crypto.model.SessionInfo
@ -80,7 +81,7 @@ internal fun ChunkEntity.addTimelineEvent(
val senderId = eventEntity.sender ?: "" val senderId = eventEntity.sender ?: ""
// Update RR for the sender of a new message with a dummy one // Update RR for the sender of a new message with a dummy one
val readReceiptsSummaryEntity = if (!ownedByThreadChunk) handleReadReceipts(realm, roomId, eventEntity, senderId) else null val readReceiptsSummaryEntity = handleReadReceiptsOfSender(realm, roomId, eventEntity, senderId)
val timelineEventEntity = realm.createObject<TimelineEventEntity>().apply { val timelineEventEntity = realm.createObject<TimelineEventEntity>().apply {
this.localId = localId this.localId = localId
this.root = eventEntity this.root = eventEntity
@ -130,7 +131,7 @@ internal fun computeIsUnique(
private val rrDimber = Dimber("ReadReceipts", DbgUtil.DBG_READ_RECEIPTS) private val rrDimber = Dimber("ReadReceipts", DbgUtil.DBG_READ_RECEIPTS)
private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventEntity, senderId: String): ReadReceiptsSummaryEntity { private fun handleReadReceiptsOfSender(realm: Realm, roomId: String, eventEntity: EventEntity, senderId: String): ReadReceiptsSummaryEntity {
val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventEntity.eventId).findFirst() val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventEntity.eventId).findFirst()
?: realm.createObject<ReadReceiptsSummaryEntity>(eventEntity.eventId).apply { ?: realm.createObject<ReadReceiptsSummaryEntity>(eventEntity.eventId).apply {
this.roomId = roomId this.roomId = roomId
@ -140,12 +141,20 @@ private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventE
val timestampOfEvent = originServerTs.toDouble() val timestampOfEvent = originServerTs.toDouble()
// SC: fight duplicate read receipts in main timeline // SC: fight duplicate read receipts in main timeline
val receiptDestinations = if (eventEntity.rootThreadEventId in listOf(null, THREAD_ID_MAIN)) { val receiptDestinations = if (eventEntity.rootThreadEventId in listOf(null, THREAD_ID_MAIN)) {
// Upstream v1.3.32 now does ?: THREAD_ID_MAIN for this... but that might cause some issues with jumping receipts
// that we had before, so stick to our approach for now...?
// setOf(eventEntity.rootThreadEventId ?: THREAD_ID_MAIN, THREAD_ID_MAIN_OR_NULL)
setOf(eventEntity.rootThreadEventId, THREAD_ID_MAIN_OR_NULL) setOf(eventEntity.rootThreadEventId, THREAD_ID_MAIN_OR_NULL)
} else { } else {
setOf(eventEntity.rootThreadEventId) setOf(eventEntity.rootThreadEventId)
} }
receiptDestinations.forEach { rootThreadEventId -> receiptDestinations.forEach { rootThreadEventId ->
val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId, threadId = rootThreadEventId) val readReceiptOfSender = ReadReceiptEntity.getOrCreate(
realm = realm,
roomId = roomId,
userId = senderId,
threadId = rootThreadEventId
)
val shouldForceMon: Boolean val shouldForceMon: Boolean
val shouldSkipMon: Boolean val shouldSkipMon: Boolean
if (rootThreadEventId == THREAD_ID_MAIN_OR_NULL) { if (rootThreadEventId == THREAD_ID_MAIN_OR_NULL) {

View File

@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.session.permalinks package org.matrix.android.sdk.internal.session.permalinks
import androidx.core.net.toUri
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.permalinks.PermalinkService import org.matrix.android.sdk.api.session.permalinks.PermalinkService
import javax.inject.Inject import javax.inject.Inject
@ -47,4 +48,9 @@ internal class DefaultPermalinkService @Inject constructor(
override fun createMentionSpanTemplate(type: PermalinkService.SpanTemplateType, forceMatrixTo: Boolean): String { override fun createMentionSpanTemplate(type: PermalinkService.SpanTemplateType, forceMatrixTo: Boolean): String {
return permalinkFactory.createMentionSpanTemplate(type, forceMatrixTo) return permalinkFactory.createMentionSpanTemplate(type, forceMatrixTo)
} }
override fun isPermalinkSupported(supportedHosts: Array<String>, url: String): Boolean {
return url.startsWith(PermalinkService.MATRIX_TO_URL_BASE) ||
supportedHosts.any { url.toUri().host == it }
}
} }

View File

@ -2599,7 +2599,9 @@
"face", "face",
"shaking", "shaking",
"shock", "shock",
"vibrate" "vibrate",
"dizzy",
"blurry"
] ]
}, },
"relieved-face": { "relieved-face": {
@ -3800,7 +3802,8 @@
"heart", "heart",
"like", "like",
"love", "love",
"pink" "pink",
"valentines"
] ]
}, },
"orange-heart": { "orange-heart": {
@ -3854,8 +3857,9 @@
"cyan", "cyan",
"heart", "heart",
"light blue", "light blue",
"light blue heart", "teal",
"teal" "ice",
"baby blue"
] ]
}, },
"purple-heart": { "purple-heart": {
@ -3892,10 +3896,10 @@
"b": "1FA76", "b": "1FA76",
"j": [ "j": [
"gray", "gray",
"grey heart",
"heart", "heart",
"silver", "silver",
"slate" "slate",
"monochrome"
] ]
}, },
"white-heart": { "white-heart": {
@ -4198,11 +4202,12 @@
"j": [ "j": [
"high five", "high five",
"leftward", "leftward",
"leftwards pushing hand",
"push", "push",
"refuse", "refuse",
"stop", "stop",
"wait" "wait",
"highfive",
"pressing"
] ]
}, },
"rightwards-pushing-hand": { "rightwards-pushing-hand": {
@ -4213,9 +4218,10 @@
"push", "push",
"refuse", "refuse",
"rightward", "rightward",
"rightwards pushing hand",
"stop", "stop",
"wait" "wait",
"highfive",
"pressing"
] ]
}, },
"ok-hand": { "ok-hand": {
@ -8620,7 +8626,11 @@
"antlers", "antlers",
"elk", "elk",
"mammal", "mammal",
"moose" "shrek",
"canada",
"sweden",
"sven",
"cool"
] ]
}, },
"donkey": { "donkey": {
@ -8630,10 +8640,10 @@
"animal", "animal",
"ass", "ass",
"burro", "burro",
"donkey",
"mammal", "mammal",
"mule", "mule",
"stubborn" "stubborn",
"eeyore"
] ]
}, },
"horse": { "horse": {
@ -9311,7 +9321,8 @@
"bird", "bird",
"flying", "flying",
"mythology", "mythology",
"wing" "angel",
"birds"
] ]
}, },
"black-bird": { "black-bird": {
@ -9331,9 +9342,10 @@
"j": [ "j": [
"bird", "bird",
"fowl", "fowl",
"goose",
"honk", "honk",
"silly" "silly",
"jemima",
"goosebumps"
] ]
}, },
"frog": { "frog": {
@ -9581,10 +9593,11 @@
"burn", "burn",
"invertebrate", "invertebrate",
"jelly", "jelly",
"jellyfish",
"marine", "marine",
"ouch", "ouch",
"stinger" "stinger",
"sting",
"tentacles"
] ]
}, },
"snail": { "snail": {
@ -9885,7 +9898,6 @@
"j": [ "j": [
"bluebonnet", "bluebonnet",
"flower", "flower",
"hyacinth",
"lavender", "lavender",
"lupine", "lupine",
"snapdragon" "snapdragon"
@ -10423,9 +10435,11 @@
"b": "1FADA", "b": "1FADA",
"j": [ "j": [
"beer", "beer",
"ginger root",
"root", "root",
"spice" "spice",
"yellow",
"cooking",
"gingerbread"
] ]
}, },
"pea-pod": { "pea-pod": {
@ -10437,7 +10451,9 @@
"legume", "legume",
"pea", "pea",
"pod", "pod",
"vegetable" "vegetable",
"cozy",
"green"
] ]
}, },
"bread": { "bread": {
@ -15533,9 +15549,9 @@
"dance", "dance",
"fan", "fan",
"flutter", "flutter",
"folding hand fan",
"hot", "hot",
"shy" "shy",
"flamenco"
] ]
}, },
"purse": { "purse": {
@ -15719,7 +15735,8 @@
"Afro", "Afro",
"comb", "comb",
"hair", "hair",
"pick" "pick",
"afro"
] ]
}, },
"crown": { "crown": {
@ -16165,7 +16182,6 @@
"b": "1FA87", "b": "1FA87",
"j": [ "j": [
"instrument", "instrument",
"maracas",
"music", "music",
"percussion", "percussion",
"rattle", "rattle",
@ -16177,11 +16193,13 @@
"b": "1FA88", "b": "1FA88",
"j": [ "j": [
"fife", "fife",
"flute",
"music", "music",
"pipe", "pipe",
"recorder", "recorder",
"woodwind" "woodwind",
"bamboo",
"instrument",
"pied piper"
] ]
}, },
"mobile-phone": { "mobile-phone": {
@ -19036,9 +19054,9 @@
"a": "⊛ Khanda", "a": "⊛ Khanda",
"b": "1FAAF", "b": "1FAAF",
"j": [ "j": [
"khanda",
"religion", "religion",
"Sikh" "Sikh",
"Sikhism"
] ]
}, },
"aries": { "aries": {
@ -19460,7 +19478,9 @@
"computer", "computer",
"internet", "internet",
"network", "network",
"wireless" "wifi",
"contactless",
"signal"
] ]
}, },
"vibration-mode": { "vibration-mode": {

View File

@ -37,7 +37,7 @@ ext.versionMinor = 5
// Note: even values are reserved for regular release, odd values for hotfix release. // Note: even values are reserved for regular release, odd values for hotfix release.
// When creating a hotfix, you should decrease the value, since the current value // When creating a hotfix, you should decrease the value, since the current value
// is the value for the next regular release. // is the value for the next regular release.
ext.versionPatch = 30 ext.versionPatch = 32
ext.scVersion = 66 ext.scVersion = 66

View File

@ -133,7 +133,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.14.0" api "com.gabrielittner.threetenbp:lazythreetenbp:0.16.0"
implementation libs.squareup.moshi implementation libs.squareup.moshi
implementation libs.squareup.moshiKt implementation libs.squareup.moshiKt
@ -311,7 +311,7 @@ dependencies {
// Fix issue with Jitsi. Inspired from https://github.com/android/android-test/issues/861#issuecomment-872067868 // Fix issue with Jitsi. Inspired from https://github.com/android/android-test/issues/861#issuecomment-872067868
// Error was lots of `Duplicate class org.checkerframework.common.reflection.qual.MethodVal found in modules jetified-checker-3.1 (org.checkerframework:checker:3.1.1) and jetified-checker-qual-3.12.0 (org.checkerframework:checker-qual:3.12.0) // Error was lots of `Duplicate class org.checkerframework.common.reflection.qual.MethodVal found in modules jetified-checker-3.1 (org.checkerframework:checker:3.1.1) and jetified-checker-qual-3.12.0 (org.checkerframework:checker-qual:3.12.0)
//noinspection GradleDependency Cannot use latest 3.15.0 since it required min API 26. //noinspection GradleDependency Cannot use latest 3.15.0 since it required min API 26.
implementation "org.checkerframework:checker:3.32.0" implementation "org.checkerframework:checker:3.33.0"
androidTestImplementation libs.androidx.testCore androidTestImplementation libs.androidx.testCore
androidTestImplementation libs.androidx.testRunner androidTestImplementation libs.androidx.testRunner

View File

@ -184,6 +184,7 @@ class HomeActivityViewModel @AssistedInject constructor(
if (analyticsConfig.isEnabled) { if (analyticsConfig.isEnabled) {
analyticsStore.didAskUserConsentFlow analyticsStore.didAskUserConsentFlow
.onEach { didAskUser -> .onEach { didAskUser ->
Timber.v("DidAskUserConsent: $didAskUser")
if (!didAskUser) { if (!didAskUser) {
_viewEvents.post(HomeActivityViewEvents.ShowAnalyticsOptIn) _viewEvents.post(HomeActivityViewEvents.ShowAnalyticsOptIn)
} else { } else {

View File

@ -152,7 +152,6 @@ import im.vector.app.features.home.room.detail.composer.MessageComposerViewModel
import im.vector.app.features.home.room.detail.composer.boolean import im.vector.app.features.home.room.detail.composer.boolean
import im.vector.app.features.home.room.detail.composer.voice.VoiceRecorderFragment import im.vector.app.features.home.room.detail.composer.voice.VoiceRecorderFragment
import im.vector.app.features.home.room.detail.error.RoomNotFound import im.vector.app.features.home.room.detail.error.RoomNotFound
import im.vector.app.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.action.EventSharedAction import im.vector.app.features.home.room.detail.timeline.action.EventSharedAction
import im.vector.app.features.home.room.detail.timeline.action.MessageActionsBottomSheet import im.vector.app.features.home.room.detail.timeline.action.MessageActionsBottomSheet
@ -172,6 +171,7 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem
import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet
import im.vector.app.features.home.room.detail.timeline.readreceipts.DisplayReadReceiptsBottomSheet
import im.vector.app.features.home.room.detail.timeline.reply.ReplyPreviewRetriever import im.vector.app.features.home.room.detail.timeline.reply.ReplyPreviewRetriever
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet

View File

@ -1442,13 +1442,17 @@ class TimelineViewModel @AssistedInject constructor(
computeUnreadState(timelineEvents, roomSummary) computeUnreadState(timelineEvents, roomSummary)
} }
// We don't want live update of unread so we skip when we already had a HasUnread or HasNoUnread // We don't want live update of unread so we skip when we already had a HasUnread or HasNoUnread
// However, we want to update an existing HasUnread, if the readMarkerId hasn't changed, // However, we want to update an existing HasUnread, if the readMarkerId hasn't changed or when we go back in live,
// as we might be loading new events to fill gaps in the timeline. // as we might be loading new events to fill gaps in the timeline.
.distinctUntilChanged { previous, current -> .distinctUntilChanged { previous, current ->
when { when {
previous is UnreadState.Unknown || previous is UnreadState.ReadMarkerNotLoaded -> false previous is UnreadState.Unknown || previous is UnreadState.ReadMarkerNotLoaded -> false
previous is UnreadState.HasUnread && current is UnreadState.HasUnread && previous is UnreadState.HasUnread && current is UnreadState.HasUnread &&
previous.readMarkerId == current.readMarkerId -> false previous.readMarkerId == current.readMarkerId -> false
previous is UnreadState.HasUnread && (
current is UnreadState.HasUnread && previous.firstUnreadEventId != current.firstUnreadEventId ||
current is UnreadState.HasNoUnread
) && timeline?.isLive.orFalse() -> false
current is UnreadState.HasUnread || current is UnreadState.HasNoUnread -> true current is UnreadState.HasUnread || current is UnreadState.HasNoUnread -> true
else -> false else -> false
} }

View File

@ -239,7 +239,7 @@ class AudioMessageHelper @Inject constructor(
val percentage = currentPosition.toFloat() / totalDuration val percentage = currentPosition.toFloat() / totalDuration
playbackTracker.updatePlayingAtPlaybackTime(id, currentPosition, percentage) playbackTracker.updatePlayingAtPlaybackTime(id, currentPosition, percentage)
} else { } else {
playbackTracker.stopPlayback(id) playbackTracker.stopPlaybackOrRecorder(id)
stopPlaybackTicker() stopPlaybackTicker()
} }
} }

View File

@ -60,6 +60,8 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageInformationD
import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryEvents import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryEvents
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem
import im.vector.app.features.home.room.detail.timeline.item.TypingItem_
import im.vector.app.features.home.room.detail.timeline.readreceipts.ReadReceiptsCache
import im.vector.app.features.home.room.detail.timeline.reply.ReplyPreviewRetriever import im.vector.app.features.home.room.detail.timeline.reply.ReplyPreviewRetriever
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
import im.vector.app.features.media.AttachmentData import im.vector.app.features.media.AttachmentData
@ -213,7 +215,7 @@ class TimelineEventController @Inject constructor(
// Map eventId to adapter position // Map eventId to adapter position
private val adapterPositionMapping = HashMap<String, Int>() private val adapterPositionMapping = HashMap<String, Int>()
private val timelineEventsGroups = TimelineEventsGroups() private val timelineEventsGroups = TimelineEventsGroups()
private val receiptsByEvent = HashMap<String, MutableList<ReadReceipt>>() private val readReceiptsCache = ReadReceiptsCache()
private val modelCache = arrayListOf<CacheItemData?>() private val modelCache = arrayListOf<CacheItemData?>()
private var currentSnapshot: List<TimelineEvent> = emptyList() private var currentSnapshot: List<TimelineEvent> = emptyList()
private var inSubmitList: Boolean = false private var inSubmitList: Boolean = false
@ -463,7 +465,7 @@ class TimelineEventController @Inject constructor(
} }
Timber.v("Preprocess events took $preprocessEventsTiming ms") Timber.v("Preprocess events took $preprocessEventsTiming ms")
var numberOfEventsToBuild = 0 var numberOfEventsToBuild = 0
val lastSentEventWithoutReadReceipts = searchLastSentEventWithoutReadReceipts(receiptsByEvent) val lastSentEventWithoutReadReceipts = searchLastSentEventWithoutReadReceipts(readReceiptsCache.receiptsByEvent())
(0 until modelCache.size).forEach { position -> (0 until modelCache.size).forEach { position ->
val event = currentSnapshot[position] val event = currentSnapshot[position]
val nextEvent = currentSnapshot.nextOrNull(position) val nextEvent = currentSnapshot.nextOrNull(position)
@ -509,7 +511,7 @@ class TimelineEventController @Inject constructor(
} }
val itemCachedData = modelCache[position] ?: return@forEach val itemCachedData = modelCache[position] ?: return@forEach
// Then update with additional models if needed // Then update with additional models if needed
modelCache[position] = itemCachedData.enrichWithModels(event, nextEvent, position, receiptsByEvent) modelCache[position] = itemCachedData.enrichWithModels(event, nextEvent, position, readReceiptsCache.receiptsByEvent())
} }
Timber.v("Number of events to rebuild: $numberOfEventsToBuild on ${modelCache.size} total events") Timber.v("Number of events to rebuild: $numberOfEventsToBuild on ${modelCache.size} total events")
} }
@ -598,15 +600,15 @@ class TimelineEventController @Inject constructor(
} }
private fun preprocessReverseEvents() { private fun preprocessReverseEvents() {
receiptsByEvent.clear() readReceiptsCache.clear()
timelineEventsGroups.clear() timelineEventsGroups.clear()
val itr = currentSnapshot.listIterator(currentSnapshot.size) val itr = currentSnapshot.listIterator(currentSnapshot.size)
var lastShownEventId: String? = null var lastShownEventId: String? = null
while (itr.hasPrevious()) { while (itr.hasPrevious()) {
val event = itr.previous() val event = itr.previous()
timelineEventsGroups.addOrIgnore(event) timelineEventsGroups.addOrIgnore(event)
val currentReadReceipts = ArrayList(event.readReceipts).filter { val currentReadReceipts = event.readReceipts.filter {
it.roomMember.userId != session.myUserId && it.isVisibleInThisThread() it.roomMember.userId != session.myUserId
} }
if (timelineEventVisibilityHelper.shouldShowEvent( if (timelineEventVisibilityHelper.shouldShowEvent(
timelineEvent = event, timelineEvent = event,
@ -619,18 +621,7 @@ class TimelineEventController @Inject constructor(
if (lastShownEventId == null) { if (lastShownEventId == null) {
continue continue
} }
val existingReceipts = receiptsByEvent.getOrPut(lastShownEventId) { ArrayList() } readReceiptsCache.addReceiptsOnEvent(currentReadReceipts, lastShownEventId)
existingReceipts.addAll(currentReadReceipts)
}
}
private fun ReadReceipt.isVisibleInThisThread(): Boolean {
return if (partialState.isFromThreadTimeline()) {
this.threadId == partialState.rootThreadEventId
} else if (DbgUtil.isDbgEnabled(DbgUtil.DBG_SHOW_DUPLICATE_READ_RECEIPTS)) {
this.threadId in listOf(null, ReadService.THREAD_ID_MAIN, ReadService.THREAD_ID_MAIN_OR_NULL)
} else {
this.threadId == ReadService.THREAD_ID_MAIN_OR_NULL
} }
} }

View File

@ -100,7 +100,7 @@ class AudioMessagePlaybackTracker @Inject constructor() {
} }
} }
fun stopPlayback(id: String) { fun stopPlaybackOrRecorder(id: String) {
val state = getPlaybackState(id) val state = getPlaybackState(id)
if (state !is Listener.State.Error) { if (state !is Listener.State.Error) {
setState(id, Listener.State.Idle) setState(id, Listener.State.Idle)

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2019 New Vector Ltd * Copyright (c) 2023 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.features.home.room.detail.readreceipts package im.vector.app.features.home.room.detail.timeline.readreceipts
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2019 New Vector Ltd * Copyright (c) 2023 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.features.home.room.detail.readreceipts package im.vector.app.features.home.room.detail.timeline.readreceipts
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2019 New Vector Ltd * Copyright (c) 2023 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.features.home.room.detail.readreceipts package im.vector.app.features.home.room.detail.timeline.readreceipts
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import de.spiritcroc.matrixsdk.util.DbgUtil import de.spiritcroc.matrixsdk.util.DbgUtil

View File

@ -0,0 +1,54 @@
/*
* 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.readreceipts
import im.vector.lib.core.utils.compat.removeIfCompat
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
class ReadReceiptsCache {
private val receiptsByEventId = HashMap<String, MutableList<ReadReceipt>>()
// Key is userId, Value is eventId
private val receiptEventIdByUserId = HashMap<String, String>()
fun receiptsByEvent(): Map<String, List<ReadReceipt>> {
return receiptsByEventId
}
fun addReceiptsOnEvent(receipts: List<ReadReceipt>, eventId: String) {
val existingReceipts = receiptsByEventId.getOrPut(eventId) { ArrayList() }
receipts.forEach { readReceipt ->
val receiptUserId = readReceipt.roomMember.userId
val receiptEventId = receiptEventIdByUserId[receiptUserId]
// If we already have a read receipt for this user, move it so we only
// use the most recent. It can happen because of threaded read receipts.
if (receiptEventId != null) {
receiptsByEventId[receiptEventId]?.removeIfCompat {
it.roomMember.userId == receiptUserId
}
}
receiptEventIdByUserId[receiptUserId] = eventId
existingReceipts.add(readReceipt)
}
}
fun clear() {
receiptsByEventId.clear()
receiptEventIdByUserId.clear()
}
}

View File

@ -29,7 +29,7 @@ import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.glide.GlideApp import im.vector.app.core.glide.GlideApp
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.html.PillImageSpan import im.vector.app.features.html.PillImageSpan
import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.getRoomSummary import org.matrix.android.sdk.api.session.getRoomSummary
import org.matrix.android.sdk.api.session.getUserOrDefault import org.matrix.android.sdk.api.session.getUserOrDefault
import org.matrix.android.sdk.api.session.permalinks.PermalinkData import org.matrix.android.sdk.api.session.permalinks.PermalinkData
@ -99,7 +99,9 @@ class EventTextRenderer @AssistedInject constructor(
private fun addPermalinksSpans(text: Spannable) { private fun addPermalinksSpans(text: Spannable) {
for (match in Patterns.WEB_URL.toRegex().findAll(text)) { for (match in Patterns.WEB_URL.toRegex().findAll(text)) {
val url = text.substring(match.range) val url = text.substring(match.range)
val matrixItem = if (MatrixPatterns.isPermalink(url)) { val supportedHosts = context.resources.getStringArray(R.array.permalink_supported_hosts)
val isPermalinkSupported = sessionHolder.getSafeActiveSession()?.permalinkService()?.isPermalinkSupported(supportedHosts, url).orFalse()
val matrixItem = if (isPermalinkSupported) {
when (val permalinkData = PermalinkParser.parse(url)) { when (val permalinkData = PermalinkParser.parse(url)) {
is PermalinkData.UserLink -> permalinkData.toMatrixItem() is PermalinkData.UserLink -> permalinkData.toMatrixItem()
is PermalinkData.RoomLink -> permalinkData.toMatrixItem() is PermalinkData.RoomLink -> permalinkData.toMatrixItem()

View File

@ -22,11 +22,13 @@ import android.text.Spanned
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.glide.GlideApp import im.vector.app.core.glide.GlideApp
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import io.noties.markwon.core.spans.LinkSpan import io.noties.markwon.core.spans.LinkSpan
import io.noties.markwon.image.AsyncDrawableSpan import io.noties.markwon.image.AsyncDrawableSpan
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.getRoomSummary import org.matrix.android.sdk.api.session.getRoomSummary
import org.matrix.android.sdk.api.session.getUser import org.matrix.android.sdk.api.session.getUser
import org.matrix.android.sdk.api.session.permalinks.PermalinkData import org.matrix.android.sdk.api.session.permalinks.PermalinkData
@ -106,12 +108,18 @@ class PillsPostProcessor @AssistedInject constructor(
PillImageSpan(GlideApp.with(context), avatarRenderer, context, matrixItem) PillImageSpan(GlideApp.with(context), avatarRenderer, context, matrixItem)
private fun LinkSpan.createPillSpan(): PillImageSpan? { private fun LinkSpan.createPillSpan(): PillImageSpan? {
val supportedHosts = context.resources.getStringArray(R.array.permalink_supported_hosts)
val isPermalinkSupported = sessionHolder.getSafeActiveSession()?.permalinkService()?.isPermalinkSupported(supportedHosts, url).orFalse()
if (isPermalinkSupported) {
val matrixItem = when (val permalinkData = PermalinkParser.parse(url)) { val matrixItem = when (val permalinkData = PermalinkParser.parse(url)) {
is PermalinkData.UserLink -> permalinkData.toMatrixItem() is PermalinkData.UserLink -> permalinkData.toMatrixItem()
is PermalinkData.RoomLink -> permalinkData.toMatrixItem() is PermalinkData.RoomLink -> permalinkData.toMatrixItem()
else -> null else -> null
} ?: return null } ?: return null
return createPillImageSpan(matrixItem) return createPillImageSpan(matrixItem)
} else {
return null
}
} }
private fun PermalinkData.UserLink.toMatrixItem(): MatrixItem? = private fun PermalinkData.UserLink.toMatrixItem(): MatrixItem? =

View File

@ -18,7 +18,6 @@ package im.vector.app.features.permalink
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
@ -38,7 +37,6 @@ import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.getRoomSummary import org.matrix.android.sdk.api.session.getRoomSummary
import org.matrix.android.sdk.api.session.permalinks.PermalinkData import org.matrix.android.sdk.api.session.permalinks.PermalinkData
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
import org.matrix.android.sdk.api.session.permalinks.PermalinkService
import org.matrix.android.sdk.api.session.room.getTimelineEvent import org.matrix.android.sdk.api.session.room.getTimelineEvent
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
@ -70,10 +68,11 @@ class PermalinkHandler @Inject constructor(
buildTask: Boolean = false, buildTask: Boolean = false,
openAnonymously: Boolean = false openAnonymously: Boolean = false
): Boolean { ): Boolean {
val supportedHosts = fragmentActivity.resources.getStringArray(R.array.permalink_supported_hosts)
return when { return when {
deepLink == null -> false deepLink == null -> false
deepLink.isIgnored() -> true deepLink.isIgnored() -> true
!isPermalinkSupported(fragmentActivity, deepLink.toString()) -> false !activeSessionHolder.getSafeActiveSession()?.permalinkService()?.isPermalinkSupported(supportedHosts, deepLink.toString()).orFalse() -> false
else -> { else -> {
tryOrNull { tryOrNull {
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
@ -172,12 +171,6 @@ class PermalinkHandler @Inject constructor(
} }
} }
private fun isPermalinkSupported(context: Context, url: String): Boolean {
return url.startsWith(PermalinkService.MATRIX_TO_URL_BASE) ||
context.resources.getStringArray(R.array.permalink_supported_hosts)
.any { url.toUri().host == it }
}
private suspend fun PermalinkData.RoomLink.getRoomId(): String? { private suspend fun PermalinkData.RoomLink.getRoomId(): String? {
val session = activeSessionHolder.getSafeActiveSession() val session = activeSessionHolder.getSafeActiveSession()
return if (isRoomAlias && session != null) { return if (isRoomAlias && session != null) {

View File

@ -527,7 +527,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
State.Idle -> { State.Idle -> {
// restart the playback time if player completed with less than 1s remaining time // restart the playback time if player completed with less than 1s remaining time
if (percentage == null || (playlist.duration - position) < 1000) { if (percentage == null || (playlist.duration - position) < 1000) {
playbackTracker.stopPlayback(id) playbackTracker.stopPlaybackOrRecorder(id)
} else { } else {
playbackTracker.updatePausedAtPlaybackTime(id, position, percentage) playbackTracker.updatePausedAtPlaybackTime(id, position, percentage)
} }

View File

@ -20,6 +20,7 @@ import android.content.Context
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import im.vector.app.core.resources.BuildMeta import im.vector.app.core.resources.BuildMeta
import im.vector.app.features.attachments.toContentAttachmentData import im.vector.app.features.attachments.toContentAttachmentData
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
import im.vector.app.features.session.coroutineScope import im.vector.app.features.session.coroutineScope
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure
@ -54,6 +55,7 @@ import javax.inject.Inject
class StartVoiceBroadcastUseCase @Inject constructor( class StartVoiceBroadcastUseCase @Inject constructor(
private val session: Session, private val session: Session,
private val voiceBroadcastRecorder: VoiceBroadcastRecorder?, private val voiceBroadcastRecorder: VoiceBroadcastRecorder?,
private val playbackTracker: AudioMessagePlaybackTracker,
private val context: Context, private val context: Context,
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase, private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase,
@ -106,10 +108,14 @@ class StartVoiceBroadcastUseCase @Inject constructor(
} }
override fun onStateUpdated(state: VoiceBroadcastRecorder.State) { override fun onStateUpdated(state: VoiceBroadcastRecorder.State) {
if (state == VoiceBroadcastRecorder.State.Error) { when (state) {
session.coroutineScope.launch { VoiceBroadcastRecorder.State.Recording -> playbackTracker.updateCurrentRecording(AudioMessagePlaybackTracker.RECORDING_ID, emptyList())
pauseVoiceBroadcastUseCase.execute(room.roomId) VoiceBroadcastRecorder.State.Idle -> playbackTracker.stopPlaybackOrRecorder(AudioMessagePlaybackTracker.RECORDING_ID)
VoiceBroadcastRecorder.State.Error -> {
playbackTracker.stopPlaybackOrRecorder(AudioMessagePlaybackTracker.RECORDING_ID)
session.coroutineScope.launch { pauseVoiceBroadcastUseCase.execute(room.roomId) }
} }
else -> Unit
} }
} }
}) })

View File

@ -47,8 +47,7 @@ class GetVoiceBroadcastStateEventLiveUseCase @Inject constructor(
) { ) {
fun execute(voiceBroadcast: VoiceBroadcast): Flow<Optional<VoiceBroadcastEvent>> { fun execute(voiceBroadcast: VoiceBroadcast): Flow<Optional<VoiceBroadcastEvent>> {
val room = session.getRoom(voiceBroadcast.roomId) ?: error("Unknown roomId: ${voiceBroadcast.roomId}") return getMostRecentVoiceBroadcastEventFlow(voiceBroadcast)
return getMostRecentVoiceBroadcastEventFlow(room, voiceBroadcast)
.onEach { event -> .onEach { event ->
Timber.d( Timber.d(
"## VoiceBroadcast | " + "## VoiceBroadcast | " +
@ -61,7 +60,8 @@ class GetVoiceBroadcastStateEventLiveUseCase @Inject constructor(
/** /**
* Get a flow of the most recent event for the given voice broadcast. * Get a flow of the most recent event for the given voice broadcast.
*/ */
private fun getMostRecentVoiceBroadcastEventFlow(room: Room, voiceBroadcast: VoiceBroadcast): Flow<Optional<VoiceBroadcastEvent>> { private fun getMostRecentVoiceBroadcastEventFlow(voiceBroadcast: VoiceBroadcast): Flow<Optional<VoiceBroadcastEvent>> {
val room = session.getRoom(voiceBroadcast.roomId) ?: error("Unknown roomId: ${voiceBroadcast.roomId}")
val startedEventFlow = room.flow().liveTimelineEvent(voiceBroadcast.voiceBroadcastId) val startedEventFlow = room.flow().liveTimelineEvent(voiceBroadcast.voiceBroadcastId)
// observe started event changes // observe started event changes
return startedEventFlow return startedEventFlow

File diff suppressed because one or more lines are too long

View File

@ -57,6 +57,7 @@ class StartVoiceBroadcastUseCaseTest {
StartVoiceBroadcastUseCase( StartVoiceBroadcastUseCase(
session = fakeSession, session = fakeSession,
voiceBroadcastRecorder = fakeVoiceBroadcastRecorder, voiceBroadcastRecorder = fakeVoiceBroadcastRecorder,
playbackTracker = mockk(),
context = FakeContext().instance, context = FakeContext().instance,
buildMeta = mockk(), buildMeta = mockk(),
getRoomLiveVoiceBroadcastsUseCase = fakeGetRoomLiveVoiceBroadcastsUseCase, getRoomLiveVoiceBroadcastsUseCase = fakeGetRoomLiveVoiceBroadcastsUseCase,