Merge branch 'develop' into separate_import_error

This commit is contained in:
Hubert Chathi 2025-10-02 14:33:55 -04:00 committed by GitHub
commit 8f8e190e68
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2773 changed files with 29051 additions and 10914 deletions

View file

@ -1,6 +1,7 @@
import config.BuildTimeConfig
import extension.buildConfigFieldStr
import extension.setupAnvil
import extension.setupDependencyInjection
import extension.testCommonDependencies
/*
* Copyright 2022-2024 New Vector Ltd.
@ -15,6 +16,8 @@ plugins {
alias(libs.plugins.kotlin.serialization)
}
setupDependencyInjection()
android {
namespace = "io.element.android.libraries.matrix.api"
@ -42,11 +45,8 @@ android {
}
}
setupAnvil()
dependencies {
implementation(projects.libraries.di)
implementation(libs.dagger)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.core)
implementation(projects.services.analytics.api)
@ -55,7 +55,6 @@ dependencies {
implementation(libs.coroutines.core)
api(projects.libraries.architecture)
testImplementation(libs.test.junit)
testImplementation(libs.test.truth)
testCommonDependencies(libs)
testImplementation(projects.libraries.matrix.test)
}

View file

@ -31,6 +31,7 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.spaces.SpaceService
import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
@ -47,6 +48,7 @@ interface MatrixClient {
val deviceId: DeviceId
val userProfile: StateFlow<MatrixUser>
val roomListService: RoomListService
val spaceService: SpaceService
val mediaLoader: MatrixMediaLoader
val sessionCoroutineScope: CoroutineScope
val ignoredUsersFlow: StateFlow<ImmutableList<UserId>>
@ -154,11 +156,6 @@ interface MatrixClient {
*/
suspend fun currentSlidingSyncVersion(): Result<SlidingSyncVersion>
/**
* Returns the available sliding sync versions for the current user.
*/
suspend fun availableSlidingSyncVersions(): Result<List<SlidingSyncVersion>>
fun canDeactivateAccount(): Boolean
suspend fun deactivateAccount(password: String, eraseData: Boolean): Result<Unit>
@ -176,6 +173,16 @@ interface MatrixClient {
* Returns the maximum file upload size allowed by the Matrix server.
*/
suspend fun getMaxFileUploadSize(): Result<Long>
/**
* Returns the list of shared recent emoji reactions for this account.
*/
suspend fun getRecentEmojis(): Result<List<String>>
/**
* Adds an emoji to the list of recent emoji reactions for this account.
*/
suspend fun addRecentEmoji(emoji: String): Result<Unit>
}
/**

View file

@ -8,6 +8,7 @@
package io.element.android.libraries.matrix.api.auth
sealed class AuthenticationException(message: String) : Exception(message) {
class AccountAlreadyLoggedIn(userId: String) : AuthenticationException(userId)
class InvalidServerName(message: String) : AuthenticationException(message)
class SlidingSyncVersion(message: String) : AuthenticationException(message)
class Oidc(message: String) : AuthenticationException(message)

View file

@ -13,14 +13,9 @@ import io.element.android.libraries.matrix.api.auth.external.ExternalSession
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.sessionstorage.api.LoggedInState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
interface MatrixAuthenticationService {
fun loggedInStateFlow(): Flow<LoggedInState>
suspend fun getLatestSessionId(): SessionId?
/**
* Restore a session from a [sessionId].
* Do not restore anything it the access token is not valid anymore.

View file

@ -151,7 +151,7 @@ object MatrixPatterns {
val urlMatch = match.groupValues[1]
when (val permalink = permalinkParser.parse(urlMatch)) {
is PermalinkData.UserLink -> {
add(MatrixPatternResult(MatrixPatternType.USER_ID, permalink.userId.toString(), match.range.first, match.range.last + 1))
add(MatrixPatternResult(MatrixPatternType.USER_ID, permalink.userId.value, match.range.first, match.range.last + 1))
}
is PermalinkData.RoomLink -> {
when (permalink.roomIdOrAlias) {

View file

@ -7,9 +7,10 @@
package io.element.android.libraries.matrix.api.mxc
import javax.inject.Inject
import dev.zacsweers.metro.Inject
class MxcTools @Inject constructor() {
@Inject
class MxcTools {
/**
* Regex to match a Matrix Content (mxc://) URI.
*

View file

@ -49,9 +49,10 @@ sealed interface NotificationContent {
val senderId: UserId,
) : MessageLike
data class CallNotify(
data class RtcNotification(
val senderId: UserId,
val type: CallNotifyType,
val type: RtcNotificationType,
val expirationTimestampMillis: Long
) : MessageLike
data object CallHangup : MessageLike
@ -118,7 +119,7 @@ sealed interface NotificationContent {
) : NotificationContent
}
enum class CallNotifyType {
enum class RtcNotificationType {
RING,
NOTIFY
}

View file

@ -8,6 +8,7 @@
package io.element.android.libraries.matrix.api.permalink
import android.net.Uri
import android.os.Parcelable
import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
@ -15,13 +16,15 @@ import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.UserId
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.parcelize.Parcelize
/**
* This sealed class represents all the permalink cases.
* You don't have to instantiate yourself but should use [PermalinkParser] instead.
*/
@Immutable
sealed interface PermalinkData {
@Parcelize
sealed interface PermalinkData : Parcelable {
data class RoomLink(
val roomIdOrAlias: RoomIdOrAlias,
val eventId: EventId? = null,

View file

@ -0,0 +1,23 @@
/*
* 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.api.recentemojis
import dev.zacsweers.metro.Inject
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.MatrixClient
import kotlinx.coroutines.withContext
@Inject
class AddRecentEmoji(
private val client: MatrixClient,
private val dispatchers: CoroutineDispatchers,
) {
suspend operator fun invoke(emoji: String): Result<Unit> = withContext(dispatchers.io) {
client.addRecentEmoji(emoji)
}
}

View file

@ -0,0 +1,30 @@
/*
* 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.api.recentemojis
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import kotlinx.coroutines.withContext
fun interface GetRecentEmojis {
suspend operator fun invoke(): Result<List<String>>
}
@ContributesBinding(SessionScope::class)
@Inject
class DefaultGetRecentEmojis(
private val client: MatrixClient,
private val dispatchers: CoroutineDispatchers,
) : GetRecentEmojis {
override suspend operator fun invoke(): Result<List<String>> = withContext(dispatchers.io) {
client.getRecentEmojis()
}
}

View file

@ -18,6 +18,7 @@ import io.element.android.libraries.matrix.api.room.tombstone.PredecessorRoom
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import java.io.Closeable
@ -239,6 +240,12 @@ interface BaseRoom : Closeable {
*/
suspend fun reportRoom(reason: String?): Result<Unit>
suspend fun declineCall(notificationEventId: EventId): Result<Unit>
suspend fun subscribeToCallDecline(notificationEventId: EventId): Flow<UserId>
suspend fun threadRootIdForEvent(eventId: EventId): Result<ThreadId?>
/**
* Destroy the room and release all resources associated to it.
*/

View file

@ -7,28 +7,32 @@
package io.element.android.libraries.matrix.api.room
enum class MessageEventType {
CALL_ANSWER,
CALL_INVITE,
CALL_HANGUP,
CALL_CANDIDATES,
CALL_NOTIFY,
KEY_VERIFICATION_READY,
KEY_VERIFICATION_START,
KEY_VERIFICATION_CANCEL,
KEY_VERIFICATION_ACCEPT,
KEY_VERIFICATION_KEY,
KEY_VERIFICATION_MAC,
KEY_VERIFICATION_DONE,
REACTION,
ROOM_ENCRYPTED,
ROOM_MESSAGE,
ROOM_REDACTION,
STICKER,
POLL_END,
POLL_RESPONSE,
POLL_START,
UNSTABLE_POLL_END,
UNSTABLE_POLL_RESPONSE,
UNSTABLE_POLL_START,
import androidx.compose.runtime.Immutable
@Immutable
sealed interface MessageEventType {
data object CallAnswer : MessageEventType
data object CallInvite : MessageEventType
data object CallHangup : MessageEventType
data object CallCandidates : MessageEventType
data object RtcNotification : MessageEventType
data object KeyVerificationReady : MessageEventType
data object KeyVerificationStart : MessageEventType
data object KeyVerificationCancel : MessageEventType
data object KeyVerificationAccept : MessageEventType
data object KeyVerificationKey : MessageEventType
data object KeyVerificationMac : MessageEventType
data object KeyVerificationDone : MessageEventType
data object Reaction : MessageEventType
data object RoomEncrypted : MessageEventType
data object RoomMessage : MessageEventType
data object RoomRedaction : MessageEventType
data object Sticker : MessageEventType
data object PollEnd : MessageEventType
data object PollResponse : MessageEventType
data object PollStart : MessageEventType
data object UnstablePollEnd : MessageEventType
data object UnstablePollResponse : MessageEventType
data object UnstablePollStart : MessageEventType
data class Other(val type: String) : MessageEventType
}

View file

@ -17,7 +17,6 @@ data class RoomMember(
val membership: RoomMembershipState,
val isNameAmbiguous: Boolean,
val powerLevel: Long,
val normalizedPowerLevel: Long,
val isIgnored: Boolean,
val role: Role,
val membershipChangeReason: String?,

View file

@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.asSharedFlow
class RoomMembershipObserver {
data class RoomMembershipUpdate(
val roomId: RoomId,
val isSpace: Boolean,
val isUserInRoom: Boolean,
val change: MembershipChange,
)
@ -22,12 +23,23 @@ class RoomMembershipObserver {
private val _updates = MutableSharedFlow<RoomMembershipUpdate>(extraBufferCapacity = 10)
val updates = _updates.asSharedFlow()
suspend fun notifyUserLeftRoom(roomId: RoomId, membershipBeforeLeft: CurrentUserMembership) {
suspend fun notifyUserLeftRoom(
roomId: RoomId,
isSpace: Boolean,
membershipBeforeLeft: CurrentUserMembership,
) {
val membershipChange = when (membershipBeforeLeft) {
CurrentUserMembership.INVITED -> MembershipChange.INVITATION_REJECTED
CurrentUserMembership.KNOCKED -> MembershipChange.KNOCK_RETRACTED
else -> MembershipChange.LEFT
}
_updates.emit(RoomMembershipUpdate(roomId, false, membershipChange))
_updates.emit(
RoomMembershipUpdate(
roomId = roomId,
isSpace = isSpace,
isUserInRoom = false,
change = membershipChange,
)
)
}
}

View file

@ -7,8 +7,10 @@
package io.element.android.libraries.matrix.api.room.join
import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.core.RoomId
@Immutable
sealed interface AllowRule {
data class RoomMembership(val roomId: RoomId) : AllowRule
data class Custom(val json: String) : AllowRule

View file

@ -7,12 +7,16 @@
package io.element.android.libraries.matrix.api.room.join
import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.ImmutableList
@Immutable
sealed interface JoinRule {
data object Public : JoinRule
data object Private : JoinRule
data object Knock : JoinRule
data object Invite : JoinRule
data class Restricted(val rules: List<AllowRule>) : JoinRule
data class KnockRestricted(val rules: List<AllowRule>) : JoinRule
data class Restricted(val rules: ImmutableList<AllowRule>) : JoinRule
data class KnockRestricted(val rules: ImmutableList<AllowRule>) : JoinRule
data class Custom(val value: String) : JoinRule
}

View file

@ -0,0 +1,37 @@
/*
* 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.api.spaces
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomType
import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.user.MatrixUser
data class SpaceRoom(
val name: String?,
val avatarUrl: String?,
val canonicalAlias: RoomAlias?,
val childrenCount: Int,
val guestCanJoin: Boolean,
val heroes: List<MatrixUser>,
val joinRule: JoinRule?,
val numJoinedMembers: Int,
val roomId: RoomId,
val roomType: RoomType,
val state: CurrentUserMembership?,
val topic: String?,
val worldReadable: Boolean,
/**
* The via parameters of the room.
*/
val via: List<String>,
) {
val isSpace = roomType == RoomType.Space
}

View file

@ -0,0 +1,30 @@
/*
* 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.api.spaces
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import java.util.Optional
interface SpaceRoomList {
sealed interface PaginationStatus {
data object Loading : PaginationStatus
data class Idle(val hasMoreToLoad: Boolean) : PaginationStatus
}
val roomId: RoomId
val currentSpaceFlow: StateFlow<Optional<SpaceRoom>>
val spaceRoomsFlow: Flow<List<SpaceRoom>>
val paginationStatusFlow: StateFlow<PaginationStatus>
suspend fun paginate(): Result<Unit>
fun destroy()
}

View file

@ -0,0 +1,18 @@
/*
* 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.api.spaces
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.coroutines.flow.SharedFlow
interface SpaceService {
val spaceRoomsFlow: SharedFlow<List<SpaceRoom>>
suspend fun joinedSpaces(): Result<List<SpaceRoom>>
fun spaceRoomList(id: RoomId): SpaceRoomList
}

View file

@ -151,7 +151,7 @@ interface Timeline : AutoCloseable {
suspend fun redactEvent(eventOrTransactionId: EventOrTransactionId, reason: String?): Result<Unit>
suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit>
suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Boolean>
suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit>

View file

@ -14,10 +14,10 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
data class EventThreadInfo(
val threadRootId: ThreadId?,
val threadSummary: ThreadSummary?,
)
sealed interface EventThreadInfo {
data class ThreadRoot(val summary: ThreadSummary) : EventThreadInfo
data class ThreadResponse(val threadRootId: ThreadId) : EventThreadInfo
}
data class ThreadSummary(
val latestEvent: AsyncData<EmbeddedEventInfo>,

View file

@ -24,7 +24,7 @@ data class MessageContent(
val body: String,
val inReplyTo: InReplyTo?,
val isEdited: Boolean,
val threadInfo: EventThreadInfo,
val threadInfo: EventThreadInfo?,
val type: MessageType
) : EventContent

View file

@ -15,5 +15,6 @@ object EventType {
// Call Events
const val CALL_INVITE = "m.call.invite"
const val CALL_NOTIFY = "m.call.notify"
const val RTC_NOTIFICATION = "org.matrix.msc4075.rtc.notification"
}

View file

@ -1,21 +0,0 @@
/*
* Copyright 2023, 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.api.user
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.SessionId
import javax.inject.Inject
@SingleIn(SessionScope::class)
class CurrentSessionIdHolder @Inject constructor(matrixClient: MatrixClient) {
val current = matrixClient.sessionId
fun isCurrentSession(sessionId: SessionId?): Boolean = current == sessionId
}

View file

@ -10,20 +10,14 @@ package io.element.android.libraries.matrix.api.verification
import android.os.Parcelable
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.FlowId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.parcelize.Parcelize
@Parcelize
data class SessionVerificationRequestDetails(
val senderProfile: SenderProfile,
val senderProfile: MatrixUser,
val flowId: FlowId,
val deviceId: DeviceId,
val deviceDisplayName: String?,
val firstSeenTimestamp: Long,
) : Parcelable {
@Parcelize
data class SenderProfile(
val userId: UserId,
val displayName: String?,
val avatarUrl: String?,
) : Parcelable
}
) : Parcelable

View file

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

View file

@ -1,4 +1,5 @@
import extension.setupAnvil
import extension.setupDependencyInjection
import extension.testCommonDependencies
/*
* Copyright 2022-2024 New Vector Ltd.
@ -16,7 +17,7 @@ android {
namespace = "io.element.android.libraries.matrix.impl"
}
setupAnvil()
setupDependencyInjection()
dependencies {
releaseImplementation(libs.matrix.sdk)
@ -35,24 +36,18 @@ dependencies {
implementation(projects.services.analytics.api)
implementation(projects.services.toolbox.api)
api(projects.libraries.matrix.api)
implementation(libs.dagger)
implementation(projects.libraries.core)
implementation("net.java.dev.jna:jna:5.17.0@aar")
implementation("net.java.dev.jna:jna:5.18.1@aar")
implementation(libs.androidx.datastore.preferences)
implementation(libs.serialization.json)
implementation(libs.kotlinx.collections.immutable)
testImplementation(libs.test.junit)
testImplementation(libs.test.truth)
testImplementation(libs.test.robolectric)
testCommonDependencies(libs)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.sessionStorage.implMemory)
testImplementation(projects.libraries.previewutils)
testImplementation(projects.libraries.sessionStorage.test)
testImplementation(projects.services.analytics.test)
testImplementation(projects.services.toolbox.test)
testImplementation(projects.tests.testutils)
testImplementation(libs.coroutines.test)
testImplementation(libs.test.turbine)
}

View file

@ -7,17 +7,18 @@
package io.element.android.libraries.matrix.impl
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import org.matrix.rustcomponents.sdk.ClientBuilder
import javax.inject.Inject
interface ClientBuilderProvider {
fun provide(): ClientBuilder
}
@ContributesBinding(AppScope::class)
class RustClientBuilderProvider @Inject constructor() : ClientBuilderProvider {
@Inject
class RustClientBuilderProvider : ClientBuilderProvider {
override fun provide(): ClientBuilder {
return ClientBuilder()
}

View file

@ -42,6 +42,7 @@ import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.spaces.SpaceService
import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.sync.SyncState
@ -50,6 +51,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.impl.encryption.RustEncryptionService
import io.element.android.libraries.matrix.impl.exception.mapClientException
import io.element.android.libraries.matrix.impl.mapper.map
import io.element.android.libraries.matrix.impl.media.RustMediaLoader
import io.element.android.libraries.matrix.impl.media.RustMediaPreviewService
import io.element.android.libraries.matrix.impl.notification.RustNotificationService
@ -71,9 +73,9 @@ import io.element.android.libraries.matrix.impl.roomdirectory.map
import io.element.android.libraries.matrix.impl.roomlist.RoomListFactory
import io.element.android.libraries.matrix.impl.roomlist.RustRoomListService
import io.element.android.libraries.matrix.impl.roomlist.roomOrNull
import io.element.android.libraries.matrix.impl.spaces.RustSpaceService
import io.element.android.libraries.matrix.impl.sync.RustSyncService
import io.element.android.libraries.matrix.impl.sync.map
import io.element.android.libraries.matrix.impl.usersearch.UserProfileMapper
import io.element.android.libraries.matrix.impl.usersearch.UserSearchResultMapper
import io.element.android.libraries.matrix.impl.util.SessionPathsProvider
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
@ -143,6 +145,7 @@ class RustMatrixClient(
private val sessionDispatcher = dispatchers.io.limitedParallelism(64)
private val innerRoomListService = innerSyncService.roomListService()
private val innerSpaceService = innerClient.spaceService()
private val rustSyncService = RustSyncService(
inner = innerSyncService,
@ -184,6 +187,12 @@ class RustMatrixClient(
roomSyncSubscriber = roomSyncSubscriber,
)
override val spaceService: SpaceService = RustSpaceService(
innerSpaceService = innerSpaceService,
sessionCoroutineScope = sessionCoroutineScope,
sessionDispatcher = sessionDispatcher,
)
private val verificationService = RustSessionVerificationService(
client = innerClient,
isSyncServiceReady = rustSyncService.syncState.map { it == SyncState.Running },
@ -226,7 +235,6 @@ class RustMatrixClient(
private val _userProfile: MutableStateFlow<MatrixUser> = MutableStateFlow(
MatrixUser(
userId = sessionId,
// TODO cache for displayName?
displayName = null,
avatarUrl = null,
)
@ -255,6 +263,16 @@ class RustMatrixClient(
// Start notification settings
notificationSettingsService.start()
// Update the user profile in the session store if needed
sessionStore.getSession(sessionId.value)?.let { sessionData ->
_userProfile.emit(
MatrixUser(
userId = sessionId,
displayName = sessionData.userDisplayName,
avatarUrl = sessionData.userAvatarUrl,
)
)
}
// Force a refresh of the profile
getUserProfile()
}
@ -278,7 +296,6 @@ class RustMatrixClient(
}
override suspend fun getRoom(roomId: RoomId): BaseRoom? = withContext(sessionDispatcher) {
innerClient.rooms()
roomFactory.getBaseRoom(roomId)
}
@ -386,12 +403,20 @@ class RustMatrixClient(
override suspend fun getProfile(userId: UserId): Result<MatrixUser> = withContext(sessionDispatcher) {
runCatchingExceptions {
innerClient.getProfile(userId.value).let(UserProfileMapper::map)
innerClient.getProfile(userId.value).map()
}
}
override suspend fun getUserProfile(): Result<MatrixUser> = getProfile(sessionId)
.onSuccess { _userProfile.tryEmit(it) }
.onSuccess { matrixUser ->
_userProfile.emit(matrixUser)
// Also update our session storage
sessionStore.updateUserProfile(
sessionId = sessionId.value,
displayName = matrixUser.displayName,
avatarUrl = matrixUser.avatarUrl,
)
}
override suspend fun searchUsers(searchTerm: String, limit: Long): Result<MatrixSearchUserResults> =
withContext(sessionDispatcher) {
@ -540,6 +565,7 @@ class RustMatrixClient(
sessionDelegate.clearCurrentClient()
innerRoomListService.close()
innerSpaceService.close()
notificationService.close()
encryptionService.close()
innerClient.close()
@ -679,12 +705,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()
@ -705,6 +725,18 @@ class RustMatrixClient(
runCatchingExceptions { innerClient.getMaxMediaUploadSize().toLong() }
}
override suspend fun addRecentEmoji(emoji: String): Result<Unit> = withContext(sessionDispatcher) {
runCatchingExceptions {
innerClient.addRecentEmoji(emoji)
}
}
override suspend fun getRecentEmojis(): Result<List<String>> = withContext(sessionDispatcher) {
runCatchingExceptions {
innerClient.getRecentEmojis().map { it.emoji }
}
}
private suspend fun File.getCacheSize(
includeCryptoDb: Boolean = false,
): Long = withContext(sessionDispatcher) {

View file

@ -7,7 +7,9 @@
package io.element.android.libraries.matrix.impl
import dev.zacsweers.metro.Inject
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.BaseDirectory
import io.element.android.libraries.di.CacheDirectory
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.featureflag.api.FeatureFlagService
@ -28,6 +30,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.ClientBuilder
import org.matrix.rustcomponents.sdk.RequestConfig
import org.matrix.rustcomponents.sdk.Session
import org.matrix.rustcomponents.sdk.SlidingSyncVersion
import org.matrix.rustcomponents.sdk.SlidingSyncVersionBuilder
@ -37,10 +40,10 @@ import uniffi.matrix_sdk_crypto.CollectStrategy
import uniffi.matrix_sdk_crypto.DecryptionSettings
import uniffi.matrix_sdk_crypto.TrustRequirement
import java.io.File
import javax.inject.Inject
class RustMatrixClientFactory @Inject constructor(
private val baseDirectory: File,
@Inject
class RustMatrixClientFactory(
@BaseDirectory private val baseDirectory: File,
@CacheDirectory private val cacheDirectory: File,
@AppCoroutineScope
private val appCoroutineScope: CoroutineScope,
@ -132,7 +135,14 @@ class RustMatrixClientFactory @Inject constructor(
)
)
.enableShareHistoryOnInvite(featureFlagService.isFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite))
.threadsEnabled(featureFlagService.isFeatureEnabled(FeatureFlags.HideThreadedEvents), threadSubscriptions = false)
.threadsEnabled(featureFlagService.isFeatureEnabled(FeatureFlags.Threads), threadSubscriptions = false)
.requestConfig(RequestConfig(
timeout = 30_000uL,
retryLimit = 0u,
// Use default values for the rest
maxConcurrentRequests = null,
maxRetryTime = null,
))
.run {
// Apply sliding sync version settings
when (slidingSyncType) {

View file

@ -7,14 +7,15 @@
package io.element.android.libraries.matrix.impl
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import io.element.android.libraries.matrix.api.SdkMetadata
import org.matrix.rustcomponents.sdk.sdkGitSha
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class RustSdkMetadata @Inject constructor() : SdkMetadata {
@Inject
class RustSdkMetadata : SdkMetadata {
override val sdkGitSha: String
get() = sdkGitSha()
}

View file

@ -14,6 +14,7 @@ import org.matrix.rustcomponents.sdk.OidcException
fun Throwable.mapAuthenticationException(): AuthenticationException {
val message = this.message ?: "Unknown error"
return when (this) {
is AuthenticationException -> this
is ClientBuildException -> when (this) {
is ClientBuildException.Generic -> AuthenticationException.Generic(message)
is ClientBuildException.InvalidServerName -> AuthenticationException.InvalidServerName(message)

View file

@ -7,13 +7,14 @@
package io.element.android.libraries.matrix.impl.auth
import dev.zacsweers.metro.Inject
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.auth.OidcConfig
import io.element.android.libraries.matrix.api.auth.OidcRedirectUrlProvider
import org.matrix.rustcomponents.sdk.OidcConfiguration
import javax.inject.Inject
class OidcConfigurationProvider @Inject constructor(
@Inject
class OidcConfigurationProvider(
private val buildMeta: BuildMeta,
private val oidcRedirectUrlProvider: OidcRedirectUrlProvider,
) {

View file

@ -7,13 +7,15 @@
package io.element.android.libraries.matrix.impl.auth
import com.squareup.anvil.annotations.ContributesBinding
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.extensions.mapFailure
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.auth.AuthenticationException
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
import io.element.android.libraries.matrix.api.auth.OidcDetails
@ -32,11 +34,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
@ -49,11 +49,11 @@ import org.matrix.rustcomponents.sdk.QrLoginProgress
import org.matrix.rustcomponents.sdk.QrLoginProgressListener
import timber.log.Timber
import uniffi.matrix_sdk.OAuthAuthorizationData
import javax.inject.Inject
@ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)
class RustMatrixAuthenticationService @Inject constructor(
@Inject
class RustMatrixAuthenticationService(
private val sessionPathsFactory: SessionPathsFactory,
private val coroutineDispatchers: CoroutineDispatchers,
private val sessionStore: SessionStore,
@ -82,14 +82,6 @@ class RustMatrixAuthenticationService @Inject constructor(
.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)
@ -148,6 +140,8 @@ class RustMatrixAuthenticationService @Inject constructor(
val client = currentClient ?: error("You need to call `setHomeserver()` first")
val currentSessionPaths = sessionPaths ?: error("You need to call `setHomeserver()` first")
client.login(username, password, "Element X Android", null)
// Ensure that the user is not already logged in with the same account
ensureNotAlreadyLoggedIn(client)
val sessionData = client.session()
.toSessionData(
isTokenValid = true,
@ -157,7 +151,7 @@ class RustMatrixAuthenticationService @Inject constructor(
)
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
@ -181,7 +175,7 @@ class RustMatrixAuthenticationService @Inject constructor(
sessionPaths = currentSessionPaths,
)
clear()
sessionStore.storeData(sessionData)
sessionStore.addSession(sessionData)
SessionId(sessionData.userId)
}
}
@ -236,20 +230,22 @@ class RustMatrixAuthenticationService @Inject constructor(
val client = currentClient ?: error("You need to call `setHomeserver()` first")
val currentSessionPaths = sessionPaths ?: error("You need to call `setHomeserver()` first")
client.loginWithOidcCallback(callbackUrl)
// Free the pending data since we won't use it to abort the flow anymore
pendingOAuthAuthorizationData?.close()
pendingOAuthAuthorizationData = null
// Ensure that the user is not already logged in with the same account
ensureNotAlreadyLoggedIn(client)
val sessionData = client.session().toSessionData(
isTokenValid = true,
loginType = LoginType.OIDC,
passphrase = pendingPassphrase,
sessionPaths = currentSessionPaths,
)
// Free the pending data since we won't use it to abort the flow anymore
pendingOAuthAuthorizationData?.close()
pendingOAuthAuthorizationData = null
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
@ -262,6 +258,21 @@ class RustMatrixAuthenticationService @Inject constructor(
}
}
@Throws(AuthenticationException.AccountAlreadyLoggedIn::class)
private suspend fun ensureNotAlreadyLoggedIn(client: Client) {
val newUserId = client.userId()
val accountAlreadyLoggedIn = sessionStore.getAllSessions().any {
it.userId == newUserId
}
if (accountAlreadyLoggedIn) {
// Sign out the client, ignoring any error
runCatchingExceptions {
client.logout()
}
throw AuthenticationException.AccountAlreadyLoggedIn(newUserId)
}
}
override suspend fun loginWithQrCode(qrCodeData: MatrixQrCodeLoginData, progress: (QrCodeLoginStep) -> Unit) =
withContext(coroutineDispatchers.io) {
val sdkQrCodeLoginData = (qrCodeData as SdkQrCodeLoginData).rustQrCodeData
@ -284,7 +295,8 @@ class RustMatrixAuthenticationService @Inject constructor(
oidcConfiguration = oidcConfiguration,
progressListener = progressListener,
)
// Ensure that the user is not already logged in with the same account
ensureNotAlreadyLoggedIn(client)
val sessionData = client.session()
.toSessionData(
isTokenValid = true,
@ -294,7 +306,7 @@ class RustMatrixAuthenticationService @Inject constructor(
)
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

@ -7,16 +7,17 @@
package io.element.android.libraries.matrix.impl.auth.qrlogin
import com.squareup.anvil.annotations.ContributesBinding
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginDataFactory
import org.matrix.rustcomponents.sdk.QrCodeData
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class RustQrCodeLoginDataFactory @Inject constructor() : MatrixQrCodeLoginDataFactory {
@Inject
class RustQrCodeLoginDataFactory : MatrixQrCodeLoginDataFactory {
override fun parseQrCodeData(data: ByteArray): Result<MatrixQrCodeLoginData> {
return runCatchingExceptions { SdkQrCodeLoginData(QrCodeData.fromBytes(data)) }
}

View file

@ -7,15 +7,16 @@
package io.element.android.libraries.matrix.impl.certificates
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import timber.log.Timber
import java.security.KeyStore
import java.security.KeyStoreException
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultUserCertificatesProvider @Inject constructor() : UserCertificatesProvider {
@Inject
class DefaultUserCertificatesProvider : UserCertificatesProvider {
/**
* Get additional user-installed certificates from the `AndroidCAStore` `Keystore`.
*

View file

@ -7,12 +7,13 @@
package io.element.android.libraries.matrix.impl.di
import com.squareup.anvil.annotations.ContributesTo
import dagger.Module
import dagger.Provides
import dev.zacsweers.metro.BindingContainer
import dev.zacsweers.metro.ContributesTo
import dev.zacsweers.metro.Provides
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.media.MediaPreviewService
@ -24,9 +25,14 @@ import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import kotlinx.coroutines.CoroutineScope
@Module
@BindingContainer
@ContributesTo(SessionScope::class)
object SessionMatrixModule {
@Provides
fun providesSessionId(matrixClient: MatrixClient): SessionId {
return matrixClient.sessionId
}
@Provides
fun providesSessionVerificationService(matrixClient: MatrixClient): SessionVerificationService {
return matrixClient.sessionVerificationService()

View file

@ -8,15 +8,16 @@
package io.element.android.libraries.matrix.impl.keys
import android.util.Base64
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import java.security.SecureRandom
import javax.inject.Inject
private const val SECRET_SIZE = 256
@ContributesBinding(AppScope::class)
class DefaultPassphraseGenerator @Inject constructor() : PassphraseGenerator {
@Inject
class DefaultPassphraseGenerator : PassphraseGenerator {
override fun generatePassphrase(): String? {
val key = ByteArray(size = SECRET_SIZE)
SecureRandom().nextBytes(key)

View file

@ -34,6 +34,11 @@ internal fun Session.toSessionData(
passphrase = passphrase,
sessionPath = sessionPaths.fileDirectory.absolutePath,
cachePath = sessionPaths.cacheDirectory.absolutePath,
// Note: position and lastUsageIndex will be set by the SessionStore when adding the session
position = 0,
lastUsageIndex = 0,
userDisplayName = null,
userAvatarUrl = null,
)
internal fun ExternalSession.toSessionData(
@ -55,4 +60,8 @@ internal fun ExternalSession.toSessionData(
passphrase = passphrase,
sessionPath = sessionPaths.fileDirectory.absolutePath,
cachePath = sessionPaths.cacheDirectory.absolutePath,
position = 0,
lastUsageIndex = 0,
userDisplayName = null,
userAvatarUrl = null,
)

View file

@ -5,17 +5,14 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.usersearch
package io.element.android.libraries.matrix.impl.mapper
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
import org.matrix.rustcomponents.sdk.UserProfile
object UserProfileMapper {
fun map(userProfile: UserProfile): MatrixUser =
MatrixUser(
userId = UserId(userProfile.userId),
displayName = userProfile.displayName,
avatarUrl = userProfile.avatarUrl,
)
}
fun UserProfile.map() = MatrixUser(
userId = UserId(userId),
displayName = displayName,
avatarUrl = avatarUrl,
)

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

@ -7,13 +7,15 @@
package io.element.android.libraries.matrix.impl.paths
import dev.zacsweers.metro.Inject
import io.element.android.libraries.di.BaseDirectory
import io.element.android.libraries.di.CacheDirectory
import java.io.File
import java.util.UUID
import javax.inject.Inject
class SessionPathsFactory @Inject constructor(
private val baseDirectory: File,
@Inject
class SessionPathsFactory(
@BaseDirectory private val baseDirectory: File,
@CacheDirectory private val cacheDirectory: File,
) {
fun create(): SessionPaths {

View file

@ -9,18 +9,19 @@ package io.element.android.libraries.matrix.impl.permalink
import android.net.Uri
import androidx.core.net.toUri
import com.squareup.anvil.annotations.ContributesBinding
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import io.element.android.appconfig.MatrixConfiguration
import io.element.android.libraries.core.extensions.replacePrefix
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.permalink.MatrixToConverter
import javax.inject.Inject
/**
* Mapping of an input URI to a matrix.to compliant URI.
*/
@ContributesBinding(AppScope::class)
class DefaultMatrixToConverter @Inject constructor() : MatrixToConverter {
@Inject
class DefaultMatrixToConverter : MatrixToConverter {
/**
* Try to convert a URL from an element web instance or from a client permalink to a matrix.to url.
* To be successfully converted, URL path should contain one of the [SUPPORTED_PATHS].

View file

@ -7,9 +7,10 @@
package io.element.android.libraries.matrix.impl.permalink
import com.squareup.anvil.annotations.ContributesBinding
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.core.MatrixPatterns
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.UserId
@ -17,10 +18,10 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilderError
import org.matrix.rustcomponents.sdk.matrixToRoomAliasPermalink
import org.matrix.rustcomponents.sdk.matrixToUserPermalink
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultPermalinkBuilder @Inject constructor() : PermalinkBuilder {
@Inject
class DefaultPermalinkBuilder : PermalinkBuilder {
override fun permalinkForUser(userId: UserId): Result<String> {
if (!MatrixPatterns.isUserId(userId.value)) {
return Result.failure(PermalinkBuilderError.InvalidData)

View file

@ -8,9 +8,10 @@
package io.element.android.libraries.matrix.impl.permalink
import androidx.core.net.toUri
import com.squareup.anvil.annotations.ContributesBinding
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
@ -22,7 +23,6 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import kotlinx.collections.immutable.toImmutableList
import org.matrix.rustcomponents.sdk.MatrixId
import org.matrix.rustcomponents.sdk.parseMatrixEntityFrom
import javax.inject.Inject
/**
* This class turns a uri to a [PermalinkData].
@ -32,7 +32,8 @@ import javax.inject.Inject
* or matrix: permalinks (e.g. matrix:u/chagai95:matrix.org)
*/
@ContributesBinding(AppScope::class)
class DefaultPermalinkParser @Inject constructor(
@Inject
class DefaultPermalinkParser(
private val matrixToConverter: MatrixToConverter
) : PermalinkParser {
/**

View file

@ -7,16 +7,17 @@
package io.element.android.libraries.matrix.impl.platform
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import io.element.android.libraries.matrix.api.platform.InitPlatformService
import io.element.android.libraries.matrix.api.tracing.TracingConfiguration
import io.element.android.libraries.matrix.impl.tracing.map
import org.matrix.rustcomponents.sdk.initPlatform
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class RustInitPlatformService @Inject constructor() : InitPlatformService {
@Inject
class RustInitPlatformService : InitPlatformService {
override fun init(tracingConfiguration: TracingConfiguration) {
initPlatform(
config = tracingConfiguration.map(),

View file

@ -11,11 +11,11 @@ import android.content.Context
import android.net.ConnectivityManager
import android.provider.Settings
import androidx.core.content.getSystemService
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import io.element.android.libraries.di.annotations.ApplicationContext
import timber.log.Timber
import javax.inject.Inject
/**
* Provides the proxy settings from the system.
@ -29,7 +29,8 @@ import javax.inject.Inject
* ```
*/
@ContributesBinding(AppScope::class)
class DefaultProxyProvider @Inject constructor(
@Inject
class DefaultProxyProvider(
@ApplicationContext
private val context: Context
) : ProxyProvider {

View file

@ -156,7 +156,7 @@ class JoinedRustRoom(
override suspend fun createTimeline(
createTimelineParams: CreateTimelineParams,
): Result<Timeline> = withContext(roomDispatcher) {
val hideThreadedEvents = featureFlagService.isFeatureEnabled(FeatureFlags.HideThreadedEvents)
val hideThreadedEvents = featureFlagService.isFeatureEnabled(FeatureFlags.Threads)
val focus = when (createTimelineParams) {
is CreateTimelineParams.PinnedOnly -> TimelineFocus.PinnedEvents(
maxEventsToLoad = 100u,

View file

@ -11,53 +11,55 @@ import io.element.android.libraries.matrix.api.room.MessageEventType
import org.matrix.rustcomponents.sdk.MessageLikeEventType
fun MessageEventType.map(): MessageLikeEventType = when (this) {
MessageEventType.CALL_ANSWER -> MessageLikeEventType.CALL_ANSWER
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.KEY_VERIFICATION_READY -> MessageLikeEventType.KEY_VERIFICATION_READY
MessageEventType.KEY_VERIFICATION_START -> MessageLikeEventType.KEY_VERIFICATION_START
MessageEventType.KEY_VERIFICATION_CANCEL -> MessageLikeEventType.KEY_VERIFICATION_CANCEL
MessageEventType.KEY_VERIFICATION_ACCEPT -> MessageLikeEventType.KEY_VERIFICATION_ACCEPT
MessageEventType.KEY_VERIFICATION_KEY -> MessageLikeEventType.KEY_VERIFICATION_KEY
MessageEventType.KEY_VERIFICATION_MAC -> MessageLikeEventType.KEY_VERIFICATION_MAC
MessageEventType.KEY_VERIFICATION_DONE -> MessageLikeEventType.KEY_VERIFICATION_DONE
MessageEventType.REACTION -> MessageLikeEventType.REACTION
MessageEventType.ROOM_ENCRYPTED -> MessageLikeEventType.ROOM_ENCRYPTED
MessageEventType.ROOM_MESSAGE -> MessageLikeEventType.ROOM_MESSAGE
MessageEventType.ROOM_REDACTION -> MessageLikeEventType.ROOM_REDACTION
MessageEventType.STICKER -> MessageLikeEventType.STICKER
MessageEventType.POLL_END -> MessageLikeEventType.POLL_END
MessageEventType.POLL_RESPONSE -> MessageLikeEventType.POLL_RESPONSE
MessageEventType.POLL_START -> MessageLikeEventType.POLL_START
MessageEventType.UNSTABLE_POLL_END -> MessageLikeEventType.UNSTABLE_POLL_END
MessageEventType.UNSTABLE_POLL_RESPONSE -> MessageLikeEventType.UNSTABLE_POLL_RESPONSE
MessageEventType.UNSTABLE_POLL_START -> MessageLikeEventType.UNSTABLE_POLL_START
MessageEventType.CallAnswer -> MessageLikeEventType.CallAnswer
MessageEventType.CallInvite -> MessageLikeEventType.CallInvite
MessageEventType.CallHangup -> MessageLikeEventType.CallHangup
MessageEventType.CallCandidates -> MessageLikeEventType.CallCandidates
MessageEventType.RtcNotification -> MessageLikeEventType.RtcNotification
MessageEventType.KeyVerificationReady -> MessageLikeEventType.KeyVerificationReady
MessageEventType.KeyVerificationStart -> MessageLikeEventType.KeyVerificationStart
MessageEventType.KeyVerificationCancel -> MessageLikeEventType.KeyVerificationCancel
MessageEventType.KeyVerificationAccept -> MessageLikeEventType.KeyVerificationAccept
MessageEventType.KeyVerificationKey -> MessageLikeEventType.KeyVerificationKey
MessageEventType.KeyVerificationMac -> MessageLikeEventType.KeyVerificationMac
MessageEventType.KeyVerificationDone -> MessageLikeEventType.KeyVerificationDone
MessageEventType.Reaction -> MessageLikeEventType.Reaction
MessageEventType.RoomEncrypted -> MessageLikeEventType.RoomEncrypted
MessageEventType.RoomMessage -> MessageLikeEventType.RoomMessage
MessageEventType.RoomRedaction -> MessageLikeEventType.RoomRedaction
MessageEventType.Sticker -> MessageLikeEventType.Sticker
MessageEventType.PollEnd -> MessageLikeEventType.PollEnd
MessageEventType.PollResponse -> MessageLikeEventType.PollResponse
MessageEventType.PollStart -> MessageLikeEventType.PollStart
MessageEventType.UnstablePollEnd -> MessageLikeEventType.UnstablePollEnd
MessageEventType.UnstablePollResponse -> MessageLikeEventType.UnstablePollResponse
MessageEventType.UnstablePollStart -> MessageLikeEventType.UnstablePollStart
is MessageEventType.Other -> MessageLikeEventType.Other(type)
}
fun MessageLikeEventType.map(): MessageEventType = when (this) {
MessageLikeEventType.CALL_ANSWER -> MessageEventType.CALL_ANSWER
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.KEY_VERIFICATION_READY -> MessageEventType.KEY_VERIFICATION_READY
MessageLikeEventType.KEY_VERIFICATION_START -> MessageEventType.KEY_VERIFICATION_START
MessageLikeEventType.KEY_VERIFICATION_CANCEL -> MessageEventType.KEY_VERIFICATION_CANCEL
MessageLikeEventType.KEY_VERIFICATION_ACCEPT -> MessageEventType.KEY_VERIFICATION_ACCEPT
MessageLikeEventType.KEY_VERIFICATION_KEY -> MessageEventType.KEY_VERIFICATION_KEY
MessageLikeEventType.KEY_VERIFICATION_MAC -> MessageEventType.KEY_VERIFICATION_MAC
MessageLikeEventType.KEY_VERIFICATION_DONE -> MessageEventType.KEY_VERIFICATION_DONE
MessageLikeEventType.REACTION -> MessageEventType.REACTION
MessageLikeEventType.ROOM_ENCRYPTED -> MessageEventType.ROOM_ENCRYPTED
MessageLikeEventType.ROOM_MESSAGE -> MessageEventType.ROOM_MESSAGE
MessageLikeEventType.ROOM_REDACTION -> MessageEventType.ROOM_REDACTION
MessageLikeEventType.STICKER -> MessageEventType.STICKER
MessageLikeEventType.POLL_END -> MessageEventType.POLL_END
MessageLikeEventType.POLL_RESPONSE -> MessageEventType.POLL_RESPONSE
MessageLikeEventType.POLL_START -> MessageEventType.POLL_START
MessageLikeEventType.UNSTABLE_POLL_END -> MessageEventType.UNSTABLE_POLL_END
MessageLikeEventType.UNSTABLE_POLL_RESPONSE -> MessageEventType.UNSTABLE_POLL_RESPONSE
MessageLikeEventType.UNSTABLE_POLL_START -> MessageEventType.UNSTABLE_POLL_START
MessageLikeEventType.CallAnswer -> MessageEventType.CallAnswer
MessageLikeEventType.CallInvite -> MessageEventType.CallInvite
MessageLikeEventType.CallHangup -> MessageEventType.CallHangup
MessageLikeEventType.CallCandidates -> MessageEventType.CallCandidates
MessageLikeEventType.RtcNotification -> MessageEventType.RtcNotification
MessageLikeEventType.KeyVerificationReady -> MessageEventType.KeyVerificationReady
MessageLikeEventType.KeyVerificationStart -> MessageEventType.KeyVerificationStart
MessageLikeEventType.KeyVerificationCancel -> MessageEventType.KeyVerificationCancel
MessageLikeEventType.KeyVerificationAccept -> MessageEventType.KeyVerificationAccept
MessageLikeEventType.KeyVerificationKey -> MessageEventType.KeyVerificationKey
MessageLikeEventType.KeyVerificationMac -> MessageEventType.KeyVerificationMac
MessageLikeEventType.KeyVerificationDone -> MessageEventType.KeyVerificationDone
MessageLikeEventType.Reaction -> MessageEventType.Reaction
MessageLikeEventType.RoomEncrypted -> MessageEventType.RoomEncrypted
MessageLikeEventType.RoomMessage -> MessageEventType.RoomMessage
MessageLikeEventType.RoomRedaction -> MessageEventType.RoomRedaction
MessageLikeEventType.Sticker -> MessageEventType.Sticker
MessageLikeEventType.PollEnd -> MessageEventType.PollEnd
MessageLikeEventType.PollResponse -> MessageEventType.PollResponse
MessageLikeEventType.PollStart -> MessageEventType.PollStart
MessageLikeEventType.UnstablePollEnd -> MessageEventType.UnstablePollEnd
MessageLikeEventType.UnstablePollResponse -> MessageEventType.UnstablePollResponse
MessageLikeEventType.UnstablePollStart -> MessageEventType.UnstablePollStart
is MessageLikeEventType.Other -> MessageEventType.Other(v1)
}

View file

@ -14,7 +14,6 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.ForwardEventException
import io.element.android.libraries.matrix.impl.roomlist.roomOrNull
import io.element.android.libraries.matrix.impl.timeline.runWithTimelineListenerRegistered
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.withTimeout
import org.matrix.rustcomponents.sdk.MsgLikeKind
import org.matrix.rustcomponents.sdk.RoomListService
@ -63,9 +62,6 @@ class RoomContentForwarder(
}
}.onFailure {
failedForwardingTo.add(RoomId(room.id()))
if (it is CancellationException) {
throw it
}
}
}

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
@ -155,7 +157,11 @@ class RustBaseRoom(
runCatchingExceptions {
innerRoom.leave()
}.onSuccess {
roomMembershipObserver.notifyUserLeftRoom(roomId, membershipBeforeLeft)
roomMembershipObserver.notifyUserLeftRoom(
roomId = roomId,
isSpace = roomInfoFlow.value.isSpace,
membershipBeforeLeft = membershipBeforeLeft,
)
}
}
@ -300,4 +306,28 @@ 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))
}
})
}
}
override suspend fun threadRootIdForEvent(eventId: EventId): Result<ThreadId?> = withContext(roomDispatcher) {
runCatchingExceptions {
innerRoom.loadOrFetchEvent(eventId.value).use {
it.threadRootEventId()?.let(::ThreadId)
}
}
}
}

View file

@ -108,7 +108,7 @@ class RustRoomFactory(
val sdkRoom = awaitRoomInRoomList(roomId) ?: return@withContext null
if (sdkRoom.membership() == Membership.JOINED) {
val hideThreadedEvents = featureFlagService.isFeatureEnabled(FeatureFlags.HideThreadedEvents)
val hideThreadedEvents = featureFlagService.isFeatureEnabled(FeatureFlags.Threads)
// Init the live timeline in the SDK from the Room
val timeline = sdkRoom.timelineWithConfiguration(
TimelineConfiguration(

View file

@ -7,19 +7,20 @@
package io.element.android.libraries.matrix.impl.room
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import io.element.android.libraries.matrix.api.room.StateEventType
import org.matrix.rustcomponents.sdk.FilterTimelineEventType
import org.matrix.rustcomponents.sdk.TimelineEventTypeFilter
import javax.inject.Inject
interface TimelineEventTypeFilterFactory {
fun create(listStateEventType: List<StateEventType>): TimelineEventTypeFilter
}
@ContributesBinding(AppScope::class)
class RustTimelineEventTypeFilterFactory @Inject constructor() : TimelineEventTypeFilterFactory {
@Inject
class RustTimelineEventTypeFilterFactory : TimelineEventTypeFilterFactory {
override fun create(listStateEventType: List<StateEventType>): TimelineEventTypeFilter {
return TimelineEventTypeFilter.exclude(
listStateEventType.map { stateEventType ->

View file

@ -7,14 +7,15 @@
package io.element.android.libraries.matrix.impl.room.alias
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultRoomAliasHelper @Inject constructor() : RoomAliasHelper {
@Inject
class DefaultRoomAliasHelper : RoomAliasHelper {
override fun roomAliasNameFromRoomDisplayName(name: String): String {
return org.matrix.rustcomponents.sdk.roomAliasNameFromRoomDisplayName(name)
}

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

@ -7,7 +7,8 @@
package io.element.android.libraries.matrix.impl.room.join
import com.squareup.anvil.annotations.ContributesBinding
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.libraries.core.extensions.mapFailure
import io.element.android.libraries.di.SessionScope
@ -18,10 +19,10 @@ import io.element.android.libraries.matrix.api.exception.ErrorKind
import io.element.android.libraries.matrix.api.room.join.JoinRoom
import io.element.android.libraries.matrix.impl.analytics.toAnalyticsJoinedRoom
import io.element.android.services.analytics.api.AnalyticsService
import javax.inject.Inject
@ContributesBinding(SessionScope::class)
class DefaultJoinRoom @Inject constructor(
@Inject
class DefaultJoinRoom(
private val client: MatrixClient,
private val analyticsService: AnalyticsService,
) : JoinRoom {

View file

@ -8,6 +8,7 @@
package io.element.android.libraries.matrix.impl.room.join
import io.element.android.libraries.matrix.api.room.join.JoinRule
import kotlinx.collections.immutable.toPersistentList
import org.matrix.rustcomponents.sdk.JoinRule as RustJoinRule
fun RustJoinRule.map(): JoinRule {
@ -16,9 +17,9 @@ fun RustJoinRule.map(): JoinRule {
RustJoinRule.Private -> JoinRule.Private
RustJoinRule.Knock -> JoinRule.Knock
RustJoinRule.Invite -> JoinRule.Invite
is RustJoinRule.Restricted -> JoinRule.Restricted(rules.map { it.map() })
is RustJoinRule.Restricted -> JoinRule.Restricted(rules.map { it.map() }.toPersistentList())
is RustJoinRule.Custom -> JoinRule.Custom(repr)
is RustJoinRule.KnockRestricted -> JoinRule.KnockRestricted(rules.map { it.map() })
is RustJoinRule.KnockRestricted -> JoinRule.KnockRestricted(rules.map { it.map() }.toPersistentList())
}
}

View file

@ -79,35 +79,35 @@ internal class RoomMemberListFetcher(
Timber.i("Loading cached members for room $roomId")
try {
// Send current member list with pending state to notify the UI that we are loading new members
emit(pendingWithCurrentMembers())
value = pendingWithCurrentMembers()
val members = parseAndEmitMembers(room.membersNoSync())
val newState = if (asPendingState) {
RoomMembersState.Pending(prevRoomMembers = members)
} else {
RoomMembersState.Ready(members)
}
emit(newState)
value = newState
} catch (exception: CancellationException) {
Timber.d("Cancelled loading cached members for room $roomId")
throw exception
} catch (exception: Exception) {
Timber.e(exception, "Failed to load cached members for room $roomId")
emit(RoomMembersState.Error(exception, _membersFlow.value.roomMembers()?.toImmutableList()))
value = RoomMembersState.Error(exception, _membersFlow.value.roomMembers()?.toImmutableList())
}
}
private suspend fun MutableStateFlow<RoomMembersState>.fetchRemoteRoomMembers() {
try {
// Send current member list with pending state to notify the UI that we are loading new members
emit(pendingWithCurrentMembers())
value = pendingWithCurrentMembers()
// Start loading new members
emit(RoomMembersState.Ready(parseAndEmitMembers(room.members())))
value = RoomMembersState.Ready(parseAndEmitMembers(room.members()))
} catch (exception: CancellationException) {
Timber.d("Cancelled loading updated members for room $roomId")
throw exception
} catch (exception: Exception) {
Timber.e(exception, "Failed to load updated members for room $roomId")
emit(RoomMembersState.Error(exception, _membersFlow.value.roomMembers()?.toImmutableList()))
value = RoomMembersState.Error(exception, _membersFlow.value.roomMembers()?.toImmutableList())
}
}

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

@ -33,6 +33,12 @@ class RoomSummaryListProcessor(
updates.forEach { update ->
applyUpdate(update)
}
// TODO remove once https://github.com/element-hq/element-x-android/issues/5031 has been confirmed as fixed
val duplicates = groupingBy { it.roomId }.eachCount().filter { it.value > 1 }
if (duplicates.isNotEmpty()) {
Timber.e("Found duplicates in room summaries after a list update from the SDK: $duplicates. Updates: $updates")
}
}
}

View file

@ -7,14 +7,15 @@
package io.element.android.libraries.matrix.impl.server
import com.squareup.anvil.annotations.ContributesBinding
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.server.UserServerResolver
import javax.inject.Inject
@ContributesBinding(SessionScope::class)
class DefaultUserServerResolver @Inject constructor(
@Inject
class DefaultUserServerResolver(
private val matrixClient: MatrixClient,
) : UserServerResolver {
override fun resolve(): String {

View file

@ -0,0 +1,97 @@
/*
* 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.spaces
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber
import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState
import java.util.Optional
import org.matrix.rustcomponents.sdk.SpaceRoomList as InnerSpaceRoomList
class RustSpaceRoomList(
override val roomId: RoomId,
private val innerProvider: suspend () -> InnerSpaceRoomList,
private val coroutineScope: CoroutineScope,
spaceRoomMapper: SpaceRoomMapper,
) : SpaceRoomList {
private val innerCompletable = CompletableDeferred<InnerSpaceRoomList>()
override val currentSpaceFlow = MutableStateFlow<Optional<SpaceRoom>>(Optional.empty())
override val spaceRoomsFlow = MutableSharedFlow<List<SpaceRoom>>(replay = 1, extraBufferCapacity = Int.MAX_VALUE)
override val paginationStatusFlow: MutableStateFlow<SpaceRoomList.PaginationStatus> =
MutableStateFlow(SpaceRoomList.PaginationStatus.Idle(hasMoreToLoad = false))
private val spaceListUpdateProcessor = SpaceListUpdateProcessor(
spaceRoomsFlow = spaceRoomsFlow,
mapper = spaceRoomMapper
)
init {
coroutineScope.launch {
val inner = innerProvider()
innerCompletable.complete(inner)
inner.paginationStateFlow()
.onEach { paginationStatus ->
paginationStatusFlow.emit(paginationStatus.into())
}
.launchIn(this)
inner.spaceListUpdateFlow()
.onEach { updates ->
spaceListUpdateProcessor.postUpdates(updates)
}
.launchIn(this)
inner.spaceUpdateFlow()
.map { space -> space.map(spaceRoomMapper::map) }
.onEach { space ->
currentSpaceFlow.emit(space)
}
.launchIn(this)
}
}
override suspend fun paginate(): Result<Unit> {
return runCatchingExceptions {
innerCompletable.await().paginate()
}
}
@OptIn(ExperimentalCoroutinesApi::class)
override fun destroy() {
Timber.d("Destroying SpaceRoomList $roomId")
coroutineScope.cancel()
try {
innerCompletable.getCompleted().destroy()
} catch (_: Exception) {
// Ignore, we just want to make sure it's completed
}
}
private fun SpaceRoomListPaginationState.into(): SpaceRoomList.PaginationStatus {
return when (this) {
is SpaceRoomListPaginationState.Idle -> SpaceRoomList.PaginationStatus.Idle(hasMoreToLoad = !endReached)
SpaceRoomListPaginationState.Loading -> SpaceRoomList.PaginationStatus.Loading
}
}
}

View file

@ -0,0 +1,92 @@
/*
* 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.spaces
import io.element.android.libraries.core.coroutine.childScope
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
import io.element.android.libraries.matrix.api.spaces.SpaceService
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.SpaceListUpdate
import org.matrix.rustcomponents.sdk.SpaceServiceInterface
import org.matrix.rustcomponents.sdk.SpaceServiceJoinedSpacesListener
import timber.log.Timber
import org.matrix.rustcomponents.sdk.SpaceService as ClientSpaceService
class RustSpaceService(
private val innerSpaceService: ClientSpaceService,
private val sessionCoroutineScope: CoroutineScope,
private val sessionDispatcher: CoroutineDispatcher,
) : SpaceService {
private val spaceRoomMapper = SpaceRoomMapper()
override val spaceRoomsFlow = MutableSharedFlow<List<SpaceRoom>>(replay = 1, extraBufferCapacity = 1)
private val spaceListUpdateProcessor = SpaceListUpdateProcessor(
spaceRoomsFlow = spaceRoomsFlow,
mapper = spaceRoomMapper
)
override suspend fun joinedSpaces(): Result<List<SpaceRoom>> = withContext(sessionDispatcher) {
runCatchingExceptions {
innerSpaceService.joinedSpaces()
.map {
it.let(spaceRoomMapper::map)
}
}
}
override fun spaceRoomList(id: RoomId): SpaceRoomList {
val childCoroutineScope = sessionCoroutineScope.childScope(sessionDispatcher, "SpaceRoomListScope-$this")
return RustSpaceRoomList(
roomId = id,
innerProvider = { innerSpaceService.spaceRoomList(id.value) },
coroutineScope = childCoroutineScope,
spaceRoomMapper = spaceRoomMapper,
)
}
init {
innerSpaceService
.spaceListUpdate()
.onEach { updates ->
spaceListUpdateProcessor.postUpdates(updates)
}
.launchIn(sessionCoroutineScope)
}
}
internal fun SpaceServiceInterface.spaceListUpdate(): Flow<List<SpaceListUpdate>> =
callbackFlow {
val listener = object : SpaceServiceJoinedSpacesListener {
override fun onUpdate(roomUpdates: List<SpaceListUpdate>) {
trySendBlocking(roomUpdates)
}
}
Timber.d("Open spaceDiffFlow for SpaceServiceInterface ${this@spaceListUpdate}")
val taskHandle = subscribeToJoinedSpaces(listener)
awaitClose {
Timber.d("Close spaceDiffFlow for SpaceServiceInterface ${this@spaceListUpdate}")
taskHandle.cancelAndDestroy()
}
}.catch {
Timber.d(it, "spaceDiffFlow() failed")
}.buffer(Channel.UNLIMITED)

View file

@ -0,0 +1,84 @@
/*
* Copyright 2023, 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.spaces
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.matrix.rustcomponents.sdk.SpaceListUpdate
import timber.log.Timber
internal class SpaceListUpdateProcessor(
private val spaceRoomsFlow: MutableSharedFlow<List<SpaceRoom>>,
private val mapper: SpaceRoomMapper,
) {
private val mutex = Mutex()
suspend fun postUpdates(updates: List<SpaceListUpdate>) {
Timber.v("Update space rooms from postUpdates (with ${updates.size} items) on ${Thread.currentThread()}")
updateSpaceRooms {
updates.forEach { update -> applyUpdate(update) }
}
}
private suspend fun updateSpaceRooms(block: MutableList<SpaceRoom>.() -> Unit) =
mutex.withLock {
val spaceRooms = if (spaceRoomsFlow.replayCache.isNotEmpty()) {
spaceRoomsFlow.first().toMutableList()
} else {
mutableListOf()
}
block(spaceRooms)
spaceRoomsFlow.emit(spaceRooms)
}
private fun MutableList<SpaceRoom>.applyUpdate(update: SpaceListUpdate) {
when (update) {
is SpaceListUpdate.Append -> {
val newSpaces = update.values.map(mapper::map)
addAll(newSpaces)
}
SpaceListUpdate.Clear -> clear()
is SpaceListUpdate.Insert -> {
val newSpace = mapper.map(update.value)
add(update.index.toInt(), newSpace)
}
SpaceListUpdate.PopBack -> {
removeAt(lastIndex)
}
SpaceListUpdate.PopFront -> {
removeAt(0)
}
is SpaceListUpdate.PushBack -> {
val newSpace = mapper.map(update.value)
add(newSpace)
}
is SpaceListUpdate.PushFront -> {
val newSpace = mapper.map(update.value)
add(0, newSpace)
}
is SpaceListUpdate.Remove -> {
removeAt(update.index.toInt())
}
is SpaceListUpdate.Reset -> {
clear()
val newSpaces = update.values.map(mapper::map)
addAll(newSpaces)
}
is SpaceListUpdate.Set -> {
val newSpace = mapper.map(update.value)
this[update.index.toInt()] = newSpace
}
is SpaceListUpdate.Truncate -> {
subList(update.length.toInt(), size).clear()
}
}
}
}

View file

@ -0,0 +1,78 @@
/*
* 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.spaces
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.catch
import org.matrix.rustcomponents.sdk.SpaceListUpdate
import org.matrix.rustcomponents.sdk.SpaceRoom
import org.matrix.rustcomponents.sdk.SpaceRoomListEntriesListener
import org.matrix.rustcomponents.sdk.SpaceRoomListInterface
import org.matrix.rustcomponents.sdk.SpaceRoomListPaginationStateListener
import org.matrix.rustcomponents.sdk.SpaceRoomListSpaceListener
import timber.log.Timber
import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState
import java.util.Optional
internal fun SpaceRoomListInterface.paginationStateFlow(): Flow<SpaceRoomListPaginationState> = callbackFlow {
val listener = object : SpaceRoomListPaginationStateListener {
override fun onUpdate(paginationState: SpaceRoomListPaginationState) {
trySend(paginationState)
}
}
// Send the initial value
trySend(paginationState())
// Then subscribe to updates
val result = subscribeToPaginationStateUpdates(listener)
awaitClose {
result.cancelAndDestroy()
}
}.catch {
Timber.d(it, "paginationStateFlow() failed")
}.buffer(Channel.UNLIMITED)
internal fun SpaceRoomListInterface.spaceListUpdateFlow(): Flow<List<SpaceListUpdate>> =
callbackFlow {
val listener = object : SpaceRoomListEntriesListener {
override fun onUpdate(rooms: List<SpaceListUpdate>) {
trySendBlocking(rooms)
}
}
Timber.d("Open spaceListUpdateFlow for SpaceRoomListInterface ${this@spaceListUpdateFlow}")
val taskHandle = subscribeToRoomUpdate(listener)
awaitClose {
Timber.d("Close spaceListUpdateFlow for SpaceRoomListInterface ${this@spaceListUpdateFlow}")
taskHandle.cancelAndDestroy()
}
}.catch {
Timber.d(it, "spaceListUpdateFlow() failed")
}.buffer(Channel.UNLIMITED)
internal fun SpaceRoomListInterface.spaceUpdateFlow(): Flow<Optional<SpaceRoom>> =
callbackFlow {
val listener = object : SpaceRoomListSpaceListener {
override fun onUpdate(space: SpaceRoom?) {
trySendBlocking(Optional.ofNullable(space))
}
}
Timber.d("Open spaceUpdateFlow for SpaceRoomListInterface ${this@spaceUpdateFlow}")
trySendBlocking(Optional.ofNullable(space()))
val taskHandle = subscribeToSpaceUpdates(listener)
awaitClose {
Timber.d("Close spaceUpdateFlow for SpaceRoomListInterface ${this@spaceUpdateFlow}")
taskHandle.cancelAndDestroy()
}
}.catch {
Timber.d(it, "spaceUpdateFlow() failed")
}.buffer(Channel.UNLIMITED)

View file

@ -0,0 +1,37 @@
/*
* 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.spaces
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.matrix.impl.room.join.map
import io.element.android.libraries.matrix.impl.room.map
import org.matrix.rustcomponents.sdk.SpaceRoom as RustSpaceRoom
class SpaceRoomMapper {
fun map(spaceRoom: RustSpaceRoom): SpaceRoom {
return SpaceRoom(
avatarUrl = spaceRoom.avatarUrl,
canonicalAlias = spaceRoom.canonicalAlias?.let(::RoomAlias),
childrenCount = spaceRoom.childrenCount.toInt(),
guestCanJoin = spaceRoom.guestCanJoin,
heroes = spaceRoom.heroes.orEmpty().map { it.map() },
joinRule = spaceRoom.joinRule?.map(),
name = spaceRoom.name,
numJoinedMembers = spaceRoom.numJoinedMembers.toInt(),
roomId = RoomId(spaceRoom.roomId),
roomType = spaceRoom.roomType.map(),
state = spaceRoom.state?.map(),
topic = spaceRoom.topic,
worldReadable = spaceRoom.worldReadable.orFalse(),
via = spaceRoom.via,
)
}
}

View file

@ -7,6 +7,7 @@
package io.element.android.libraries.matrix.impl.timeline
import io.element.android.libraries.androidutils.hash.hash
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.RoomId
@ -122,7 +123,7 @@ class RustTimeline(
)
override val forwardPaginationStatus = MutableStateFlow(
Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode !is Timeline.Mode.FocusedOnEvent)
Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode is Timeline.Mode.FocusedOnEvent)
)
init {
@ -220,7 +221,6 @@ class RustTimeline(
items = items,
hasMoreToLoadBackward = backwardPaginationStatus.hasMoreToLoad,
hasMoreToLoadForward = forwardPaginationStatus.hasMoreToLoad,
timelineMode = mode,
)
}
.let { items ->
@ -338,6 +338,7 @@ class RustTimeline(
formattedCaption: String?,
inReplyToEventId: EventId?,
): Result<MediaUploadHandler> {
Timber.d("Sending image ${file.path.hash()}")
return sendAttachment(listOfNotNull(file, thumbnailFile)) {
inner.sendImage(
params = UploadParameters(
@ -363,6 +364,7 @@ class RustTimeline(
formattedCaption: String?,
inReplyToEventId: EventId?,
): Result<MediaUploadHandler> {
Timber.d("Sending video ${file.path.hash()}")
return sendAttachment(listOfNotNull(file, thumbnailFile)) {
inner.sendVideo(
params = UploadParameters(
@ -387,6 +389,7 @@ class RustTimeline(
formattedCaption: String?,
inReplyToEventId: EventId?,
): Result<MediaUploadHandler> {
Timber.d("Sending audio ${file.path.hash()}")
return sendAttachment(listOf(file)) {
inner.sendAudio(
params = UploadParameters(
@ -410,6 +413,7 @@ class RustTimeline(
formattedCaption: String?,
inReplyToEventId: EventId?,
): Result<MediaUploadHandler> {
Timber.d("Sending file ${file.path.hash()}")
return sendAttachment(listOf(file)) {
inner.sendFile(
params = UploadParameters(
@ -426,7 +430,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

@ -38,7 +38,7 @@ private const val MSG_TYPE_GALLERY_UNSTABLE = "dm.filament.gallery"
class EventMessageMapper {
private val inReplyToMapper by lazy { InReplyToMapper(TimelineEventContentMapper()) }
fun map(message: MsgLikeKind.Message, inReplyTo: InReplyToDetails?, threadInfo: EventThreadInfo): MessageContent = message.use {
fun map(message: MsgLikeKind.Message, inReplyTo: InReplyToDetails?, threadInfo: EventThreadInfo?): MessageContent = message.use {
val type = it.content.msgType.use(this::mapMessageType)
val inReplyToEvent: InReplyTo? = inReplyTo?.use(inReplyToMapper::map)
MessageContent(

View file

@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershi
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
import io.element.android.libraries.matrix.api.timeline.item.event.UtdCause
import io.element.android.libraries.matrix.impl.media.map
import io.element.android.libraries.matrix.impl.poll.map
@ -79,7 +80,7 @@ class TimelineEventContentMapper(
content = map(latestEvent.content),
senderId = UserId(latestEvent.sender),
senderProfile = latestEvent.senderProfile.map(),
timestamp = latestEvent.timestamp.toLong()
timestamp = latestEvent.timestamp.toLong(),
)
)
}
@ -89,10 +90,12 @@ class TimelineEventContentMapper(
numberOfReplies = numberOfReplies,
)
}
val threadInfo = EventThreadInfo(
threadRootId = it.content.threadRoot?.let(::ThreadId),
threadSummary = threadSummary,
)
val threadRootId = it.content.threadRoot?.let(::ThreadId)
val threadInfo = when {
threadSummary != null -> EventThreadInfo.ThreadRoot(threadSummary)
threadRootId != null -> EventThreadInfo.ThreadResponse(threadRootId)
else -> null
}
eventMessageMapper.map(kind, inReplyTo, threadInfo)
}
is MsgLikeKind.Redacted -> {
@ -124,6 +127,7 @@ class TimelineEventContentMapper(
source = kind.source.map(),
)
}
is MsgLikeKind.Other -> UnknownContent
}
}
is TimelineItemContent.ProfileChange -> {
@ -149,7 +153,7 @@ class TimelineEventContentMapper(
)
}
is TimelineItemContent.CallInvite -> LegacyCallInviteContent
is TimelineItemContent.CallNotify -> CallNotifyContent
is TimelineItemContent.RtcNotification -> CallNotifyContent
}
}
}

View file

@ -18,9 +18,8 @@ class LoadingIndicatorsPostProcessor(private val systemClock: SystemClock) {
items: List<MatrixTimelineItem>,
hasMoreToLoadBackward: Boolean,
hasMoreToLoadForward: Boolean,
timelineMode: Timeline.Mode,
): List<MatrixTimelineItem> {
val shouldAddForwardLoadingIndicator = timelineMode is Timeline.Mode.Live && hasMoreToLoadForward && items.isNotEmpty()
val shouldAddForwardLoadingIndicator = hasMoreToLoadForward && items.isNotEmpty()
val currentTimestamp = systemClock.epochMillis()
return buildList {
if (hasMoreToLoadBackward) {

View file

@ -7,9 +7,10 @@
package io.element.android.libraries.matrix.impl.tracing
import com.squareup.anvil.annotations.ContributesBinding
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.tracing.LogLevel
import io.element.android.libraries.matrix.api.tracing.TracingConfiguration
import io.element.android.libraries.matrix.api.tracing.TracingService
@ -17,10 +18,10 @@ import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration
import org.matrix.rustcomponents.sdk.TracingFileConfiguration
import org.matrix.rustcomponents.sdk.reloadTracingFileWriter
import timber.log.Timber
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class RustTracingService @Inject constructor(private val buildMeta: BuildMeta) : TracingService {
@Inject
class RustTracingService(private val buildMeta: BuildMeta) : TracingService {
override fun createTimberTree(target: String): Timber.Tree {
return RustTracingTree(target = target, retrieveFromStackTrace = buildMeta.isDebuggable)
}

View file

@ -8,13 +8,16 @@
package io.element.android.libraries.matrix.impl.usersearch
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
import io.element.android.libraries.matrix.impl.mapper.map
import kotlinx.collections.immutable.toImmutableList
import org.matrix.rustcomponents.sdk.SearchUsersResults
object UserSearchResultMapper {
fun map(result: SearchUsersResults): MatrixSearchUserResults {
return MatrixSearchUserResults(
results = result.results.map(UserProfileMapper::map).toImmutableList(),
results = result.results
.map { userProfile -> userProfile.map() }
.toImmutableList(),
limited = result.limited,
)
}

View file

@ -12,22 +12,17 @@ import io.element.android.libraries.matrix.api.core.FlowId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
import io.element.android.libraries.matrix.api.verification.VerificationRequest
import io.element.android.libraries.matrix.impl.mapper.map
import org.matrix.rustcomponents.sdk.SessionVerificationRequestDetails as RustSessionVerificationRequestDetails
import org.matrix.rustcomponents.sdk.UserProfile as RustUserProfile
fun RustSessionVerificationRequestDetails.map() = SessionVerificationRequestDetails(
senderProfile = senderProfile.map(),
flowId = FlowId(flowId),
deviceId = DeviceId(deviceId),
deviceDisplayName = deviceDisplayName,
firstSeenTimestamp = firstSeenTimestamp.toLong(),
)
fun RustUserProfile.map() = SessionVerificationRequestDetails.SenderProfile(
userId = UserId(userId),
displayName = displayName,
avatarUrl = avatarUrl,
)
fun RustSessionVerificationRequestDetails.toVerificationRequest(currentUserId: UserId): VerificationRequest.Incoming {
val details = map()
return if (currentUserId == details.senderProfile.userId) {

View file

@ -7,42 +7,38 @@
package io.element.android.libraries.matrix.impl.widget
import com.squareup.anvil.annotations.ContributesBinding
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.widget.CallAnalyticCredentialsProvider
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
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 javax.inject.Inject
import uniffi.matrix_sdk.VirtualElementCallWidgetConfig
import uniffi.matrix_sdk.VirtualElementCallWidgetProperties
import uniffi.matrix_sdk.Intent as CallIntent
@ContributesBinding(AppScope::class)
class DefaultCallWidgetSettingsProvider @Inject constructor(
@Inject
class DefaultCallWidgetSettingsProvider(
private val buildMeta: BuildMeta,
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 },
@ -50,13 +46,25 @@ class DefaultCallWidgetSettingsProvider @Inject constructor(
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

@ -10,7 +10,7 @@ package io.element.android.libraries.matrix.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSession
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore
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.ExperimentalCoroutinesApi
@ -23,11 +23,12 @@ import org.junit.Test
class RustClientSessionDelegateTest {
@Test
fun `saveSessionInKeychain should update the store`() = runTest {
val sessionStore = InMemorySessionStore()
sessionStore.storeData(
aSessionData(
accessToken = "anAccessToken",
refreshToken = "aRefreshToken",
val sessionStore = InMemorySessionStore(
initialList = listOf(
aSessionData(
accessToken = "anAccessToken",
refreshToken = "aRefreshToken",
)
)
)
val sut = aRustClientSessionDelegate(sessionStore)

View file

@ -15,7 +15,7 @@ import io.element.android.libraries.matrix.impl.auth.FakeUserCertificatesProvide
import io.element.android.libraries.matrix.impl.room.FakeTimelineEventTypeFilterFactory
import io.element.android.libraries.network.useragent.SimpleUserAgentProvider
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
@ -38,7 +38,10 @@ class RustMatrixClientFactoryTest {
fun TestScope.createRustMatrixClientFactory(
baseDirectory: File = File("/base"),
cacheDirectory: File = File("/cache"),
sessionStore: SessionStore = InMemorySessionStore(),
sessionStore: SessionStore = InMemorySessionStore(
updateUserProfileResult = { _, _, _ -> },
),
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
) = RustMatrixClientFactory(
baseDirectory = baseDirectory,
cacheDirectory = cacheDirectory,
@ -52,5 +55,5 @@ fun TestScope.createRustMatrixClientFactory(
analyticsService = FakeAnalyticsService(),
featureFlagService = FakeFeatureFlagService(),
timelineEventTypeFilterFactory = FakeTimelineEventTypeFilterFactory(),
clientBuilderProvider = FakeClientBuilderProvider(),
clientBuilderProvider = clientBuilderProvider,
)

View file

@ -5,6 +5,8 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.libraries.matrix.impl
import com.google.common.truth.Truth.assertThat
@ -12,17 +14,24 @@ import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiSyncService
import io.element.android.libraries.matrix.impl.room.FakeTimelineEventTypeFilterFactory
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_DEVICE_ID
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.UserProfile
import java.io.File
class RustMatrixClientTest {
@ -51,9 +60,46 @@ class RustMatrixClientTest {
client.destroy()
}
@Test
fun `retrieving the UserProfile updates the database`() = runTest {
val updateUserProfileResult = lambdaRecorder<String, String?, String?, Unit> { _, _, _ -> }
val sessionStore = InMemorySessionStore(
initialList = listOf(
aSessionData(
sessionId = A_USER_ID.value,
userDisplayName = null,
userAvatarUrl = null,
)
),
updateUserProfileResult = updateUserProfileResult,
)
val client = createRustMatrixClient(
client = FakeFfiClient(
getProfileResult = { userId ->
UserProfile(
userId = userId,
displayName = A_USER_NAME,
avatarUrl = AN_AVATAR_URL,
)
},
),
sessionStore = sessionStore,
)
advanceUntilIdle()
updateUserProfileResult.assertions().isCalledOnce()
.with(
value(A_USER_ID.value),
value(A_USER_NAME),
value(AN_AVATAR_URL),
)
client.destroy()
}
private fun TestScope.createRustMatrixClient(
client: Client = FakeFfiClient(),
sessionStore: SessionStore = InMemorySessionStore(),
sessionStore: SessionStore = InMemorySessionStore(
updateUserProfileResult = { _, _, _ -> },
),
) = RustMatrixClient(
innerClient = client,
baseDirectory = File(""),

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.impl.memory.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
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

@ -0,0 +1,49 @@
/*
* 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.factories
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.test.A_ROOM_ID
import org.matrix.rustcomponents.sdk.JoinRule
import org.matrix.rustcomponents.sdk.Membership
import org.matrix.rustcomponents.sdk.RoomHero
import org.matrix.rustcomponents.sdk.RoomType
import org.matrix.rustcomponents.sdk.SpaceRoom
fun aRustSpaceRoom(
roomId: RoomId = A_ROOM_ID,
isDirect: Boolean = false,
canonicalAlias: String? = null,
name: String? = null,
topic: String? = null,
avatarUrl: String? = null,
roomType: RoomType = RoomType.Space,
numJoinedMembers: ULong = 0uL,
joinRule: JoinRule? = null,
worldReadable: Boolean? = null,
guestCanJoin: Boolean = false,
childrenCount: ULong = 0uL,
state: Membership? = null,
heroes: List<RoomHero> = emptyList(),
) = SpaceRoom(
roomId = roomId.value,
isDirect = isDirect,
canonicalAlias = canonicalAlias,
name = name,
topic = topic,
avatarUrl = avatarUrl,
roomType = roomType,
numJoinedMembers = numJoinedMembers,
joinRule = joinRule,
worldReadable = worldReadable,
guestCanJoin = guestCanJoin,
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
@ -25,6 +26,7 @@ import org.matrix.rustcomponents.sdk.PusherKind
import org.matrix.rustcomponents.sdk.RoomDirectorySearch
import org.matrix.rustcomponents.sdk.Session
import org.matrix.rustcomponents.sdk.SessionVerificationController
import org.matrix.rustcomponents.sdk.SpaceService
import org.matrix.rustcomponents.sdk.SyncService
import org.matrix.rustcomponents.sdk.SyncServiceBuilder
import org.matrix.rustcomponents.sdk.TaskHandle
@ -40,6 +42,8 @@ class FakeFfiClient(
private val session: Session = aRustSession(),
private val clearCachesResult: () -> Unit = { lambdaError() },
private val withUtdHook: (UnableToDecryptDelegate) -> Unit = { lambdaError() },
private val getProfileResult: (String) -> UserProfile = { UserProfile(userId = userId, displayName = null, avatarUrl = null) },
private val homeserverLoginDetailsResult: () -> HomeserverLoginDetails = { lambdaError() },
private val closeResult: () -> Unit = {},
) : Client(NoPointer) {
override fun userId(): String = userId
@ -52,6 +56,7 @@ class FakeFfiClient(
override suspend fun cachedAvatarUrl(): String? = null
override suspend fun restoreSession(session: Session) = Unit
override fun syncService(): SyncServiceBuilder = FakeFfiSyncServiceBuilder()
override fun spaceService(): SpaceService = FakeFfiSpaceService()
override fun roomDirectorySearch(): RoomDirectorySearch = FakeFfiRoomDirectorySearch()
override suspend fun setPusher(
identifiers: PusherIdentifiers,
@ -69,12 +74,18 @@ class FakeFfiClient(
override suspend fun ignoredUsers(): List<String> {
return emptyList()
}
override fun subscribeToIgnoredUsers(listener: IgnoredUsersListener): TaskHandle {
return FakeFfiTaskHandle()
}
override suspend fun getProfile(userId: String): UserProfile {
return UserProfile(userId = userId, displayName = null, avatarUrl = null)
return getProfileResult(userId)
}
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
@ -40,8 +42,5 @@ class FakeFfiClientBuilder : ClientBuilder(NoPointer) {
override fun username(username: String) = this
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

@ -0,0 +1,13 @@
/*
* 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 org.matrix.rustcomponents.sdk.NoPointer
import org.matrix.rustcomponents.sdk.SpaceService
class FakeFfiSpaceService : SpaceService(NoPointer)

View file

@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.usersearch
package io.element.android.libraries.matrix.impl.mapper
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.user.MatrixUser
@ -16,7 +16,7 @@ import org.junit.Test
class UserProfileMapperTest {
@Test
fun map() {
assertThat(UserProfileMapper.map(aRustUserProfile(A_USER_ID.value, "displayName", "avatarUrl")))
assertThat(aRustUserProfile(A_USER_ID.value, "displayName", "avatarUrl").map())
.isEqualTo(MatrixUser(A_USER_ID, "displayName", "avatarUrl"))
}
}

View file

@ -15,55 +15,55 @@ import org.matrix.rustcomponents.sdk.MessageLikeEventType
class MessageEventTypeKtTest {
@Test
fun `map Rust type should result to correct Kotlin type`() {
assertThat(MessageLikeEventType.CALL_ANSWER.map()).isEqualTo(MessageEventType.CALL_ANSWER)
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.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)
assertThat(MessageLikeEventType.KEY_VERIFICATION_ACCEPT.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_ACCEPT)
assertThat(MessageLikeEventType.KEY_VERIFICATION_KEY.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_KEY)
assertThat(MessageLikeEventType.KEY_VERIFICATION_MAC.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_MAC)
assertThat(MessageLikeEventType.KEY_VERIFICATION_DONE.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_DONE)
assertThat(MessageLikeEventType.REACTION.map()).isEqualTo(MessageEventType.REACTION)
assertThat(MessageLikeEventType.ROOM_ENCRYPTED.map()).isEqualTo(MessageEventType.ROOM_ENCRYPTED)
assertThat(MessageLikeEventType.ROOM_MESSAGE.map()).isEqualTo(MessageEventType.ROOM_MESSAGE)
assertThat(MessageLikeEventType.ROOM_REDACTION.map()).isEqualTo(MessageEventType.ROOM_REDACTION)
assertThat(MessageLikeEventType.STICKER.map()).isEqualTo(MessageEventType.STICKER)
assertThat(MessageLikeEventType.POLL_END.map()).isEqualTo(MessageEventType.POLL_END)
assertThat(MessageLikeEventType.POLL_RESPONSE.map()).isEqualTo(MessageEventType.POLL_RESPONSE)
assertThat(MessageLikeEventType.POLL_START.map()).isEqualTo(MessageEventType.POLL_START)
assertThat(MessageLikeEventType.UNSTABLE_POLL_END.map()).isEqualTo(MessageEventType.UNSTABLE_POLL_END)
assertThat(MessageLikeEventType.UNSTABLE_POLL_RESPONSE.map()).isEqualTo(MessageEventType.UNSTABLE_POLL_RESPONSE)
assertThat(MessageLikeEventType.UNSTABLE_POLL_START.map()).isEqualTo(MessageEventType.UNSTABLE_POLL_START)
assertThat(MessageLikeEventType.CallAnswer.map()).isEqualTo(MessageEventType.CallAnswer)
assertThat(MessageLikeEventType.CallInvite.map()).isEqualTo(MessageEventType.CallInvite)
assertThat(MessageLikeEventType.CallHangup.map()).isEqualTo(MessageEventType.CallHangup)
assertThat(MessageLikeEventType.CallCandidates.map()).isEqualTo(MessageEventType.CallCandidates)
assertThat(MessageLikeEventType.RtcNotification.map()).isEqualTo(MessageEventType.RtcNotification)
assertThat(MessageLikeEventType.KeyVerificationReady.map()).isEqualTo(MessageEventType.KeyVerificationReady)
assertThat(MessageLikeEventType.KeyVerificationStart.map()).isEqualTo(MessageEventType.KeyVerificationStart)
assertThat(MessageLikeEventType.KeyVerificationCancel.map()).isEqualTo(MessageEventType.KeyVerificationCancel)
assertThat(MessageLikeEventType.KeyVerificationAccept.map()).isEqualTo(MessageEventType.KeyVerificationAccept)
assertThat(MessageLikeEventType.KeyVerificationKey.map()).isEqualTo(MessageEventType.KeyVerificationKey)
assertThat(MessageLikeEventType.KeyVerificationMac.map()).isEqualTo(MessageEventType.KeyVerificationMac)
assertThat(MessageLikeEventType.KeyVerificationDone.map()).isEqualTo(MessageEventType.KeyVerificationDone)
assertThat(MessageLikeEventType.Reaction.map()).isEqualTo(MessageEventType.Reaction)
assertThat(MessageLikeEventType.RoomEncrypted.map()).isEqualTo(MessageEventType.RoomEncrypted)
assertThat(MessageLikeEventType.RoomMessage.map()).isEqualTo(MessageEventType.RoomMessage)
assertThat(MessageLikeEventType.RoomRedaction.map()).isEqualTo(MessageEventType.RoomRedaction)
assertThat(MessageLikeEventType.Sticker.map()).isEqualTo(MessageEventType.Sticker)
assertThat(MessageLikeEventType.PollEnd.map()).isEqualTo(MessageEventType.PollEnd)
assertThat(MessageLikeEventType.PollResponse.map()).isEqualTo(MessageEventType.PollResponse)
assertThat(MessageLikeEventType.PollStart.map()).isEqualTo(MessageEventType.PollStart)
assertThat(MessageLikeEventType.UnstablePollEnd.map()).isEqualTo(MessageEventType.UnstablePollEnd)
assertThat(MessageLikeEventType.UnstablePollResponse.map()).isEqualTo(MessageEventType.UnstablePollResponse)
assertThat(MessageLikeEventType.UnstablePollStart.map()).isEqualTo(MessageEventType.UnstablePollStart)
}
@Test
fun `map Kotlin type should result to correct Rust type`() {
assertThat(MessageEventType.CALL_ANSWER.map()).isEqualTo(MessageLikeEventType.CALL_ANSWER)
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.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)
assertThat(MessageEventType.KEY_VERIFICATION_ACCEPT.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_ACCEPT)
assertThat(MessageEventType.KEY_VERIFICATION_KEY.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_KEY)
assertThat(MessageEventType.KEY_VERIFICATION_MAC.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_MAC)
assertThat(MessageEventType.KEY_VERIFICATION_DONE.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_DONE)
assertThat(MessageEventType.REACTION.map()).isEqualTo(MessageLikeEventType.REACTION)
assertThat(MessageEventType.ROOM_ENCRYPTED.map()).isEqualTo(MessageLikeEventType.ROOM_ENCRYPTED)
assertThat(MessageEventType.ROOM_MESSAGE.map()).isEqualTo(MessageLikeEventType.ROOM_MESSAGE)
assertThat(MessageEventType.ROOM_REDACTION.map()).isEqualTo(MessageLikeEventType.ROOM_REDACTION)
assertThat(MessageEventType.STICKER.map()).isEqualTo(MessageLikeEventType.STICKER)
assertThat(MessageEventType.POLL_END.map()).isEqualTo(MessageLikeEventType.POLL_END)
assertThat(MessageEventType.POLL_RESPONSE.map()).isEqualTo(MessageLikeEventType.POLL_RESPONSE)
assertThat(MessageEventType.POLL_START.map()).isEqualTo(MessageLikeEventType.POLL_START)
assertThat(MessageEventType.UNSTABLE_POLL_END.map()).isEqualTo(MessageLikeEventType.UNSTABLE_POLL_END)
assertThat(MessageEventType.UNSTABLE_POLL_RESPONSE.map()).isEqualTo(MessageLikeEventType.UNSTABLE_POLL_RESPONSE)
assertThat(MessageEventType.UNSTABLE_POLL_START.map()).isEqualTo(MessageLikeEventType.UNSTABLE_POLL_START)
assertThat(MessageEventType.CallAnswer.map()).isEqualTo(MessageLikeEventType.CallAnswer)
assertThat(MessageEventType.CallInvite.map()).isEqualTo(MessageLikeEventType.CallInvite)
assertThat(MessageEventType.CallHangup.map()).isEqualTo(MessageLikeEventType.CallHangup)
assertThat(MessageEventType.CallCandidates.map()).isEqualTo(MessageLikeEventType.CallCandidates)
assertThat(MessageEventType.RtcNotification.map()).isEqualTo(MessageLikeEventType.RtcNotification)
assertThat(MessageEventType.KeyVerificationReady.map()).isEqualTo(MessageLikeEventType.KeyVerificationReady)
assertThat(MessageEventType.KeyVerificationStart.map()).isEqualTo(MessageLikeEventType.KeyVerificationStart)
assertThat(MessageEventType.KeyVerificationCancel.map()).isEqualTo(MessageLikeEventType.KeyVerificationCancel)
assertThat(MessageEventType.KeyVerificationAccept.map()).isEqualTo(MessageLikeEventType.KeyVerificationAccept)
assertThat(MessageEventType.KeyVerificationKey.map()).isEqualTo(MessageLikeEventType.KeyVerificationKey)
assertThat(MessageEventType.KeyVerificationMac.map()).isEqualTo(MessageLikeEventType.KeyVerificationMac)
assertThat(MessageEventType.KeyVerificationDone.map()).isEqualTo(MessageLikeEventType.KeyVerificationDone)
assertThat(MessageEventType.Reaction.map()).isEqualTo(MessageLikeEventType.Reaction)
assertThat(MessageEventType.RoomEncrypted.map()).isEqualTo(MessageLikeEventType.RoomEncrypted)
assertThat(MessageEventType.RoomMessage.map()).isEqualTo(MessageLikeEventType.RoomMessage)
assertThat(MessageEventType.RoomRedaction.map()).isEqualTo(MessageLikeEventType.RoomRedaction)
assertThat(MessageEventType.Sticker.map()).isEqualTo(MessageLikeEventType.Sticker)
assertThat(MessageEventType.PollEnd.map()).isEqualTo(MessageLikeEventType.PollEnd)
assertThat(MessageEventType.PollResponse.map()).isEqualTo(MessageLikeEventType.PollResponse)
assertThat(MessageEventType.PollStart.map()).isEqualTo(MessageLikeEventType.PollStart)
assertThat(MessageEventType.UnstablePollEnd.map()).isEqualTo(MessageLikeEventType.UnstablePollEnd)
assertThat(MessageEventType.UnstablePollResponse.map()).isEqualTo(MessageLikeEventType.UnstablePollResponse)
assertThat(MessageEventType.UnstablePollStart.map()).isEqualTo(MessageLikeEventType.UnstablePollStart)
}
}

View file

@ -57,6 +57,7 @@ class RustBaseRoomTest {
leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) {
val membershipUpdate = awaitItem()
assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId)
assertThat(membershipUpdate.isSpace).isFalse()
assertThat(membershipUpdate.isUserInRoom).isFalse()
assertThat(membershipUpdate.change).isEqualTo(MembershipChange.LEFT)
}
@ -77,6 +78,7 @@ class RustBaseRoomTest {
leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) {
val membershipUpdate = awaitItem()
assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId)
assertThat(membershipUpdate.isSpace).isFalse()
assertThat(membershipUpdate.isUserInRoom).isFalse()
assertThat(membershipUpdate.change).isEqualTo(MembershipChange.KNOCK_RETRACTED)
}
@ -97,6 +99,7 @@ class RustBaseRoomTest {
leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) {
val membershipUpdate = awaitItem()
assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId)
assertThat(membershipUpdate.isSpace).isFalse()
assertThat(membershipUpdate.isUserInRoom).isFalse()
assertThat(membershipUpdate.change).isEqualTo(MembershipChange.INVITATION_REJECTED)
}

View file

@ -0,0 +1,189 @@
/*
* 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.spaces
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSpaceRoom
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.matrix.test.A_ROOM_ID_3
import io.element.android.libraries.previewutils.room.aSpaceRoom
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.matrix.rustcomponents.sdk.SpaceListUpdate
class RoomSummaryListProcessorTest {
private val spaceRoomsFlow = MutableStateFlow<List<SpaceRoom>>(emptyList())
@Test
fun `Append adds new entries at the end of the list`() = runTest {
spaceRoomsFlow.value = listOf(aSpaceRoom())
val processor = createProcessor()
val newEntry = aRustSpaceRoom(roomId = A_ROOM_ID_2)
processor.postUpdates(listOf(SpaceListUpdate.Append(listOf(newEntry, newEntry, newEntry))))
assertThat(spaceRoomsFlow.value.count()).isEqualTo(4)
assertThat(spaceRoomsFlow.value.subList(1, 4).all { it.roomId == A_ROOM_ID_2 }).isTrue()
}
@Test
fun `PushBack adds a new entry at the end of the list`() = runTest {
spaceRoomsFlow.value = listOf(aSpaceRoom())
val processor = createProcessor()
processor.postUpdates(listOf(SpaceListUpdate.PushBack(aRustSpaceRoom(roomId = A_ROOM_ID_2))))
assertThat(spaceRoomsFlow.value.count()).isEqualTo(2)
assertThat(spaceRoomsFlow.value.last().roomId).isEqualTo(A_ROOM_ID_2)
}
@Test
fun `PushFront inserts a new entry at the start of the list`() = runTest {
spaceRoomsFlow.value = listOf(aSpaceRoom())
val processor = createProcessor()
processor.postUpdates(listOf(SpaceListUpdate.PushFront(aRustSpaceRoom(roomId = A_ROOM_ID_2))))
assertThat(spaceRoomsFlow.value.count()).isEqualTo(2)
assertThat(spaceRoomsFlow.value.first().roomId).isEqualTo(A_ROOM_ID_2)
}
@Test
fun `Set replaces an entry at some index`() = runTest {
spaceRoomsFlow.value = listOf(aSpaceRoom())
val processor = createProcessor()
val index = 0
processor.postUpdates(listOf(SpaceListUpdate.Set(index.toUInt(), aRustSpaceRoom(roomId = A_ROOM_ID_2))))
assertThat(spaceRoomsFlow.value.count()).isEqualTo(1)
assertThat(spaceRoomsFlow.value[index].roomId).isEqualTo(A_ROOM_ID_2)
}
@Test
fun `Insert inserts a new entry at the provided index`() = runTest {
spaceRoomsFlow.value = listOf(aSpaceRoom())
val processor = createProcessor()
val index = 0
processor.postUpdates(listOf(SpaceListUpdate.Insert(index.toUInt(), aRustSpaceRoom(roomId = A_ROOM_ID_2))))
assertThat(spaceRoomsFlow.value.count()).isEqualTo(2)
assertThat(spaceRoomsFlow.value[index].roomId).isEqualTo(A_ROOM_ID_2)
}
@Test
fun `Remove removes an entry at some index`() = runTest {
spaceRoomsFlow.value = listOf(
aSpaceRoom(roomId = A_ROOM_ID),
aSpaceRoom(roomId = A_ROOM_ID_2)
)
val processor = createProcessor()
val index = 0
processor.postUpdates(listOf(SpaceListUpdate.Remove(index.toUInt())))
assertThat(spaceRoomsFlow.value.count()).isEqualTo(1)
assertThat(spaceRoomsFlow.value[index].roomId).isEqualTo(A_ROOM_ID_2)
}
@Test
fun `PopBack removes an entry at the end of the list`() = runTest {
spaceRoomsFlow.value = listOf(
aSpaceRoom(roomId = A_ROOM_ID),
aSpaceRoom(roomId = A_ROOM_ID_2)
)
val processor = createProcessor()
val index = 0
processor.postUpdates(listOf(SpaceListUpdate.PopBack))
assertThat(spaceRoomsFlow.value.count()).isEqualTo(1)
assertThat(spaceRoomsFlow.value[index].roomId).isEqualTo(A_ROOM_ID)
}
@Test
fun `PopFront removes an entry at the start of the list`() = runTest {
spaceRoomsFlow.value = listOf(
aSpaceRoom(roomId = A_ROOM_ID),
aSpaceRoom(roomId = A_ROOM_ID_2)
)
val processor = createProcessor()
val index = 0
processor.postUpdates(listOf(SpaceListUpdate.PopFront))
assertThat(spaceRoomsFlow.value.count()).isEqualTo(1)
assertThat(spaceRoomsFlow.value[index].roomId).isEqualTo(A_ROOM_ID_2)
}
@Test
fun `Clear removes all the entries`() = runTest {
spaceRoomsFlow.value = listOf(
aSpaceRoom(roomId = A_ROOM_ID),
aSpaceRoom(roomId = A_ROOM_ID_2)
)
val processor = createProcessor()
processor.postUpdates(listOf(SpaceListUpdate.Clear))
assertThat(spaceRoomsFlow.value).isEmpty()
}
@Test
fun `Truncate removes all entries after the provided length`() = runTest {
spaceRoomsFlow.value = listOf(
aSpaceRoom(roomId = A_ROOM_ID),
aSpaceRoom(roomId = A_ROOM_ID_2)
)
val processor = createProcessor()
val index = 0
processor.postUpdates(listOf(SpaceListUpdate.Truncate(1u)))
assertThat(spaceRoomsFlow.value.count()).isEqualTo(1)
assertThat(spaceRoomsFlow.value[index].roomId).isEqualTo(A_ROOM_ID)
}
@Test
fun `Reset removes all entries and add the provided ones`() = runTest {
spaceRoomsFlow.value = listOf(
aSpaceRoom(roomId = A_ROOM_ID),
aSpaceRoom(roomId = A_ROOM_ID_2)
)
val processor = createProcessor()
val index = 0
processor.postUpdates(listOf(SpaceListUpdate.Reset(listOf(aRustSpaceRoom(A_ROOM_ID_3)))))
assertThat(spaceRoomsFlow.value.count()).isEqualTo(1)
assertThat(spaceRoomsFlow.value[index].roomId).isEqualTo(A_ROOM_ID_3)
}
@Test
fun `When there is no replay cache SpaceListUpdateProcessor starts with an empty list`() = runTest {
val spaceRoomsSharedFlow = MutableSharedFlow<List<SpaceRoom>>(replay = 1)
val processor = createProcessor(
spaceRoomsFlow = spaceRoomsSharedFlow,
)
assertThat(spaceRoomsSharedFlow.replayCache).isEmpty()
val newEntry = aRustSpaceRoom(roomId = A_ROOM_ID)
processor.postUpdates(listOf(SpaceListUpdate.Append(listOf(newEntry))))
assertThat(spaceRoomsSharedFlow.replayCache).hasSize(1)
assertThat(spaceRoomsSharedFlow.replayCache.first()).hasSize(1)
}
private fun createProcessor(
spaceRoomsFlow: MutableSharedFlow<List<SpaceRoom>> = this.spaceRoomsFlow,
) = SpaceListUpdateProcessor(
spaceRoomsFlow = spaceRoomsFlow,
mapper = SpaceRoomMapper(),
)
}

View file

@ -0,0 +1,101 @@
/*
* 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.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()
}
private fun TestScope.createRustSpaceRoomList(
roomId: RoomId = A_ROOM_ID,
innerSpaceRoomList: InnerSpaceRoomList = FakeFfiSpaceRoomList(),
innerProvider: suspend () -> InnerSpaceRoomList = { innerSpaceRoomList },
spaceRoomMapper: SpaceRoomMapper = SpaceRoomMapper(),
): RustSpaceRoomList {
return RustSpaceRoomList(
roomId = roomId,
innerProvider = innerProvider,
coroutineScope = backgroundScope,
spaceRoomMapper = spaceRoomMapper,
)
}
}

View file

@ -24,7 +24,6 @@ class LoadingIndicatorsPostProcessorTest {
items = listOf(messageEvent, messageEvent2),
hasMoreToLoadBackward = true,
hasMoreToLoadForward = false,
timelineMode = Timeline.Mode.Live,
)
assertThat(result).containsExactly(
MatrixTimelineItem.Virtual(
@ -47,7 +46,6 @@ class LoadingIndicatorsPostProcessorTest {
items = listOf(messageEvent, messageEvent2),
hasMoreToLoadBackward = false,
hasMoreToLoadForward = true,
timelineMode = Timeline.Mode.Live,
)
assertThat(result).containsExactly(
messageEvent,
@ -70,7 +68,6 @@ class LoadingIndicatorsPostProcessorTest {
items = listOf(messageEvent, messageEvent2),
hasMoreToLoadBackward = true,
hasMoreToLoadForward = true,
timelineMode = Timeline.Mode.Live,
)
assertThat(result).containsExactly(
MatrixTimelineItem.Virtual(
@ -100,7 +97,6 @@ class LoadingIndicatorsPostProcessorTest {
items = listOf(),
hasMoreToLoadBackward = true,
hasMoreToLoadForward = true,
timelineMode = Timeline.Mode.Live,
)
assertThat(result).containsExactly(
MatrixTimelineItem.Virtual(

View file

@ -9,7 +9,7 @@ package io.element.android.libraries.matrix.impl.util
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -24,14 +24,15 @@ class SessionPathsProviderTest {
@Test
fun `if session is found, provides returns the data`() = runTest {
val store = InMemorySessionStore()
val sut = SessionPathsProvider(store)
store.storeData(
aSessionData(
sessionPath = "/a/path/to/a/session",
cachePath = "/a/path/to/a/cache",
val store = InMemorySessionStore(
initialList = listOf(
aSessionData(
sessionPath = "/a/path/to/a/session",
cachePath = "/a/path/to/a/cache",
)
)
)
val sut = SessionPathsProvider(store)
val result = sut.provides(A_SESSION_ID)!!
assertThat(result.fileDirectory.absolutePath).isEqualTo("/a/path/to/a/session")
assertThat(result.cacheDirectory.absolutePath).isEqualTo("/a/path/to/a/cache")

View file

@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.spaces.SpaceService
import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
import io.element.android.libraries.matrix.api.user.MatrixUser
@ -42,6 +43,7 @@ import io.element.android.libraries.matrix.test.notificationsettings.FakeNotific
import io.element.android.libraries.matrix.test.pushers.FakePushersService
import io.element.android.libraries.matrix.test.roomdirectory.FakeRoomDirectoryService
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
import io.element.android.libraries.matrix.test.sync.FakeSyncService
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import io.element.android.tests.testutils.lambda.lambdaError
@ -65,6 +67,7 @@ class FakeMatrixClient(
private val userDisplayName: String? = A_USER_NAME,
private val userAvatarUrl: String? = AN_AVATAR_URL,
override val roomListService: RoomListService = FakeRoomListService(),
override val spaceService: SpaceService = FakeSpaceService(),
override val mediaLoader: MatrixMediaLoader = FakeMatrixMediaLoader(),
private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
private val pushersService: FakePushersService = FakePushersService(),
@ -87,7 +90,6 @@ class FakeMatrixClient(
private val canDeactivateAccountResult: () -> Boolean = { lambdaError() },
private val deactivateAccountResult: (String, Boolean) -> Result<Unit> = { _, _ -> lambdaError() },
private val currentSlidingSyncVersionLambda: () -> Result<SlidingSyncVersion> = { lambdaError() },
private val availableSlidingSyncVersionsLambda: () -> Result<List<SlidingSyncVersion>> = { lambdaError() },
private val ignoreUserResult: (UserId) -> Result<Unit> = { lambdaError() },
private var unIgnoreUserResult: (UserId) -> Result<Unit> = { Result.success(Unit) },
private val canReportRoomLambda: () -> Boolean = { false },
@ -95,6 +97,8 @@ class FakeMatrixClient(
override val ignoredUsersFlow: StateFlow<ImmutableList<UserId>> = MutableStateFlow(persistentListOf()),
private val getMaxUploadSizeResult: () -> Result<Long> = { lambdaError() },
private val getJoinedRoomIdsResult: () -> Result<Set<RoomId>> = { Result.success(emptySet()) },
private val getRecentEmojisLambda: () -> Result<List<String>> = { Result.success(emptyList()) },
private val addRecentEmojiLambda: (String) -> Result<Unit> = { Result.success(Unit) },
) : MatrixClient {
var setDisplayNameCalled: Boolean = false
private set
@ -336,10 +340,6 @@ class FakeMatrixClient(
return currentSlidingSyncVersionLambda()
}
override suspend fun availableSlidingSyncVersions(): Result<List<SlidingSyncVersion>> {
return availableSlidingSyncVersionsLambda()
}
override suspend fun canReportRoom(): Boolean {
return canReportRoomLambda()
}
@ -351,4 +351,12 @@ class FakeMatrixClient(
override suspend fun getMaxFileUploadSize(): Result<Long> {
return getMaxUploadSizeResult()
}
override suspend fun addRecentEmoji(emoji: String): Result<Unit> {
return addRecentEmojiLambda(emoji)
}
override suspend fun getRecentEmojis(): Result<List<String>> {
return getRecentEmojisLambda()
}
}

View file

@ -25,6 +25,7 @@ const val A_USER_NAME_2 = "Bob"
const val A_PASSWORD = "password"
const val A_PASSPHRASE = "passphrase"
const val A_SECRET = "secret"
const val AN_APPLICATION_NAME = "AppName"
val A_USER_ID = UserId("@alice:server.org")
val A_USER_ID_2 = UserId("@bob:server.org")
@ -64,6 +65,8 @@ const val ANOTHER_MESSAGE = "Hello universe!"
const val A_CAPTION = "A media caption"
const val A_REASON = "A reason"
const val A_SPACE_NAME = "A space name"
const val A_REDACTION_REASON = "A redaction reason"
const val A_HOMESERVER_URL = "matrix.org"

View file

@ -19,14 +19,11 @@ import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.sessionstorage.api.LoggedInState
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.simulateLongTask
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOf
val A_OIDC_DATA = OidcDetails(url = "a-url")
@ -44,14 +41,6 @@ class FakeMatrixAuthenticationService(
private var matrixClient: MatrixClient? = null
private var onAuthenticationListener: ((MatrixClient) -> Unit)? = null
var getLatestSessionIdLambda: (() -> SessionId?) = { null }
override fun loggedInStateFlow(): Flow<LoggedInState> {
return flowOf(LoggedInState.NotLoggedIn)
}
override suspend fun getLatestSessionId(): SessionId? = getLatestSessionIdLambda()
override suspend fun restoreSession(sessionId: SessionId): Result<MatrixClient> {
matrixClientResult?.let {
return it.invoke(sessionId)

View file

@ -29,6 +29,8 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.test.TestScope
@ -59,7 +61,7 @@ class FakeBaseRoom(
private val markAsReadResult: (ReceiptType) -> Result<Unit> = { Result.success(Unit) },
private val powerLevelsResult: () -> Result<RoomPowerLevelsValues> = { lambdaError() },
private val leaveRoomLambda: () -> Result<Unit> = { lambdaError() },
private val updateMembersResult: () -> Unit = { lambdaError() },
private var updateMembersResult: () -> Unit = { lambdaError() },
private val getMembersResult: (Int) -> Result<List<RoomMember>> = { lambdaError() },
private val saveComposerDraftLambda: (ComposerDraft) -> Result<Unit> = { _: ComposerDraft -> Result.success(Unit) },
private val loadComposerDraftLambda: () -> Result<ComposerDraft?> = { Result.success<ComposerDraft?>(null) },
@ -69,6 +71,7 @@ class FakeBaseRoom(
private val forgetResult: () -> Result<Unit> = { lambdaError() },
private val reportRoomResult: (String?) -> Result<Unit> = { lambdaError() },
private val predecessorRoomResult: () -> PredecessorRoom? = { null },
private val threadRootIdForEventResult: (EventId) -> Result<ThreadId?> = { lambdaError() },
) : BaseRoom {
private val _roomInfoFlow: MutableStateFlow<RoomInfo> = MutableStateFlow(initialRoomInfo)
override val roomInfoFlow: StateFlow<RoomInfo> = _roomInfoFlow
@ -77,6 +80,12 @@ class FakeBaseRoom(
_roomInfoFlow.tryEmit(roomInfo)
}
private val declineCallFlowMap: MutableMap<EventId, MutableSharedFlow<UserId>> = mutableMapOf()
suspend fun givenDecliner(userId: UserId, forNotificationEventId: EventId) {
declineCallFlowMap[forNotificationEventId]?.emit(userId)
}
override val membersStateFlow: MutableStateFlow<RoomMembersState> = MutableStateFlow(RoomMembersState.Unknown)
override suspend fun updateMembers() = updateMembersResult()
@ -222,7 +231,24 @@ class FakeBaseRoom(
override suspend fun reportRoom(reason: String?) = reportRoomResult(reason)
override suspend fun declineCall(notificationEventId: EventId): Result<Unit> {
return Result.success(Unit)
}
override suspend fun subscribeToCallDecline(notificationEventId: EventId): Flow<UserId> {
val flow = declineCallFlowMap.getOrPut(notificationEventId, { MutableSharedFlow() })
return flow
}
override fun predecessorRoom(): PredecessorRoom? = predecessorRoomResult()
fun givenUpdateMembersResult(result: () -> Unit) {
updateMembersResult = result
}
override suspend fun threadRootIdForEvent(eventId: EventId): Result<ThreadId?> {
return threadRootIdForEventResult(eventId)
}
}
fun defaultRoomPowerLevelValues() = RoomPowerLevelsValues(

View file

@ -19,7 +19,6 @@ fun aRoomMember(
membership: RoomMembershipState = RoomMembershipState.JOIN,
isNameAmbiguous: Boolean = false,
powerLevel: Long = 0L,
normalizedPowerLevel: Long = 0L,
isIgnored: Boolean = false,
role: RoomMember.Role = RoomMember.Role.User,
membershipChangeReason: String? = null,
@ -30,7 +29,6 @@ fun aRoomMember(
membership = membership,
isNameAmbiguous = isNameAmbiguous,
powerLevel = powerLevel,
normalizedPowerLevel = normalizedPowerLevel,
isIgnored = isIgnored,
role = role,
membershipChangeReason = membershipChangeReason,

View file

@ -0,0 +1,57 @@
/*
* 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.test.spaces
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import java.util.Optional
class FakeSpaceRoomList(
override val roomId: RoomId = A_ROOM_ID,
initialSpaceFlowValue: SpaceRoom? = null,
initialSpaceRoomsValue: List<SpaceRoom> = emptyList(),
initialSpaceRoomList: SpaceRoomList.PaginationStatus = SpaceRoomList.PaginationStatus.Loading,
private val paginateResult: () -> Result<Unit> = { lambdaError() },
) : SpaceRoomList {
private val currentSpaceMutableStateFlow: MutableStateFlow<Optional<SpaceRoom>> = MutableStateFlow(Optional.ofNullable(initialSpaceFlowValue))
override val currentSpaceFlow: StateFlow<Optional<SpaceRoom>> = currentSpaceMutableStateFlow.asStateFlow()
fun emitCurrentSpace(value: SpaceRoom?) {
currentSpaceMutableStateFlow.value = Optional.ofNullable(value)
}
private val _spaceRoomsFlow: MutableStateFlow<List<SpaceRoom>> = MutableStateFlow(initialSpaceRoomsValue)
override val spaceRoomsFlow: Flow<List<SpaceRoom>> = _spaceRoomsFlow.asStateFlow()
fun emitSpaceRooms(value: List<SpaceRoom>) {
_spaceRoomsFlow.value = value
}
private val _paginationStatusFlow: MutableStateFlow<SpaceRoomList.PaginationStatus> = MutableStateFlow(initialSpaceRoomList)
override val paginationStatusFlow: StateFlow<SpaceRoomList.PaginationStatus> = _paginationStatusFlow.asStateFlow()
fun emitPaginationStatus(value: SpaceRoomList.PaginationStatus) {
_paginationStatusFlow.value = value
}
override suspend fun paginate(): Result<Unit> = simulateLongTask {
paginateResult()
}
override fun destroy() {
// No op
}
}

View file

@ -0,0 +1,39 @@
/*
* 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.test.spaces
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
import io.element.android.libraries.matrix.api.spaces.SpaceService
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
class FakeSpaceService(
private val joinedSpacesResult: () -> Result<List<SpaceRoom>> = { lambdaError() },
private val spaceRoomListResult: (RoomId) -> SpaceRoomList = { lambdaError() },
) : SpaceService {
private val _spaceRoomsFlow = MutableSharedFlow<List<SpaceRoom>>()
override val spaceRoomsFlow: SharedFlow<List<SpaceRoom>>
get() = _spaceRoomsFlow.asSharedFlow()
suspend fun emitSpaceRoomList(value: List<SpaceRoom>) {
_spaceRoomsFlow.emit(value)
}
override suspend fun joinedSpaces(): Result<List<SpaceRoom>> = simulateLongTask {
return joinedSpacesResult()
}
override fun spaceRoomList(id: RoomId): SpaceRoomList {
return spaceRoomListResult(id)
}
}

View file

@ -304,9 +304,9 @@ class FakeTimeline(
)
}
var toggleReactionLambda: (emoji: String, eventOrTransactionId: EventOrTransactionId) -> Result<Unit> = { _, _ -> lambdaError() }
var toggleReactionLambda: (emoji: String, eventOrTransactionId: EventOrTransactionId) -> Result<Boolean> = { _, _ -> lambdaError() }
override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit> = simulateLongTask {
override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Boolean> = simulateLongTask {
toggleReactionLambda(
emoji,
eventOrTransactionId,

Some files were not shown because too many files have changed in this diff Show more