Merge branch 'develop' into feature/fga/join_space

This commit is contained in:
ganfra 2025-09-19 16:35:55 +02:00
commit c4308e9810
446 changed files with 5669 additions and 2617 deletions

View file

@ -287,7 +287,6 @@ class RustMatrixClient(
}
override suspend fun getRoom(roomId: RoomId): BaseRoom? = withContext(sessionDispatcher) {
innerClient.rooms()
roomFactory.getBaseRoom(roomId)
}
@ -689,12 +688,6 @@ class RustMatrixClient(
})
}.buffer(Channel.UNLIMITED)
override suspend fun availableSlidingSyncVersions(): Result<List<SlidingSyncVersion>> = withContext(sessionDispatcher) {
runCatchingExceptions {
innerClient.availableSlidingSyncVersions().map { it.map() }
}
}
override suspend fun currentSlidingSyncVersion(): Result<SlidingSyncVersion> = withContext(sessionDispatcher) {
runCatchingExceptions {
innerClient.session().slidingSyncVersion.map()

View file

@ -33,11 +33,9 @@ import io.element.android.libraries.matrix.impl.keys.PassphraseGenerator
import io.element.android.libraries.matrix.impl.mapper.toSessionData
import io.element.android.libraries.matrix.impl.paths.SessionPaths
import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
import io.element.android.libraries.sessionstorage.api.LoggedInState
import io.element.android.libraries.sessionstorage.api.LoginType
import io.element.android.libraries.sessionstorage.api.SessionStore
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext
@ -83,14 +81,6 @@ class RustMatrixAuthenticationService(
.also { sessionPaths = it }
}
override fun loggedInStateFlow(): Flow<LoggedInState> {
return sessionStore.isLoggedIn()
}
override suspend fun getLatestSessionId(): SessionId? = withContext(coroutineDispatchers.io) {
sessionStore.getLatestSession()?.userId?.let { SessionId(it) }
}
override suspend fun restoreSession(sessionId: SessionId): Result<MatrixClient> = withContext(coroutineDispatchers.io) {
runCatchingExceptions {
val sessionData = sessionStore.getSession(sessionId.value)
@ -158,7 +148,7 @@ class RustMatrixAuthenticationService(
)
val matrixClient = rustMatrixClientFactory.create(client)
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
sessionStore.storeData(sessionData)
sessionStore.addSession(sessionData)
// Clean up the strong reference held here since it's no longer necessary
currentClient = null
@ -182,7 +172,7 @@ class RustMatrixAuthenticationService(
sessionPaths = currentSessionPaths,
)
clear()
sessionStore.storeData(sessionData)
sessionStore.addSession(sessionData)
SessionId(sessionData.userId)
}
}
@ -250,7 +240,7 @@ class RustMatrixAuthenticationService(
val matrixClient = rustMatrixClientFactory.create(client)
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
sessionStore.storeData(sessionData)
sessionStore.addSession(sessionData)
// Clean up the strong reference held here since it's no longer necessary
currentClient = null
@ -295,7 +285,7 @@ class RustMatrixAuthenticationService(
)
val matrixClient = rustMatrixClientFactory.create(client)
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
sessionStore.storeData(sessionData)
sessionStore.addSession(sessionData)
// Clean up the strong reference held here since it's no longer necessary
currentClient = null

View file

@ -10,16 +10,16 @@ package io.element.android.libraries.matrix.impl.notification
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.CallNotifyType
import io.element.android.libraries.matrix.api.notification.NotificationContent
import io.element.android.libraries.matrix.api.notification.RtcNotificationType
import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessageMapper
import org.matrix.rustcomponents.sdk.MessageLikeEventContent
import org.matrix.rustcomponents.sdk.NotifyType
import org.matrix.rustcomponents.sdk.StateEventContent
import org.matrix.rustcomponents.sdk.TimelineEvent
import org.matrix.rustcomponents.sdk.TimelineEventType
import org.matrix.rustcomponents.sdk.use
import org.matrix.rustcomponents.sdk.RtcNotificationType as SdkRtcNotificationType
class TimelineEventToNotificationContentMapper {
fun map(timelineEvent: TimelineEvent): Result<NotificationContent> {
@ -78,7 +78,11 @@ private fun MessageLikeEventContent.toContent(senderId: UserId): NotificationCon
MessageLikeEventContent.CallCandidates -> NotificationContent.MessageLike.CallCandidates
MessageLikeEventContent.CallHangup -> NotificationContent.MessageLike.CallHangup
MessageLikeEventContent.CallInvite -> NotificationContent.MessageLike.CallInvite(senderId)
is MessageLikeEventContent.CallNotify -> NotificationContent.MessageLike.CallNotify(senderId, notifyType.map())
is MessageLikeEventContent.RtcNotification -> NotificationContent.MessageLike.RtcNotification(
senderId = senderId,
type = notificationType.map(),
expirationTimestampMillis = expirationTs.toLong()
)
MessageLikeEventContent.KeyVerificationAccept -> NotificationContent.MessageLike.KeyVerificationAccept
MessageLikeEventContent.KeyVerificationCancel -> NotificationContent.MessageLike.KeyVerificationCancel
MessageLikeEventContent.KeyVerificationDone -> NotificationContent.MessageLike.KeyVerificationDone
@ -101,7 +105,7 @@ private fun MessageLikeEventContent.toContent(senderId: UserId): NotificationCon
}
}
private fun NotifyType.map(): CallNotifyType = when (this) {
NotifyType.NOTIFY -> CallNotifyType.NOTIFY
NotifyType.RING -> CallNotifyType.RING
private fun SdkRtcNotificationType.map(): RtcNotificationType = when (this) {
SdkRtcNotificationType.NOTIFICATION -> RtcNotificationType.NOTIFY
SdkRtcNotificationType.RING -> RtcNotificationType.RING
}

View file

@ -15,7 +15,7 @@ fun MessageEventType.map(): MessageLikeEventType = when (this) {
MessageEventType.CALL_INVITE -> MessageLikeEventType.CALL_INVITE
MessageEventType.CALL_HANGUP -> MessageLikeEventType.CALL_HANGUP
MessageEventType.CALL_CANDIDATES -> MessageLikeEventType.CALL_CANDIDATES
MessageEventType.CALL_NOTIFY -> MessageLikeEventType.CALL_NOTIFY
MessageEventType.RTC_NOTIFICATION -> MessageLikeEventType.RTC_NOTIFICATION
MessageEventType.KEY_VERIFICATION_READY -> MessageLikeEventType.KEY_VERIFICATION_READY
MessageEventType.KEY_VERIFICATION_START -> MessageLikeEventType.KEY_VERIFICATION_START
MessageEventType.KEY_VERIFICATION_CANCEL -> MessageLikeEventType.KEY_VERIFICATION_CANCEL
@ -41,7 +41,7 @@ fun MessageLikeEventType.map(): MessageEventType = when (this) {
MessageLikeEventType.CALL_INVITE -> MessageEventType.CALL_INVITE
MessageLikeEventType.CALL_HANGUP -> MessageEventType.CALL_HANGUP
MessageLikeEventType.CALL_CANDIDATES -> MessageEventType.CALL_CANDIDATES
MessageLikeEventType.CALL_NOTIFY -> MessageEventType.CALL_NOTIFY
MessageLikeEventType.RTC_NOTIFICATION -> MessageEventType.RTC_NOTIFICATION
MessageLikeEventType.KEY_VERIFICATION_READY -> MessageEventType.KEY_VERIFICATION_READY
MessageLikeEventType.KEY_VERIFICATION_START -> MessageEventType.KEY_VERIFICATION_START
MessageLikeEventType.KEY_VERIFICATION_CANCEL -> MessageEventType.KEY_VERIFICATION_CANCEL

View file

@ -38,10 +38,12 @@ import io.element.android.libraries.matrix.impl.timeline.toRustReceiptType
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.CallDeclineListener
import org.matrix.rustcomponents.sdk.RoomInfoListener
import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
@ -300,4 +302,20 @@ class RustBaseRoom(
innerRoom.reportRoom(reason.orEmpty())
}
}
override suspend fun declineCall(notificationEventId: EventId): Result<Unit> = withContext(roomDispatcher) {
runCatchingExceptions {
innerRoom.declineCall(notificationEventId.value)
}
}
override suspend fun subscribeToCallDecline(notificationEventId: EventId): Flow<UserId> = withContext(roomDispatcher) {
mxCallbackFlow {
innerRoom.subscribeToCallDeclineEvents(notificationEventId.value, object : CallDeclineListener {
override fun call(declinerUserId: String) {
trySend(UserId(declinerUserId))
}
})
}
}
}

View file

@ -20,7 +20,7 @@ fun RustAllowRule.map(): AllowRule {
fun AllowRule.map(): RustAllowRule {
return when (this) {
is AllowRule.RoomMembership -> RustAllowRule.RoomMembership(roomId.toString())
is AllowRule.RoomMembership -> RustAllowRule.RoomMembership(roomId.value)
is AllowRule.Custom -> RustAllowRule.Custom(json)
}
}

View file

@ -25,7 +25,6 @@ object RoomMemberMapper {
membership = mapMembership(roomMember.membership),
isNameAmbiguous = roomMember.isNameAmbiguous,
powerLevel = powerLevel,
normalizedPowerLevel = roomMember.normalizedPowerLevel.into(),
isIgnored = roomMember.isIgnored,
role = mapRole(roomMember.suggestedRoleForPowerLevel, powerLevel),
membershipChangeReason = roomMember.membershipChangeReason

View file

@ -431,7 +431,7 @@ class RustTimeline(
}
}
override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit> = withContext(dispatcher) {
override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Boolean> = withContext(dispatcher) {
runCatchingExceptions {
inner.toggleReaction(
key = emoji,

View file

@ -149,7 +149,7 @@ class TimelineEventContentMapper(
)
}
is TimelineItemContent.CallInvite -> LegacyCallInviteContent
is TimelineItemContent.CallNotify -> CallNotifyContent
is TimelineItemContent.RtcNotification -> CallNotifyContent
}
}
}

View file

@ -18,10 +18,10 @@ import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.flow.first
import org.matrix.rustcomponents.sdk.newVirtualElementCallWidget
import timber.log.Timber
import uniffi.matrix_sdk.EncryptionSystem
import uniffi.matrix_sdk.HeaderStyle
import uniffi.matrix_sdk.NotificationType
import uniffi.matrix_sdk.VirtualElementCallWidgetOptions
import uniffi.matrix_sdk.VirtualElementCallWidgetConfig
import uniffi.matrix_sdk.VirtualElementCallWidgetProperties
import uniffi.matrix_sdk.Intent as CallIntent
@ContributesBinding(AppScope::class)
@ -31,19 +31,14 @@ class DefaultCallWidgetSettingsProvider(
private val callAnalyticsCredentialsProvider: CallAnalyticCredentialsProvider,
private val analyticsService: AnalyticsService,
) : CallWidgetSettingsProvider {
override suspend fun provide(baseUrl: String, widgetId: String, encrypted: Boolean, direct: Boolean): MatrixWidgetSettings {
override suspend fun provide(baseUrl: String, widgetId: String, encrypted: Boolean, direct: Boolean, hasActiveCall: Boolean): MatrixWidgetSettings {
val isAnalyticsEnabled = analyticsService.userConsentFlow.first()
val options = VirtualElementCallWidgetOptions(
val properties = VirtualElementCallWidgetProperties(
elementCallUrl = baseUrl,
widgetId = widgetId,
preload = null,
fontScale = null,
appPrompt = false,
confineToRoom = true,
font = null,
encryption = if (encrypted) EncryptionSystem.PerParticipantKeys else EncryptionSystem.Unencrypted,
intent = CallIntent.START_CALL,
hideScreensharing = false,
posthogUserId = callAnalyticsCredentialsProvider.posthogUserId.takeIf { isAnalyticsEnabled },
posthogApiHost = callAnalyticsCredentialsProvider.posthogApiHost.takeIf { isAnalyticsEnabled },
posthogApiKey = callAnalyticsCredentialsProvider.posthogApiKey.takeIf { isAnalyticsEnabled },
@ -51,13 +46,25 @@ class DefaultCallWidgetSettingsProvider(
sentryDsn = callAnalyticsCredentialsProvider.sentryDsn.takeIf { isAnalyticsEnabled },
sentryEnvironment = if (buildMeta.buildType == BuildType.RELEASE) "RELEASE" else "DEBUG",
parentUrl = null,
// For backwards compatibility, it'll be ignored in recent versions of Element Call
hideHeader = true,
controlledMediaDevices = true,
header = HeaderStyle.APP_BAR,
sendNotificationType = if (direct) NotificationType.RING else NotificationType.NOTIFICATION,
)
val rustWidgetSettings = newVirtualElementCallWidget(options)
val config = VirtualElementCallWidgetConfig(
// TODO remove this once we have the next EC version
preload = false,
// TODO remove this once we have the next EC version
skipLobby = null,
intent = when {
direct && hasActiveCall -> CallIntent.JOIN_EXISTING_DM
hasActiveCall -> CallIntent.JOIN_EXISTING
direct -> CallIntent.START_CALL_DM
else -> CallIntent.START_CALL
}.also {
Timber.d("Starting/joining call with intent: $it")
}
)
val rustWidgetSettings = newVirtualElementCallWidget(
props = properties,
config = config,
)
return MatrixWidgetSettings.fromRustWidgetSettings(rustWidgetSettings)
}
}

View file

@ -10,8 +10,10 @@ package io.element.android.libraries.matrix.impl
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClientBuilder
import org.matrix.rustcomponents.sdk.ClientBuilder
class FakeClientBuilderProvider : ClientBuilderProvider {
class FakeClientBuilderProvider(
private val provideResult: () -> ClientBuilder = { FakeFfiClientBuilder() }
) : ClientBuilderProvider {
override fun provide(): ClientBuilder {
return FakeFfiClientBuilder()
return provideResult()
}
}

View file

@ -39,6 +39,7 @@ fun TestScope.createRustMatrixClientFactory(
baseDirectory: File = File("/base"),
cacheDirectory: File = File("/cache"),
sessionStore: SessionStore = InMemorySessionStore(),
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
) = RustMatrixClientFactory(
baseDirectory = baseDirectory,
cacheDirectory = cacheDirectory,
@ -52,5 +53,5 @@ fun TestScope.createRustMatrixClientFactory(
analyticsService = FakeAnalyticsService(),
featureFlagService = FakeFeatureFlagService(),
timelineEventTypeFilterFactory = FakeTimelineEventTypeFilterFactory(),
clientBuilderProvider = FakeClientBuilderProvider(),
clientBuilderProvider = clientBuilderProvider,
)

View file

@ -8,14 +8,17 @@
package io.element.android.libraries.matrix.impl.auth
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.impl.ClientBuilderProvider
import io.element.android.libraries.matrix.impl.FakeClientBuilderProvider
import io.element.android.libraries.matrix.impl.createRustMatrixClientFactory
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClientBuilder
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiHomeserverLoginDetails
import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
import io.element.android.libraries.matrix.test.auth.FakeOidcRedirectUrlProvider
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
@ -24,18 +27,28 @@ import java.io.File
class RustMatrixAuthenticationServiceTest {
@Test
fun `getLatestSessionId should return the value from the store`() = runTest {
val sessionStore = InMemorySessionStore()
fun `setHomeserver is successful`() = runTest {
val sut = createRustMatrixAuthenticationService(
sessionStore = sessionStore,
clientBuilderProvider = FakeClientBuilderProvider(
provideResult = {
FakeFfiClientBuilder(
buildResult = {
FakeFfiClient(
homeserverLoginDetailsResult = {
FakeFfiHomeserverLoginDetails()
}
)
}
)
}
),
)
assertThat(sut.getLatestSessionId()).isNull()
sessionStore.storeData(aSessionData(sessionId = "@alice:server.org"))
assertThat(sut.getLatestSessionId()).isEqualTo(SessionId("@alice:server.org"))
assertThat(sut.setHomeserver("matrix.org").isSuccess).isTrue()
}
private fun TestScope.createRustMatrixAuthenticationService(
sessionStore: SessionStore = InMemorySessionStore(),
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
): RustMatrixAuthenticationService {
val baseDirectory = File("/base")
val cacheDirectory = File("/cache")
@ -43,6 +56,7 @@ class RustMatrixAuthenticationServiceTest {
baseDirectory = baseDirectory,
cacheDirectory = cacheDirectory,
sessionStore = sessionStore,
clientBuilderProvider = clientBuilderProvider,
)
return RustMatrixAuthenticationService(
sessionPathsFactory = SessionPathsFactory(baseDirectory, cacheDirectory),

View file

@ -30,7 +30,6 @@ fun aRustRoomMember(
membership = membership,
isNameAmbiguous = isNameAmbiguous,
powerLevel = powerLevel,
normalizedPowerLevel = powerLevel,
isIgnored = isIgnored,
suggestedRoleForPowerLevel = role,
membershipChangeReason = membershipChangeReason,

View file

@ -43,4 +43,5 @@ fun aRustSpaceRoom(
childrenCount = childrenCount,
state = state,
heroes = heroes,
via = emptyList()
)

View file

@ -15,6 +15,7 @@ import io.element.android.tests.testutils.simulateLongTask
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.ClientDelegate
import org.matrix.rustcomponents.sdk.Encryption
import org.matrix.rustcomponents.sdk.HomeserverLoginDetails
import org.matrix.rustcomponents.sdk.IgnoredUsersListener
import org.matrix.rustcomponents.sdk.NoPointer
import org.matrix.rustcomponents.sdk.NotificationClient
@ -41,6 +42,7 @@ class FakeFfiClient(
private val session: Session = aRustSession(),
private val clearCachesResult: () -> Unit = { lambdaError() },
private val withUtdHook: (UnableToDecryptDelegate) -> Unit = { lambdaError() },
private val homeserverLoginDetailsResult: () -> HomeserverLoginDetails = { lambdaError() },
private val closeResult: () -> Unit = {},
) : Client(NoPointer) {
override fun userId(): String = userId
@ -71,6 +73,7 @@ class FakeFfiClient(
override suspend fun ignoredUsers(): List<String> {
return emptyList()
}
override fun subscribeToIgnoredUsers(listener: IgnoredUsersListener): TaskHandle {
return FakeFfiTaskHandle()
}
@ -78,5 +81,10 @@ class FakeFfiClient(
override suspend fun getProfile(userId: String): UserProfile {
return UserProfile(userId = userId, displayName = null, avatarUrl = null)
}
override suspend fun homeserverLoginDetails(): HomeserverLoginDetails {
return homeserverLoginDetailsResult()
}
override fun close() = closeResult()
}

View file

@ -17,7 +17,9 @@ import uniffi.matrix_sdk.BackupDownloadStrategy
import uniffi.matrix_sdk_crypto.CollectStrategy
import uniffi.matrix_sdk_crypto.DecryptionSettings
class FakeFfiClientBuilder : ClientBuilder(NoPointer) {
class FakeFfiClientBuilder(
val buildResult: () -> Client = { FakeFfiClient(withUtdHook = {}) }
) : ClientBuilder(NoPointer) {
override fun addRootCertificates(certificates: List<ByteArray>) = this
override fun autoEnableBackups(autoEnableBackups: Boolean) = this
override fun autoEnableCrossSigning(autoEnableCrossSigning: Boolean) = this
@ -41,7 +43,5 @@ class FakeFfiClientBuilder : ClientBuilder(NoPointer) {
override fun enableShareHistoryOnInvite(enableShareHistoryOnInvite: Boolean): ClientBuilder = this
override fun threadsEnabled(enabled: Boolean, threadSubscriptions: Boolean): ClientBuilder = this
override suspend fun build(): Client {
return FakeFfiClient(withUtdHook = {})
}
override suspend fun build() = buildResult()
}

View file

@ -0,0 +1,58 @@
/*
* Copyright 2025 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 io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask
import org.matrix.rustcomponents.sdk.NoPointer
import org.matrix.rustcomponents.sdk.SpaceListUpdate
import org.matrix.rustcomponents.sdk.SpaceRoom
import org.matrix.rustcomponents.sdk.SpaceRoomList
import org.matrix.rustcomponents.sdk.SpaceRoomListEntriesListener
import org.matrix.rustcomponents.sdk.SpaceRoomListPaginationStateListener
import org.matrix.rustcomponents.sdk.TaskHandle
import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState
class FakeFfiSpaceRoomList(
private val paginateResult: () -> Unit = { lambdaError() },
private val paginationStateResult: () -> SpaceRoomListPaginationState = { lambdaError() },
private val roomsResult: () -> List<SpaceRoom> = { lambdaError() },
) : SpaceRoomList(NoPointer) {
private var spaceRoomListPaginationStateListener: SpaceRoomListPaginationStateListener? = null
private var spaceRoomListEntriesListener: SpaceRoomListEntriesListener? = null
override suspend fun paginate() = simulateLongTask {
paginateResult()
}
override fun paginationState(): SpaceRoomListPaginationState {
return paginationStateResult()
}
override fun rooms(): List<SpaceRoom> {
return roomsResult()
}
override fun subscribeToPaginationStateUpdates(listener: SpaceRoomListPaginationStateListener): TaskHandle {
spaceRoomListPaginationStateListener = listener
return FakeFfiTaskHandle()
}
fun triggerPaginationStateUpdate(state: SpaceRoomListPaginationState) {
spaceRoomListPaginationStateListener?.onUpdate(state)
}
override fun subscribeToRoomUpdate(listener: SpaceRoomListEntriesListener): TaskHandle {
spaceRoomListEntriesListener = listener
return FakeFfiTaskHandle()
}
fun triggerRoomListUpdate(rooms: List<SpaceListUpdate>) {
spaceRoomListEntriesListener?.onUpdate(rooms)
}
}

View file

@ -19,7 +19,7 @@ class MessageEventTypeKtTest {
assertThat(MessageLikeEventType.CALL_INVITE.map()).isEqualTo(MessageEventType.CALL_INVITE)
assertThat(MessageLikeEventType.CALL_HANGUP.map()).isEqualTo(MessageEventType.CALL_HANGUP)
assertThat(MessageLikeEventType.CALL_CANDIDATES.map()).isEqualTo(MessageEventType.CALL_CANDIDATES)
assertThat(MessageLikeEventType.CALL_NOTIFY.map()).isEqualTo(MessageEventType.CALL_NOTIFY)
assertThat(MessageLikeEventType.RTC_NOTIFICATION.map()).isEqualTo(MessageEventType.RTC_NOTIFICATION)
assertThat(MessageLikeEventType.KEY_VERIFICATION_READY.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_READY)
assertThat(MessageLikeEventType.KEY_VERIFICATION_START.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_START)
assertThat(MessageLikeEventType.KEY_VERIFICATION_CANCEL.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_CANCEL)
@ -46,7 +46,7 @@ class MessageEventTypeKtTest {
assertThat(MessageEventType.CALL_INVITE.map()).isEqualTo(MessageLikeEventType.CALL_INVITE)
assertThat(MessageEventType.CALL_HANGUP.map()).isEqualTo(MessageLikeEventType.CALL_HANGUP)
assertThat(MessageEventType.CALL_CANDIDATES.map()).isEqualTo(MessageLikeEventType.CALL_CANDIDATES)
assertThat(MessageEventType.CALL_NOTIFY.map()).isEqualTo(MessageLikeEventType.CALL_NOTIFY)
assertThat(MessageEventType.RTC_NOTIFICATION.map()).isEqualTo(MessageLikeEventType.RTC_NOTIFICATION)
assertThat(MessageEventType.KEY_VERIFICATION_READY.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_READY)
assertThat(MessageEventType.KEY_VERIFICATION_START.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_START)
assertThat(MessageEventType.KEY_VERIFICATION_CANCEL.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_CANCEL)

View file

@ -0,0 +1,118 @@
/*
* Copyright 2025 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.
*/
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.libraries.matrix.impl.spaces
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSpaceRoom
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiSpaceRoomList
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
import io.element.android.libraries.previewutils.room.aSpaceRoom
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.matrix.rustcomponents.sdk.SpaceListUpdate
import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState
import org.matrix.rustcomponents.sdk.SpaceRoomList as InnerSpaceRoomList
class RustSpaceRoomListTest {
@Test
fun `paginationStatusFlow emits values`() = runTest {
val innerSpaceRoomList = FakeFfiSpaceRoomList(
paginationStateResult = { SpaceRoomListPaginationState.Idle(false) }
)
val sut = createRustSpaceRoomList(
innerSpaceRoomList = innerSpaceRoomList,
)
sut.paginationStatusFlow.test {
// First value is the initial one
assertThat(awaitItem()).isEqualTo(SpaceRoomList.PaginationStatus.Idle(hasMoreToLoad = false))
// First value after the subscription occurs
assertThat(awaitItem()).isEqualTo(SpaceRoomList.PaginationStatus.Idle(hasMoreToLoad = true))
innerSpaceRoomList.triggerPaginationStateUpdate(SpaceRoomListPaginationState.Loading)
assertThat(awaitItem()).isEqualTo(SpaceRoomList.PaginationStatus.Loading)
innerSpaceRoomList.triggerPaginationStateUpdate(SpaceRoomListPaginationState.Idle(true))
assertThat(awaitItem()).isEqualTo(SpaceRoomList.PaginationStatus.Idle(hasMoreToLoad = false))
innerSpaceRoomList.triggerPaginationStateUpdate(SpaceRoomListPaginationState.Idle(false))
assertThat(awaitItem()).isEqualTo(SpaceRoomList.PaginationStatus.Idle(hasMoreToLoad = true))
}
}
@Test
fun `spaceRoomsFlow emits values`() = runTest {
val innerSpaceRoomList = FakeFfiSpaceRoomList(
paginationStateResult = { SpaceRoomListPaginationState.Idle(false) }
)
val sut = createRustSpaceRoomList(
innerSpaceRoomList = innerSpaceRoomList,
)
sut.spaceRoomsFlow.test {
// Give time for the subscription to be set
runCurrent()
innerSpaceRoomList.triggerRoomListUpdate(
listOf(
SpaceListUpdate.PushBack(aRustSpaceRoom(roomId = A_ROOM_ID_2))
)
)
val rooms = awaitItem()
assertThat(rooms).hasSize(1)
assertThat(rooms[0].roomId).isEqualTo(A_ROOM_ID_2)
}
}
@Test
fun `paginate invokes paginate on the inner class`() = runTest {
val paginateResult = lambdaRecorder<Unit> { }
val innerSpaceRoomList = FakeFfiSpaceRoomList(
paginateResult = paginateResult,
)
val sut = createRustSpaceRoomList(
innerSpaceRoomList = innerSpaceRoomList,
)
sut.paginate()
paginateResult.assertions().isCalledOnce()
}
@Test
fun `currentSpaceFlow reads value from the SpaceRoomCache`() = runTest {
val spaceRoomCache = SpaceRoomCache()
val sut = createRustSpaceRoomList(
spaceRoomCache = spaceRoomCache,
)
sut.currentSpaceFlow().test {
assertThat(awaitItem()).isNull()
val spaceRoom = aSpaceRoom(roomId = A_ROOM_ID)
spaceRoomCache.update(listOf(spaceRoom))
assertThat(awaitItem()).isEqualTo(spaceRoom)
}
}
private fun TestScope.createRustSpaceRoomList(
roomId: RoomId = A_ROOM_ID,
innerSpaceRoomList: InnerSpaceRoomList = FakeFfiSpaceRoomList(),
innerProvider: suspend () -> InnerSpaceRoomList = { innerSpaceRoomList },
spaceRoomMapper: SpaceRoomMapper = SpaceRoomMapper(),
spaceRoomCache: SpaceRoomCache = SpaceRoomCache(),
): RustSpaceRoomList {
return RustSpaceRoomList(
roomId = roomId,
innerProvider = innerProvider,
sessionCoroutineScope = backgroundScope,
spaceRoomMapper = spaceRoomMapper,
spaceRoomCache = spaceRoomCache,
)
}
}