Delegate call notifications to Element Call, upgrade SDK and EC embedded (#5119)

* Stop sending call notifications manually: the Element Call widget can now assume responsibility for sending them when you start a call.

* Upgrade SDK version to `v25.8.5`, fix API breaks

* Upgrade Element Call embedded to `v0.14.1`

* Fix tests and lint issues

* Add `RoomListEntriesDynamicFilterKind.NonSpace` to avoid displaying spaces in the room list

---------

Co-authored-by: Robin <robin@robin.town>
This commit is contained in:
Jorge Martin Espinosa 2025-08-05 17:14:51 +02:00 committed by GitHub
parent 0b895f631d
commit a87bbdd91c
17 changed files with 64 additions and 174 deletions

View file

@ -33,14 +33,11 @@ import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import io.element.android.libraries.network.useragent.UserAgentProvider import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.services.analytics.api.ScreenTracker import io.element.android.services.analytics.api.ScreenTracker
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import io.element.android.services.appnavstate.api.AppForegroundStateService import io.element.android.services.appnavstate.api.AppForegroundStateService
import io.element.android.services.toolbox.api.systemclock.SystemClock import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -64,7 +61,6 @@ class CallScreenPresenter @AssistedInject constructor(
private val activeCallManager: ActiveCallManager, private val activeCallManager: ActiveCallManager,
private val languageTagProvider: LanguageTagProvider, private val languageTagProvider: LanguageTagProvider,
private val appForegroundStateService: AppForegroundStateService, private val appForegroundStateService: AppForegroundStateService,
private val activeRoomsHolder: ActiveRoomsHolder,
@AppCoroutineScope @AppCoroutineScope
private val appCoroutineScope: CoroutineScope, private val appCoroutineScope: CoroutineScope,
) : Presenter<CallScreenState> { ) : Presenter<CallScreenState> {
@ -75,7 +71,6 @@ class CallScreenPresenter @AssistedInject constructor(
private val isInWidgetMode = callType is CallType.RoomCall private val isInWidgetMode = callType is CallType.RoomCall
private val userAgent = userAgentProvider.provide() private val userAgent = userAgentProvider.provide()
private var notifiedCallStart = false
@Composable @Composable
override fun present(): CallScreenState { override fun present(): CallScreenState {
@ -248,9 +243,7 @@ class CallScreenPresenter @AssistedInject constructor(
Timber.d("Observing sync state in-call for sessionId: ${roomCallType.sessionId}") Timber.d("Observing sync state in-call for sessionId: ${roomCallType.sessionId}")
client.syncService().syncState client.syncService().syncState
.collect { state -> .collect { state ->
if (state == SyncState.Running) { if (state != SyncState.Running) {
client.notifyCallStartIfNeeded(callType.roomId)
} else {
appForegroundStateService.updateIsInCallState(true) appForegroundStateService.updateIsInCallState(true)
} }
} }
@ -263,32 +256,6 @@ class CallScreenPresenter @AssistedInject constructor(
} }
} }
private suspend fun MatrixClient.notifyCallStartIfNeeded(roomId: RoomId) {
if (notifiedCallStart) return
val activeRoomForSession = activeRoomsHolder.getActiveRoomMatching(sessionId, roomId)
val sendCallNotificationResult = if (activeRoomForSession != null) {
Timber.d("Notifying call start for room $roomId. Has room call: ${activeRoomForSession.info().hasRoomCall}")
activeRoomForSession.sendCallNotificationIfNeeded()
} else {
// Instantiate the room from the session and roomId and send the notification
getJoinedRoom(roomId)?.use { room ->
Timber.d("Notifying call start for room $roomId. Has room call: ${room.info().hasRoomCall}")
room.sendCallNotificationIfNeeded()
} ?: run {
Timber.w("No room found for session $sessionId and room $roomId, skipping call notification.")
return
}
}
sendCallNotificationResult.fold(
onSuccess = { notifiedCallStart = true },
onFailure = { error ->
Timber.e(error, "Failed to send call notification for room $roomId.")
}
)
}
private fun parseMessage(message: String): WidgetMessage? { private fun parseMessage(message: String): WidgetMessage? {
return WidgetMessageSerializer.deserialize(message).getOrNull() return WidgetMessageSerializer.deserialize(message).getOrNull()
} }

View file

@ -13,6 +13,7 @@ import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.services.appnavstate.api.ActiveRoomsHolder import io.element.android.services.appnavstate.api.ActiveRoomsHolder
@ -44,7 +45,7 @@ class DefaultCallWidgetProvider @Inject constructor(
val baseUrl = customBaseUrl ?: EMBEDDED_CALL_WIDGET_BASE_URL val baseUrl = customBaseUrl ?: EMBEDDED_CALL_WIDGET_BASE_URL
val isEncrypted = room.info().isEncrypted ?: room.getUpdatedIsEncrypted().getOrThrow() val isEncrypted = room.info().isEncrypted ?: room.getUpdatedIsEncrypted().getOrThrow()
val widgetSettings = callWidgetSettingsProvider.provide(baseUrl, encrypted = isEncrypted) val widgetSettings = callWidgetSettingsProvider.provide(baseUrl, encrypted = isEncrypted, direct = room.isDm())
val callUrl = room.generateWidgetWebViewUrl( val callUrl = room.generateWidgetWebViewUrl(
widgetSettings = widgetSettings, widgetSettings = widgetSettings,
clientId = clientId, clientId = clientId,

View file

@ -26,13 +26,11 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.sync.FakeSyncService import io.element.android.libraries.matrix.test.sync.FakeSyncService
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
import io.element.android.libraries.network.useragent.UserAgentProvider import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.services.analytics.api.ScreenTracker import io.element.android.services.analytics.api.ScreenTracker
import io.element.android.services.analytics.test.FakeScreenTracker import io.element.android.services.analytics.test.FakeScreenTracker
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
import io.element.android.services.toolbox.api.systemclock.SystemClock import io.element.android.services.toolbox.api.systemclock.SystemClock
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
@ -82,19 +80,12 @@ import kotlin.time.Duration.Companion.seconds
} }
@Test @Test
fun `present - with CallType RoomCall sets call as active, loads URL, runs WidgetDriver and notifies the other clients a call started`() = runTest { fun `present - with CallType RoomCall sets call as active, loads URL and runs WidgetDriver`() = runTest {
val sendCallNotificationIfNeededLambda = lambdaRecorder<Result<Boolean>> { Result.success(true) }
val syncService = FakeSyncService(SyncState.Running)
val fakeRoom = FakeJoinedRoom(sendCallNotificationIfNeededResult = sendCallNotificationIfNeededLambda)
val client = FakeMatrixClient(syncService = syncService).apply {
givenGetRoomResult(A_ROOM_ID, fakeRoom)
}
val widgetDriver = FakeMatrixWidgetDriver() val widgetDriver = FakeMatrixWidgetDriver()
val widgetProvider = FakeCallWidgetProvider(widgetDriver) val widgetProvider = FakeCallWidgetProvider(widgetDriver)
val analyticsLambda = lambdaRecorder<MobileScreen.ScreenName, Unit> {} val analyticsLambda = lambdaRecorder<MobileScreen.ScreenName, Unit> {}
val joinedCallLambda = lambdaRecorder<CallType, Unit> {} val joinedCallLambda = lambdaRecorder<CallType, Unit> {}
val presenter = createCallScreenPresenter( val presenter = createCallScreenPresenter(
matrixClientsProvider = FakeMatrixClientProvider(getClient = { Result.success(client) }),
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
widgetDriver = widgetDriver, widgetDriver = widgetDriver,
widgetProvider = widgetProvider, widgetProvider = widgetProvider,
@ -116,7 +107,6 @@ import kotlin.time.Duration.Companion.seconds
assertThat(widgetProvider.getWidgetCalled).isTrue() assertThat(widgetProvider.getWidgetCalled).isTrue()
assertThat(widgetDriver.runCalledCount).isEqualTo(1) assertThat(widgetDriver.runCalledCount).isEqualTo(1)
analyticsLambda.assertions().isCalledOnce().with(value(MobileScreen.ScreenName.RoomCall)) analyticsLambda.assertions().isCalledOnce().with(value(MobileScreen.ScreenName.RoomCall))
sendCallNotificationIfNeededLambda.assertions().isCalledOnce()
// Wait until the WidgetDriver is loaded // Wait until the WidgetDriver is loaded
skipItems(1) skipItems(1)
@ -399,7 +389,6 @@ import kotlin.time.Duration.Companion.seconds
activeCallManager: FakeActiveCallManager = FakeActiveCallManager(), activeCallManager: FakeActiveCallManager = FakeActiveCallManager(),
screenTracker: ScreenTracker = FakeScreenTracker(), screenTracker: ScreenTracker = FakeScreenTracker(),
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(), appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(),
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
): CallScreenPresenter { ): CallScreenPresenter {
val userAgentProvider = object : UserAgentProvider { val userAgentProvider = object : UserAgentProvider {
override fun provide(): String { override fun provide(): String {
@ -420,7 +409,6 @@ import kotlin.time.Duration.Companion.seconds
languageTagProvider = FakeLanguageTagProvider("en-US"), languageTagProvider = FakeLanguageTagProvider("en-US"),
appForegroundStateService = appForegroundStateService, appForegroundStateService = appForegroundStateService,
appCoroutineScope = backgroundScope, appCoroutineScope = backgroundScope,
activeRoomsHolder = activeRoomsHolder,
) )
} }
} }

View file

@ -176,7 +176,7 @@ jsoup = "org.jsoup:jsoup:1.21.1"
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = "app.cash.molecule:molecule-runtime:2.1.0" molecule-runtime = "app.cash.molecule:molecule-runtime:2.1.0"
timber = "com.jakewharton.timber:timber:5.0.1" timber = "com.jakewharton.timber:timber:5.0.1"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.7.28" matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.8.5"
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" } matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" } matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
@ -215,7 +215,7 @@ anvil_compiler_api = { module = "dev.zacsweers.anvil:compiler-api", version.ref
anvil_compiler_utils = { module = "dev.zacsweers.anvil:compiler-utils", version.ref = "anvil" } anvil_compiler_utils = { module = "dev.zacsweers.anvil:compiler-utils", version.ref = "anvil" }
# Element Call # Element Call
element_call_embedded = "io.element.android:element-call-embedded:0.13.1" element_call_embedded = "io.element.android:element-call-embedded:0.14.1"
# Auto services # Auto services
google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" } google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" }

View file

@ -156,11 +156,6 @@ interface JoinedRoom : BaseRoom {
*/ */
fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result<MatrixWidgetDriver> fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result<MatrixWidgetDriver>
/**
* Send an Element Call started notification if needed.
*/
suspend fun sendCallNotificationIfNeeded(): Result<Boolean>
suspend fun setSendQueueEnabled(enabled: Boolean) suspend fun setSendQueueEnabled(enabled: Boolean)
/** /**

View file

@ -14,5 +14,6 @@ interface CallWidgetSettingsProvider {
baseUrl: String, baseUrl: String,
widgetId: String = UUID.randomUUID().toString(), widgetId: String = UUID.randomUUID().toString(),
encrypted: Boolean, encrypted: Boolean,
direct: Boolean,
): MatrixWidgetSettings ): MatrixWidgetSettings
} }

View file

@ -428,12 +428,6 @@ class JoinedRustRoom(
} }
} }
override suspend fun sendCallNotificationIfNeeded(): Result<Boolean> = withContext(roomDispatcher) {
runCatchingExceptions {
innerRoom.sendCallNotificationIfNeeded()
}
}
override suspend fun setSendQueueEnabled(enabled: Boolean) { override suspend fun setSendQueueEnabled(enabled: Boolean) {
withContext(roomDispatcher) { withContext(roomDispatcher) {
Timber.d("setSendQueuesEnabled: $enabled") Timber.d("setSendQueuesEnabled: $enabled")

View file

@ -29,6 +29,7 @@ import org.matrix.rustcomponents.sdk.RoomList as InnerRoomList
private val ROOM_LIST_RUST_FILTERS = listOf( private val ROOM_LIST_RUST_FILTERS = listOf(
RoomListEntriesDynamicFilterKind.NonLeft, RoomListEntriesDynamicFilterKind.NonLeft,
RoomListEntriesDynamicFilterKind.NonSpace,
RoomListEntriesDynamicFilterKind.DeduplicateVersions RoomListEntriesDynamicFilterKind.DeduplicateVersions
) )

View file

@ -14,7 +14,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import org.matrix.rustcomponents.sdk.TimelineChange
import org.matrix.rustcomponents.sdk.TimelineDiff import org.matrix.rustcomponents.sdk.TimelineDiff
import org.matrix.rustcomponents.sdk.TimelineItem import org.matrix.rustcomponents.sdk.TimelineItem
import timber.log.Timber import timber.log.Timber
@ -49,13 +48,13 @@ internal class MatrixTimelineDiffProcessor(
} }
private fun MutableList<MatrixTimelineItem>.applyDiff(diff: TimelineDiff) { private fun MutableList<MatrixTimelineItem>.applyDiff(diff: TimelineDiff) {
when (diff.change()) { when (diff) {
TimelineChange.APPEND -> { is TimelineDiff.Append -> {
val items = diff.append()?.map { it.asMatrixTimelineItem() } ?: return val items = diff.values.map { it.asMatrixTimelineItem() }
addAll(items) addAll(items)
} }
TimelineChange.PUSH_BACK -> { is TimelineDiff.PushBack -> {
val item = diff.pushBack()?.asMatrixTimelineItem() ?: return val item = diff.value.asMatrixTimelineItem()
if (item is MatrixTimelineItem.Event && item.event.content is RoomMembershipContent) { if (item is MatrixTimelineItem.Event && item.event.content is RoomMembershipContent) {
// TODO - This is a temporary solution to notify the room screen about membership changes // TODO - This is a temporary solution to notify the room screen about membership changes
// Ideally, this should be implemented by the Rust SDK // Ideally, this should be implemented by the Rust SDK
@ -63,41 +62,37 @@ internal class MatrixTimelineDiffProcessor(
} }
add(item) add(item)
} }
TimelineChange.PUSH_FRONT -> { is TimelineDiff.PushFront -> {
val item = diff.pushFront()?.asMatrixTimelineItem() ?: return val item = diff.value.asMatrixTimelineItem()
add(0, item) add(0, item)
} }
TimelineChange.SET -> { is TimelineDiff.Set -> {
val updateAtData = diff.set() ?: return val item = diff.value.asMatrixTimelineItem()
val item = updateAtData.item.asMatrixTimelineItem() set(diff.index.toInt(), item)
set(updateAtData.index.toInt(), item)
} }
TimelineChange.INSERT -> { is TimelineDiff.Insert -> {
val insertAtData = diff.insert() ?: return val item = diff.value.asMatrixTimelineItem()
val item = insertAtData.item.asMatrixTimelineItem() add(diff.index.toInt(), item)
add(insertAtData.index.toInt(), item)
} }
TimelineChange.REMOVE -> { is TimelineDiff.Remove -> {
val removeAtData = diff.remove() ?: return removeAt(diff.index.toInt())
removeAt(removeAtData.toInt())
} }
TimelineChange.RESET -> { is TimelineDiff.Reset -> {
clear() clear()
val items = diff.reset()?.map { it.asMatrixTimelineItem() } ?: return val items = diff.values.map { it.asMatrixTimelineItem() }
addAll(items) addAll(items)
} }
TimelineChange.POP_FRONT -> { TimelineDiff.PopFront -> {
removeFirstOrNull() removeFirstOrNull()
} }
TimelineChange.POP_BACK -> { TimelineDiff.PopBack -> {
removeLastOrNull() removeLastOrNull()
} }
TimelineChange.CLEAR -> { TimelineDiff.Clear -> {
clear() clear()
} }
TimelineChange.TRUNCATE -> { is TimelineDiff.Truncate -> {
val index = diff.truncate() ?: return subList(diff.length.toInt(), size).clear()
subList(index.toInt(), size).clear()
} }
} }
} }

View file

@ -7,7 +7,6 @@
package io.element.android.libraries.matrix.impl.timeline package io.element.android.libraries.matrix.impl.timeline
import org.matrix.rustcomponents.sdk.TimelineChange
import org.matrix.rustcomponents.sdk.TimelineDiff import org.matrix.rustcomponents.sdk.TimelineDiff
import org.matrix.rustcomponents.sdk.TimelineItem import org.matrix.rustcomponents.sdk.TimelineItem
import uniffi.matrix_sdk_ui.EventItemOrigin import uniffi.matrix_sdk_ui.EventItemOrigin
@ -17,25 +16,13 @@ import uniffi.matrix_sdk_ui.EventItemOrigin
* If there is multiple events in the diff, uses the first one as it should be a good indicator. * If there is multiple events in the diff, uses the first one as it should be a good indicator.
*/ */
internal fun TimelineDiff.eventOrigin(): EventItemOrigin? { internal fun TimelineDiff.eventOrigin(): EventItemOrigin? {
return when (change()) { return when (this) {
TimelineChange.APPEND -> { is TimelineDiff.Append -> values.firstOrNull()?.eventOrigin()
append()?.firstOrNull()?.eventOrigin() is TimelineDiff.PushBack -> value.eventOrigin()
} is TimelineDiff.PushFront -> value.eventOrigin()
TimelineChange.PUSH_BACK -> { is TimelineDiff.Set -> value.eventOrigin()
pushBack()?.eventOrigin() is TimelineDiff.Insert -> value.eventOrigin()
} is TimelineDiff.Reset -> values.firstOrNull()?.eventOrigin()
TimelineChange.PUSH_FRONT -> {
pushFront()?.eventOrigin()
}
TimelineChange.SET -> {
set()?.item?.eventOrigin()
}
TimelineChange.INSERT -> {
insert()?.item?.eventOrigin()
}
TimelineChange.RESET -> {
reset()?.firstOrNull()?.eventOrigin()
}
else -> null else -> null
} }
} }

View file

@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.first
import org.matrix.rustcomponents.sdk.newVirtualElementCallWidget import org.matrix.rustcomponents.sdk.newVirtualElementCallWidget
import uniffi.matrix_sdk.EncryptionSystem import uniffi.matrix_sdk.EncryptionSystem
import uniffi.matrix_sdk.HeaderStyle import uniffi.matrix_sdk.HeaderStyle
import uniffi.matrix_sdk.NotificationType
import uniffi.matrix_sdk.VirtualElementCallWidgetOptions import uniffi.matrix_sdk.VirtualElementCallWidgetOptions
import javax.inject.Inject import javax.inject.Inject
import uniffi.matrix_sdk.Intent as CallIntent import uniffi.matrix_sdk.Intent as CallIntent
@ -29,7 +30,7 @@ class DefaultCallWidgetSettingsProvider @Inject constructor(
private val callAnalyticsCredentialsProvider: CallAnalyticCredentialsProvider, private val callAnalyticsCredentialsProvider: CallAnalyticCredentialsProvider,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
) : CallWidgetSettingsProvider { ) : CallWidgetSettingsProvider {
override suspend fun provide(baseUrl: String, widgetId: String, encrypted: Boolean): MatrixWidgetSettings { override suspend fun provide(baseUrl: String, widgetId: String, encrypted: Boolean, direct: Boolean): MatrixWidgetSettings {
val isAnalyticsEnabled = analyticsService.userConsentFlow.first() val isAnalyticsEnabled = analyticsService.userConsentFlow.first()
val options = VirtualElementCallWidgetOptions( val options = VirtualElementCallWidgetOptions(
elementCallUrl = baseUrl, elementCallUrl = baseUrl,
@ -53,6 +54,7 @@ class DefaultCallWidgetSettingsProvider @Inject constructor(
hideHeader = true, hideHeader = true,
controlledMediaDevices = true, controlledMediaDevices = true,
header = HeaderStyle.APP_BAR, header = HeaderStyle.APP_BAR,
sendNotificationType = if (direct) NotificationType.RING else NotificationType.NOTIFICATION,
) )
val rustWidgetSettings = newVirtualElementCallWidget(options) val rustWidgetSettings = newVirtualElementCallWidget(options)
return MatrixWidgetSettings.fromRustWidgetSettings(rustWidgetSettings) return MatrixWidgetSettings.fromRustWidgetSettings(rustWidgetSettings)

View file

@ -1,30 +0,0 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.fixtures.fakes
import org.matrix.rustcomponents.sdk.InsertData
import org.matrix.rustcomponents.sdk.NoPointer
import org.matrix.rustcomponents.sdk.SetData
import org.matrix.rustcomponents.sdk.TimelineChange
import org.matrix.rustcomponents.sdk.TimelineDiff
import org.matrix.rustcomponents.sdk.TimelineItem
class FakeFfiTimelineDiff(
private val change: TimelineChange,
private val item: TimelineItem? = FakeFfiTimelineItem()
) : TimelineDiff(NoPointer) {
override fun change() = change
override fun append(): List<TimelineItem>? = item?.let { listOf(it) }
override fun insert(): InsertData? = item?.let { InsertData(1u, it) }
override fun pushBack(): TimelineItem? = item
override fun pushFront(): TimelineItem? = item
override fun remove(): UInt? = 1u
override fun reset(): List<TimelineItem>? = item?.let { listOf(it) }
override fun set(): SetData? = item?.let { SetData(1u, it) }
override fun truncate(): UInt? = 1u
}

View file

@ -9,7 +9,7 @@ package io.element.android.libraries.matrix.impl.timeline
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimelineDiff import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimelineItem
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper
import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper
@ -21,7 +21,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
import org.matrix.rustcomponents.sdk.TimelineChange import org.matrix.rustcomponents.sdk.TimelineDiff
class MatrixTimelineDiffProcessorTest { class MatrixTimelineDiffProcessorTest {
private val timelineItems = MutableStateFlow<List<MatrixTimelineItem>>(emptyList()) private val timelineItems = MutableStateFlow<List<MatrixTimelineItem>>(emptyList())
@ -33,7 +33,7 @@ class MatrixTimelineDiffProcessorTest {
fun `Append adds new entries at the end of the list`() = runTest { fun `Append adds new entries at the end of the list`() = runTest {
timelineItems.value = listOf(anEvent) timelineItems.value = listOf(anEvent)
val processor = createMatrixTimelineDiffProcessor(timelineItems) val processor = createMatrixTimelineDiffProcessor(timelineItems)
processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.APPEND))) processor.postDiffs(listOf(TimelineDiff.Append(listOf(FakeFfiTimelineItem()))))
assertThat(timelineItems.value.count()).isEqualTo(2) assertThat(timelineItems.value.count()).isEqualTo(2)
assertThat(timelineItems.value).containsExactly( assertThat(timelineItems.value).containsExactly(
anEvent, anEvent,
@ -45,7 +45,7 @@ class MatrixTimelineDiffProcessorTest {
fun `PushBack adds a new entry at the end of the list`() = runTest { fun `PushBack adds a new entry at the end of the list`() = runTest {
timelineItems.value = listOf(anEvent) timelineItems.value = listOf(anEvent)
val processor = createMatrixTimelineDiffProcessor(timelineItems) val processor = createMatrixTimelineDiffProcessor(timelineItems)
processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.PUSH_BACK))) processor.postDiffs(listOf(TimelineDiff.PushBack(FakeFfiTimelineItem())))
assertThat(timelineItems.value.count()).isEqualTo(2) assertThat(timelineItems.value.count()).isEqualTo(2)
assertThat(timelineItems.value).containsExactly( assertThat(timelineItems.value).containsExactly(
anEvent, anEvent,
@ -57,7 +57,7 @@ class MatrixTimelineDiffProcessorTest {
fun `PushFront inserts a new entry at the start of the list`() = runTest { fun `PushFront inserts a new entry at the start of the list`() = runTest {
timelineItems.value = listOf(anEvent) timelineItems.value = listOf(anEvent)
val processor = createMatrixTimelineDiffProcessor(timelineItems) val processor = createMatrixTimelineDiffProcessor(timelineItems)
processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.PUSH_FRONT))) processor.postDiffs(listOf(TimelineDiff.PushFront(FakeFfiTimelineItem())))
assertThat(timelineItems.value.count()).isEqualTo(2) assertThat(timelineItems.value.count()).isEqualTo(2)
assertThat(timelineItems.value).containsExactly( assertThat(timelineItems.value).containsExactly(
MatrixTimelineItem.Other, MatrixTimelineItem.Other,
@ -69,7 +69,7 @@ class MatrixTimelineDiffProcessorTest {
fun `Set replaces an entry at some index`() = runTest { fun `Set replaces an entry at some index`() = runTest {
timelineItems.value = listOf(anEvent, anEvent2) timelineItems.value = listOf(anEvent, anEvent2)
val processor = createMatrixTimelineDiffProcessor(timelineItems) val processor = createMatrixTimelineDiffProcessor(timelineItems)
processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.SET))) processor.postDiffs(listOf(TimelineDiff.Set(1u, FakeFfiTimelineItem())))
assertThat(timelineItems.value.count()).isEqualTo(2) assertThat(timelineItems.value.count()).isEqualTo(2)
assertThat(timelineItems.value).containsExactly( assertThat(timelineItems.value).containsExactly(
anEvent, anEvent,
@ -81,7 +81,7 @@ class MatrixTimelineDiffProcessorTest {
fun `Insert inserts a new entry at the provided index`() = runTest { fun `Insert inserts a new entry at the provided index`() = runTest {
timelineItems.value = listOf(anEvent, anEvent2) timelineItems.value = listOf(anEvent, anEvent2)
val processor = createMatrixTimelineDiffProcessor(timelineItems) val processor = createMatrixTimelineDiffProcessor(timelineItems)
processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.INSERT))) processor.postDiffs(listOf(TimelineDiff.Insert(1u, FakeFfiTimelineItem())))
assertThat(timelineItems.value.count()).isEqualTo(3) assertThat(timelineItems.value.count()).isEqualTo(3)
assertThat(timelineItems.value).containsExactly( assertThat(timelineItems.value).containsExactly(
anEvent, anEvent,
@ -94,7 +94,7 @@ class MatrixTimelineDiffProcessorTest {
fun `Remove removes an entry at some index`() = runTest { fun `Remove removes an entry at some index`() = runTest {
timelineItems.value = listOf(anEvent, MatrixTimelineItem.Other, anEvent2) timelineItems.value = listOf(anEvent, MatrixTimelineItem.Other, anEvent2)
val processor = createMatrixTimelineDiffProcessor(timelineItems) val processor = createMatrixTimelineDiffProcessor(timelineItems)
processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.REMOVE))) processor.postDiffs(listOf(TimelineDiff.Remove(1u)))
assertThat(timelineItems.value.count()).isEqualTo(2) assertThat(timelineItems.value.count()).isEqualTo(2)
assertThat(timelineItems.value).containsExactly( assertThat(timelineItems.value).containsExactly(
anEvent, anEvent,
@ -106,7 +106,7 @@ class MatrixTimelineDiffProcessorTest {
fun `PopBack removes an entry at the end of the list`() = runTest { fun `PopBack removes an entry at the end of the list`() = runTest {
timelineItems.value = listOf(anEvent, anEvent2) timelineItems.value = listOf(anEvent, anEvent2)
val processor = createMatrixTimelineDiffProcessor(timelineItems) val processor = createMatrixTimelineDiffProcessor(timelineItems)
processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.POP_BACK))) processor.postDiffs(listOf(TimelineDiff.PopBack))
assertThat(timelineItems.value.count()).isEqualTo(1) assertThat(timelineItems.value.count()).isEqualTo(1)
assertThat(timelineItems.value).containsExactly( assertThat(timelineItems.value).containsExactly(
anEvent, anEvent,
@ -117,7 +117,7 @@ class MatrixTimelineDiffProcessorTest {
fun `PopFront removes an entry at the start of the list`() = runTest { fun `PopFront removes an entry at the start of the list`() = runTest {
timelineItems.value = listOf(anEvent, anEvent2) timelineItems.value = listOf(anEvent, anEvent2)
val processor = createMatrixTimelineDiffProcessor(timelineItems) val processor = createMatrixTimelineDiffProcessor(timelineItems)
processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.POP_FRONT))) processor.postDiffs(listOf(TimelineDiff.PopFront))
assertThat(timelineItems.value.count()).isEqualTo(1) assertThat(timelineItems.value.count()).isEqualTo(1)
assertThat(timelineItems.value).containsExactly( assertThat(timelineItems.value).containsExactly(
anEvent2, anEvent2,
@ -128,7 +128,7 @@ class MatrixTimelineDiffProcessorTest {
fun `Clear removes all the entries`() = runTest { fun `Clear removes all the entries`() = runTest {
timelineItems.value = listOf(anEvent, anEvent2) timelineItems.value = listOf(anEvent, anEvent2)
val processor = createMatrixTimelineDiffProcessor(timelineItems) val processor = createMatrixTimelineDiffProcessor(timelineItems)
processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.CLEAR))) processor.postDiffs(listOf(TimelineDiff.Clear))
assertThat(timelineItems.value).isEmpty() assertThat(timelineItems.value).isEmpty()
} }
@ -136,7 +136,7 @@ class MatrixTimelineDiffProcessorTest {
fun `Truncate removes all entries after the provided length`() = runTest { fun `Truncate removes all entries after the provided length`() = runTest {
timelineItems.value = listOf(anEvent, MatrixTimelineItem.Other, anEvent2) timelineItems.value = listOf(anEvent, MatrixTimelineItem.Other, anEvent2)
val processor = createMatrixTimelineDiffProcessor(timelineItems) val processor = createMatrixTimelineDiffProcessor(timelineItems)
processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.TRUNCATE))) processor.postDiffs(listOf(TimelineDiff.Truncate(1u)))
assertThat(timelineItems.value.count()).isEqualTo(1) assertThat(timelineItems.value.count()).isEqualTo(1)
assertThat(timelineItems.value).containsExactly( assertThat(timelineItems.value).containsExactly(
anEvent, anEvent,
@ -147,7 +147,7 @@ class MatrixTimelineDiffProcessorTest {
fun `Reset removes all entries and add the provided ones`() = runTest { fun `Reset removes all entries and add the provided ones`() = runTest {
timelineItems.value = listOf(anEvent, MatrixTimelineItem.Other, anEvent2) timelineItems.value = listOf(anEvent, MatrixTimelineItem.Other, anEvent2)
val processor = createMatrixTimelineDiffProcessor(timelineItems) val processor = createMatrixTimelineDiffProcessor(timelineItems)
processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.RESET))) processor.postDiffs(listOf(TimelineDiff.Reset(listOf(FakeFfiTimelineItem()))))
assertThat(timelineItems.value.count()).isEqualTo(1) assertThat(timelineItems.value.count()).isEqualTo(1)
assertThat(timelineItems.value).containsExactly( assertThat(timelineItems.value).containsExactly(
MatrixTimelineItem.Other, MatrixTimelineItem.Other,

View file

@ -18,7 +18,6 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomListService import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomListService
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimeline import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimeline
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimelineDiff
import io.element.android.libraries.matrix.impl.room.RoomContentForwarder import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomInfo
@ -33,7 +32,7 @@ import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
import org.matrix.rustcomponents.sdk.TimelineChange import org.matrix.rustcomponents.sdk.TimelineDiff
import uniffi.matrix_sdk.RoomPaginationStatus import uniffi.matrix_sdk.RoomPaginationStatus
import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline
@ -51,10 +50,7 @@ class RustTimelineTest {
runCurrent() runCurrent()
inner.emitDiff( inner.emitDiff(
listOf( listOf(
FakeFfiTimelineDiff( TimelineDiff.Reset(emptyList())
item = null,
change = TimelineChange.RESET,
)
) )
) )
with(awaitItem()) { with(awaitItem()) {

View file

@ -12,7 +12,6 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustEventTimelineItem import io.element.android.libraries.matrix.impl.fixtures.factories.aRustEventTimelineItem
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimeline import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimeline
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimelineDiff
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimelineItem import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimelineItem
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
@ -24,7 +23,7 @@ import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
import org.matrix.rustcomponents.sdk.Timeline import org.matrix.rustcomponents.sdk.Timeline
import org.matrix.rustcomponents.sdk.TimelineChange import org.matrix.rustcomponents.sdk.TimelineDiff
import uniffi.matrix_sdk_ui.EventItemOrigin import uniffi.matrix_sdk_ui.EventItemOrigin
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@ -42,7 +41,7 @@ class TimelineItemsSubscriberTest {
timelineItemsSubscriber.subscribeIfNeeded() timelineItemsSubscriber.subscribeIfNeeded()
// Wait for the listener to be set. // Wait for the listener to be set.
runCurrent() runCurrent()
timeline.emitDiff(listOf(FakeFfiTimelineDiff(item = null, change = TimelineChange.RESET))) timeline.emitDiff(listOf(TimelineDiff.Reset(emptyList())))
val final = awaitItem() val final = awaitItem()
assertThat(final).isEmpty() assertThat(final).isEmpty()
timelineItemsSubscriber.unsubscribeIfNeeded() timelineItemsSubscriber.unsubscribeIfNeeded()
@ -62,7 +61,7 @@ class TimelineItemsSubscriberTest {
timelineItemsSubscriber.subscribeIfNeeded() timelineItemsSubscriber.subscribeIfNeeded()
// Wait for the listener to be set. // Wait for the listener to be set.
runCurrent() runCurrent()
timeline.emitDiff(listOf(FakeFfiTimelineDiff(item = FakeFfiTimelineItem(), change = TimelineChange.RESET))) timeline.emitDiff(listOf(TimelineDiff.Reset(listOf(FakeFfiTimelineItem()))))
val final = awaitItem() val final = awaitItem()
assertThat(final).isNotEmpty() assertThat(final).isNotEmpty()
timelineItemsSubscriber.unsubscribeIfNeeded() timelineItemsSubscriber.unsubscribeIfNeeded()
@ -86,11 +85,10 @@ class TimelineItemsSubscriberTest {
runCurrent() runCurrent()
timeline.emitDiff( timeline.emitDiff(
listOf( listOf(
FakeFfiTimelineDiff( TimelineDiff.Reset(
item = FakeFfiTimelineItem( listOf(FakeFfiTimelineItem(
asEventResult = aRustEventTimelineItem(origin = EventItemOrigin.SYNC), asEventResult = aRustEventTimelineItem(origin = EventItemOrigin.SYNC),
), ))
change = TimelineChange.RESET,
) )
) )
) )

View file

@ -55,7 +55,6 @@ class FakeJoinedRoom(
private val roomNotificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), private val roomNotificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(),
private var createTimelineResult: (CreateTimelineParams) -> Result<Timeline> = { lambdaError() }, private var createTimelineResult: (CreateTimelineParams) -> Result<Timeline> = { lambdaError() },
private val editMessageLambda: (EventId, String, String?, List<IntentionalMention>) -> Result<Unit> = { _, _, _, _ -> lambdaError() }, private val editMessageLambda: (EventId, String, String?, List<IntentionalMention>) -> Result<Unit> = { _, _, _, _ -> lambdaError() },
private val sendCallNotificationIfNeededResult: () -> Result<Boolean> = { lambdaError() },
private val progressCallbackValues: List<Pair<Long, Long>> = emptyList(), private val progressCallbackValues: List<Pair<Long, Long>> = emptyList(),
private val generateWidgetWebViewUrlResult: (MatrixWidgetSettings, String, String?, String?) -> Result<String> = { _, _, _, _ -> lambdaError() }, private val generateWidgetWebViewUrlResult: (MatrixWidgetSettings, String, String?, String?) -> Result<String> = { _, _, _, _ -> lambdaError() },
private val getWidgetDriverResult: (MatrixWidgetSettings) -> Result<MatrixWidgetDriver> = { lambdaError() }, private val getWidgetDriverResult: (MatrixWidgetSettings) -> Result<MatrixWidgetDriver> = { lambdaError() },
@ -207,10 +206,6 @@ class FakeJoinedRoom(
return getWidgetDriverResult(widgetSettings) return getWidgetDriverResult(widgetSettings)
} }
override suspend fun sendCallNotificationIfNeeded(): Result<Boolean> = simulateLongTask {
sendCallNotificationIfNeededResult()
}
override suspend fun setSendQueueEnabled(enabled: Boolean) = simulateLongTask { override suspend fun setSendQueueEnabled(enabled: Boolean) = simulateLongTask {
setSendQueueEnabledResult(enabled) setSendQueueEnabledResult(enabled)
} }

View file

@ -11,12 +11,12 @@ import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
class FakeCallWidgetSettingsProvider( class FakeCallWidgetSettingsProvider(
private val provideFn: (String, String) -> MatrixWidgetSettings = { _, _ -> MatrixWidgetSettings("id", true, "url") } private val provideFn: (String, String, Boolean, Boolean) -> MatrixWidgetSettings = { _, _, _, _ -> MatrixWidgetSettings("id", true, "url") }
) : CallWidgetSettingsProvider { ) : CallWidgetSettingsProvider {
val providedBaseUrls = mutableListOf<String>() val providedBaseUrls = mutableListOf<String>()
override suspend fun provide(baseUrl: String, widgetId: String, encrypted: Boolean): MatrixWidgetSettings { override suspend fun provide(baseUrl: String, widgetId: String, encrypted: Boolean, direct: Boolean): MatrixWidgetSettings {
providedBaseUrls += baseUrl providedBaseUrls += baseUrl
return provideFn(baseUrl, widgetId) return provideFn(baseUrl, widgetId, encrypted, direct)
} }
} }