Merge branch 'develop' into separate_import_error
This commit is contained in:
commit
8f8e190e68
2773 changed files with 29051 additions and 10914 deletions
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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].
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(""),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ fun aRustRoomMember(
|
|||
membership = membership,
|
||||
isNameAmbiguous = isNameAmbiguous,
|
||||
powerLevel = powerLevel,
|
||||
normalizedPowerLevel = powerLevel,
|
||||
isIgnored = isIgnored,
|
||||
suggestedRoleForPowerLevel = role,
|
||||
membershipChangeReason = membershipChangeReason,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue