mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-03 06:55:41 +00:00
Merge branch 'develop' into feature/bma/scroll-to-unread-messages
This commit is contained in:
+12
-8
@@ -32,6 +32,7 @@ import io.element.android.libraries.matrix.api.linknewdevice.LinkDesktopHandler
|
||||
import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileHandler
|
||||
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
||||
import io.element.android.libraries.matrix.api.oauth.AccountManagementAction
|
||||
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.api.room.BaseRoom
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
@@ -81,7 +82,6 @@ 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.UserSearchResultMapper
|
||||
import io.element.android.libraries.matrix.impl.util.SessionPathsProvider
|
||||
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
|
||||
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
|
||||
import io.element.android.libraries.matrix.impl.verification.RustSessionVerificationService
|
||||
@@ -137,6 +137,7 @@ import org.matrix.rustcomponents.sdk.RoomPreset as RustRoomPreset
|
||||
import org.matrix.rustcomponents.sdk.SyncService as ClientSyncService
|
||||
|
||||
class RustMatrixClient(
|
||||
override val sessionPaths: SessionPaths,
|
||||
private val innerClient: Client,
|
||||
private val sessionStore: SessionStore,
|
||||
private val sessionDelegate: RustClientSessionDelegate,
|
||||
@@ -187,8 +188,6 @@ class RustMatrixClient(
|
||||
sessionDispatcher = sessionDispatcher,
|
||||
)
|
||||
|
||||
private val sessionPathsProvider = SessionPathsProvider(sessionStore)
|
||||
|
||||
private val roomSyncSubscriber: RoomSyncSubscriber = RoomSyncSubscriber(innerRoomListService, dispatchers)
|
||||
|
||||
override val roomListService: RoomListService = RustRoomListService(
|
||||
@@ -801,6 +800,12 @@ class RustMatrixClient(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun markAllRoomsAsRead(): Result<Unit> = withContext(sessionDispatcher) {
|
||||
runCatchingExceptions {
|
||||
innerClient.markAllRoomsAsRead()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun performDatabaseVacuum(): Result<Unit> = withContext(sessionDispatcher) {
|
||||
runCatchingExceptions {
|
||||
Timber.d("Performing database vacuuming for session $sessionId...")
|
||||
@@ -824,17 +829,16 @@ class RustMatrixClient(
|
||||
private suspend fun getCacheSize(
|
||||
includeCryptoDb: Boolean = false,
|
||||
): Long = withContext(sessionDispatcher) {
|
||||
val sessionDirectory = sessionPathsProvider.provides(sessionId) ?: return@withContext 0L
|
||||
val cacheSize = sessionDirectory.cacheDirectory.getSizeOfFiles()
|
||||
val cacheSize = sessionPaths.cacheDirectory.getSizeOfFiles()
|
||||
if (includeCryptoDb) {
|
||||
cacheSize + sessionDirectory.fileDirectory.getSizeOfFiles()
|
||||
cacheSize + sessionPaths.fileDirectory.getSizeOfFiles()
|
||||
} else {
|
||||
cacheSize + listOf(
|
||||
"matrix-sdk-state.sqlite3",
|
||||
"matrix-sdk-state.sqlite3-shm",
|
||||
"matrix-sdk-state.sqlite3-wal",
|
||||
).map { fileName ->
|
||||
File(sessionDirectory.fileDirectory, fileName)
|
||||
File(sessionPaths.fileDirectory, fileName)
|
||||
}.sumOf { file ->
|
||||
file.length()
|
||||
}
|
||||
@@ -843,7 +847,7 @@ class RustMatrixClient(
|
||||
|
||||
private suspend fun deleteSessionDirectory() = withContext(sessionDispatcher) {
|
||||
// Delete all the files for this session
|
||||
sessionPathsProvider.provides(sessionId)?.deleteRecursively()
|
||||
sessionPaths.deleteRecursively()
|
||||
}
|
||||
|
||||
private fun scheduleDatabaseVacuum() {
|
||||
|
||||
+4
-6
@@ -17,9 +17,8 @@ import io.element.android.libraries.di.CacheDirectory
|
||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.impl.analytics.UtdTracker
|
||||
import io.element.android.libraries.matrix.impl.certificates.UserCertificatesProvider
|
||||
import io.element.android.libraries.matrix.impl.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.impl.paths.getSessionPaths
|
||||
import io.element.android.libraries.matrix.impl.proxy.ProxyProvider
|
||||
import io.element.android.libraries.matrix.impl.room.TimelineEventFilterFactory
|
||||
@@ -60,7 +59,6 @@ class RustMatrixClientFactory(
|
||||
private val sessionStore: SessionStore,
|
||||
private val userAgentProvider: UserAgentProvider,
|
||||
private val proxyProvider: ProxyProvider,
|
||||
private val userCertificatesProvider: UserCertificatesProvider,
|
||||
private val clock: SystemClock,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
@@ -102,10 +100,10 @@ class RustMatrixClientFactory(
|
||||
|
||||
client.restoreSession(sessionData.toSession())
|
||||
|
||||
create(client)
|
||||
create(client, sessionData)
|
||||
}
|
||||
|
||||
suspend fun create(client: Client): RustMatrixClient {
|
||||
suspend fun create(client: Client, sessionData: SessionData): RustMatrixClient {
|
||||
val (anonymizedAccessToken, anonymizedRefreshToken) = client.session().anonymizedTokens()
|
||||
|
||||
// Must be called before creating the sync service, timelines etc.
|
||||
@@ -121,6 +119,7 @@ class RustMatrixClientFactory(
|
||||
.finish()
|
||||
|
||||
return RustMatrixClient(
|
||||
sessionPaths = sessionData.getSessionPaths(),
|
||||
innerClient = client,
|
||||
sessionStore = sessionStore,
|
||||
appCoroutineScope = appCoroutineScope,
|
||||
@@ -151,7 +150,6 @@ class RustMatrixClientFactory(
|
||||
}
|
||||
.setSessionDelegate(sessionDelegate)
|
||||
.userAgent(userAgentProvider.provide())
|
||||
.addRootCertificates(userCertificatesProvider.provides())
|
||||
.autoEnableBackups(true)
|
||||
.autoEnableCrossSigning(true)
|
||||
.roomKeyRecipientStrategy(
|
||||
|
||||
+37
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
@@ -9,19 +9,53 @@ package io.element.android.libraries.matrix.impl.analytics
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.core.data.bytes
|
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.matrix.api.analytics.GetDatabaseSizesUseCase
|
||||
import io.element.android.libraries.matrix.api.analytics.SdkStoreSizes
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import java.io.File
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* An implementation of [GetDatabaseSizesUseCase] that manually calculates the database sizes by checking the file system.
|
||||
* This is not as accurate as the SDK's internal method, but is used as a fallback for Sentry's non-suspendable hook where
|
||||
* calling the SDK's suspendable method blocks the main thread and causes ANRs.
|
||||
*/
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultGetDatabaseSizesUseCase(
|
||||
private val clientProvider: Lazy<MatrixClientProvider>,
|
||||
) : GetDatabaseSizesUseCase {
|
||||
override suspend fun invoke(sessionId: SessionId): Result<SdkStoreSizes> {
|
||||
override fun invoke(sessionId: SessionId): Result<SdkStoreSizes> {
|
||||
val client = clientProvider.value.getOrNull(sessionId)
|
||||
?: return Result.failure(IllegalArgumentException("No MatrixClient for session $sessionId"))
|
||||
|
||||
return client.getDatabaseSizes()
|
||||
val fileDir = client.sessionPaths.fileDirectory
|
||||
val cacheDir = client.sessionPaths.cacheDirectory
|
||||
|
||||
val cryptoSize = getDatabaseSize(fileDir, "matrix-sdk-crypto")
|
||||
val stateSize = getDatabaseSize(fileDir, "matrix-sdk-state")
|
||||
|
||||
val eventCacheSize = getDatabaseSize(cacheDir, "matrix-sdk-event-cache")
|
||||
val mediaCacheSize = getDatabaseSize(cacheDir, "matrix-sdk-media")
|
||||
|
||||
return Result.success(SdkStoreSizes(
|
||||
stateStore = stateSize.bytes,
|
||||
cryptoStore = cryptoSize.bytes,
|
||||
eventCacheStore = eventCacheSize.bytes,
|
||||
mediaStore = mediaCacheSize.bytes,
|
||||
))
|
||||
}
|
||||
|
||||
private fun getDatabaseSize(baseDirectory: File, databaseFileName: String): Long {
|
||||
// Check the sizes of both the main database file and the -wal file, and return the max of the two:
|
||||
// The -wal file can contain additional data not yet merged into the main file, becoming larger than it.
|
||||
val databaseFile = File(baseDirectory, "$databaseFileName.sqlite3")
|
||||
val walFile = File(baseDirectory, "$databaseFileName.sqlite3-wal")
|
||||
|
||||
val databaseSize = if (databaseFile.exists()) databaseFile.length() else 0L
|
||||
val walSize = if (walFile.exists()) walFile.length() else 0L
|
||||
|
||||
return max(databaseSize, walSize)
|
||||
}
|
||||
}
|
||||
|
||||
-3
@@ -13,19 +13,16 @@ import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker
|
||||
import io.element.android.libraries.matrix.impl.ClientBuilderProvider
|
||||
import io.element.android.libraries.matrix.impl.certificates.UserCertificatesProvider
|
||||
import timber.log.Timber
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class RustHomeServerLoginCompatibilityChecker(
|
||||
private val clientBuilderProvider: ClientBuilderProvider,
|
||||
private val userCertificatesProvider: UserCertificatesProvider,
|
||||
) : HomeServerLoginCompatibilityChecker {
|
||||
override suspend fun check(url: String): Result<Boolean> = runCatchingExceptions {
|
||||
clientBuilderProvider.provide()
|
||||
.inMemoryStore()
|
||||
.serverNameOrHomeserverUrl(url)
|
||||
.addRootCertificates(userCertificatesProvider.provides())
|
||||
.build()
|
||||
.use {
|
||||
it.homeserverLoginDetails()
|
||||
|
||||
+5
-5
@@ -29,6 +29,7 @@ import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginDat
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||
import io.element.android.libraries.matrix.impl.ClientBuilderSlidingSync
|
||||
import io.element.android.libraries.matrix.impl.RustMatrixClientFactory
|
||||
@@ -38,7 +39,6 @@ import io.element.android.libraries.matrix.impl.auth.qrlogin.toStep
|
||||
import io.element.android.libraries.matrix.impl.exception.mapClientException
|
||||
import io.element.android.libraries.matrix.impl.keys.SecretGenerator
|
||||
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.matrix.impl.toSession
|
||||
import io.element.android.libraries.sessionstorage.api.LoginType
|
||||
@@ -160,7 +160,7 @@ class RustMatrixAuthenticationService(
|
||||
passphrase = pendingKey.formattedAsString(),
|
||||
sessionPaths = currentSessionPaths,
|
||||
)
|
||||
val matrixClient = rustMatrixClientFactory.create(client)
|
||||
val matrixClient = rustMatrixClientFactory.create(client, sessionData)
|
||||
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
|
||||
sessionStore.addSession(sessionData)
|
||||
|
||||
@@ -235,7 +235,7 @@ class RustMatrixAuthenticationService(
|
||||
|
||||
// We restore the client using the just retrieved session data
|
||||
client.restoreSession(sessionData.toSession())
|
||||
val matrixClient = rustMatrixClientFactory.create(client)
|
||||
val matrixClient = rustMatrixClientFactory.create(client, sessionData)
|
||||
|
||||
// We wait for the verification state to be known
|
||||
matrixClient.waitForKnownVerificationState()
|
||||
@@ -325,7 +325,7 @@ class RustMatrixAuthenticationService(
|
||||
passphrase = pendingKey.formattedAsString(),
|
||||
sessionPaths = currentSessionPaths,
|
||||
)
|
||||
val matrixClient = rustMatrixClientFactory.create(client)
|
||||
val matrixClient = rustMatrixClientFactory.create(client, sessionData)
|
||||
matrixClient.waitForKnownVerificationState()
|
||||
|
||||
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
|
||||
@@ -390,7 +390,7 @@ class RustMatrixAuthenticationService(
|
||||
passphrase = pendingKey.formattedAsString(),
|
||||
sessionPaths = emptySessionPaths,
|
||||
)
|
||||
val matrixClient = rustMatrixClientFactory.create(client)
|
||||
val matrixClient = rustMatrixClientFactory.create(client, sessionData)
|
||||
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
|
||||
sessionStore.addSession(sessionData)
|
||||
|
||||
|
||||
-76
@@ -1,76 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations 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.certificates
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import timber.log.Timber
|
||||
import java.security.KeyStore
|
||||
import java.security.KeyStoreException
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultUserCertificatesProvider : UserCertificatesProvider {
|
||||
/**
|
||||
* Get additional user-installed certificates from the `AndroidCAStore` `Keystore`.
|
||||
*
|
||||
* The Rust HTTP client doesn't include user-installed certificates in its internal certificate
|
||||
* store. This means that whatever the user installs will be ignored.
|
||||
*
|
||||
* While most users don't need user-installed certificates some special deployments or debugging
|
||||
* setups using a proxy might want to use them.
|
||||
*
|
||||
* @return A list of byte arrays where each byte array is a single user-installed certificate
|
||||
* in encoded form.
|
||||
*/
|
||||
override fun provides(): List<ByteArray> {
|
||||
// At least for API 34 the `AndroidCAStore` `Keystore` type contained user certificates as well.
|
||||
// I have not found this to be documented anywhere.
|
||||
val keyStore: KeyStore = try {
|
||||
KeyStore.getInstance("AndroidCAStore")
|
||||
} catch (e: KeyStoreException) {
|
||||
Timber.w(e, "Failed to get AndroidCAStore keystore")
|
||||
return emptyList()
|
||||
}
|
||||
val aliases = try {
|
||||
keyStore.load(null)
|
||||
keyStore.aliases()
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e, "Failed to load and get aliases AndroidCAStore keystore")
|
||||
return emptyList()
|
||||
}
|
||||
return aliases.toList()
|
||||
.filter { alias ->
|
||||
// The certificate alias always contains the prefix `system` or
|
||||
// `user` and the MD5 subject hash separated by a colon.
|
||||
//
|
||||
// The subject hash can be calculated using openssl as such:
|
||||
// openssl x509 -subject_hash_old -noout -in mycert.cer
|
||||
//
|
||||
// Again, I have not found this to be documented somewhere.
|
||||
alias.startsWith("user")
|
||||
}
|
||||
.mapNotNull { alias ->
|
||||
try {
|
||||
keyStore.getEntry(alias, null)
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e, "Failed to get entry for alias $alias")
|
||||
null
|
||||
}
|
||||
}
|
||||
.filterIsInstance<KeyStore.TrustedCertificateEntry>()
|
||||
.map { trustedCertificateEntry ->
|
||||
trustedCertificateEntry.trustedCertificate.encoded
|
||||
}
|
||||
.also {
|
||||
// Let's at least log the number of user-installed certificates we found,
|
||||
// since the alias isn't particularly useful nor does the issuer seem to
|
||||
// be easily available.
|
||||
Timber.i("Found ${it.size} additional user-provided certificates.")
|
||||
}
|
||||
}
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations 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.certificates
|
||||
|
||||
interface UserCertificatesProvider {
|
||||
fun provides(): List<ByteArray>
|
||||
}
|
||||
+14
-5
@@ -131,19 +131,28 @@ class RustEncryptionService(
|
||||
|
||||
override suspend fun enableRecovery(
|
||||
waitForBackupsToUpload: Boolean,
|
||||
): Result<Unit> = withContext(dispatchers.io) {
|
||||
passphrase: String?,
|
||||
): Result<String> = withContext(dispatchers.io) {
|
||||
runCatchingExceptions {
|
||||
service.enableRecovery(
|
||||
// The key arrives as the suspend return value (like resetRecoveryKey), avoiding a
|
||||
// flow/return-value race; the listener only feeds sub-progress.
|
||||
enableRecoveryProgressStateFlow.value = EnableRecoveryProgress.Starting
|
||||
val key = service.enableRecovery(
|
||||
waitForBackupsToUpload = waitForBackupsToUpload,
|
||||
progressListener = object : EnableRecoveryProgressListener {
|
||||
override fun onUpdate(status: RustEnableRecoveryProgress) {
|
||||
enableRecoveryProgressStateFlow.value = enableRecoveryProgressMapper.map(status)
|
||||
}
|
||||
},
|
||||
passphrase = null,
|
||||
passphrase = passphrase,
|
||||
)
|
||||
// enableRecovery returns the encryption key, but we read it from the state flow
|
||||
.let { }
|
||||
// Pin Done explicitly so observers get a coherent terminal value. For the passphrase
|
||||
// path the user never sees the SDK base58 key, so keep it out of this session-scoped
|
||||
// (in-memory) flow entirely; the caller still receives it as the return value and is
|
||||
// responsible for scrubbing it. The auto-generated path must retain the key here since
|
||||
// that is how it is surfaced to the user.
|
||||
enableRecoveryProgressStateFlow.value = EnableRecoveryProgress.Done(if (passphrase != null) "" else key)
|
||||
key
|
||||
}.mapFailure {
|
||||
it.mapRecoveryException()
|
||||
}
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
package io.element.android.libraries.matrix.impl.mapper
|
||||
|
||||
import io.element.android.libraries.matrix.api.auth.external.ExternalSession
|
||||
import io.element.android.libraries.matrix.impl.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||
import io.element.android.libraries.sessionstorage.api.LoginType
|
||||
import io.element.android.libraries.sessionstorage.api.SessionData
|
||||
import org.matrix.rustcomponents.sdk.Session
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations 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.media
|
||||
|
||||
import io.element.android.libraries.matrix.api.media.GalleryItemInfo
|
||||
import org.matrix.rustcomponents.sdk.GalleryItemInfo as RustGalleryItemInfo
|
||||
import org.matrix.rustcomponents.sdk.UploadSource as RustUploadSource
|
||||
|
||||
fun GalleryItemInfo.map(): RustGalleryItemInfo = when (this) {
|
||||
is GalleryItemInfo.Image -> {
|
||||
RustGalleryItemInfo.Image(
|
||||
imageInfo = imageInfo.map(),
|
||||
source = RustUploadSource.File(file.path),
|
||||
caption = null,
|
||||
formattedCaption = null,
|
||||
thumbnailSource = thumbnailFile?.path?.let(RustUploadSource::File),
|
||||
)
|
||||
}
|
||||
is GalleryItemInfo.Video -> {
|
||||
RustGalleryItemInfo.Video(
|
||||
videoInfo = videoInfo.map(),
|
||||
source = RustUploadSource.File(file.path),
|
||||
caption = null,
|
||||
formattedCaption = null,
|
||||
thumbnailSource = thumbnailFile?.path?.let(RustUploadSource::File),
|
||||
)
|
||||
}
|
||||
is GalleryItemInfo.Audio -> {
|
||||
RustGalleryItemInfo.Audio(
|
||||
audioInfo = audioInfo.map(),
|
||||
source = RustUploadSource.File(file.path),
|
||||
caption = null,
|
||||
formattedCaption = null,
|
||||
)
|
||||
}
|
||||
is GalleryItemInfo.MediaFile -> {
|
||||
RustGalleryItemInfo.File(
|
||||
fileInfo = fileInfo.map(),
|
||||
source = RustUploadSource.File(file.path),
|
||||
caption = null,
|
||||
formattedCaption = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations 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.media
|
||||
|
||||
import io.element.android.libraries.androidutils.file.safeDelete
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
|
||||
import org.matrix.rustcomponents.sdk.SendGalleryJoinHandle
|
||||
import java.io.File
|
||||
|
||||
class GalleryMediaUploadHandlerImpl(
|
||||
private val filesToUpload: List<File>,
|
||||
private val sendGalleryJoinHandle: SendGalleryJoinHandle,
|
||||
) : MediaUploadHandler {
|
||||
override suspend fun await(): Result<Unit> =
|
||||
runCatchingExceptions {
|
||||
sendGalleryJoinHandle.join()
|
||||
}
|
||||
.also { cleanUpFiles() }
|
||||
|
||||
override fun cancel() {
|
||||
sendGalleryJoinHandle.cancel()
|
||||
cleanUpFiles()
|
||||
}
|
||||
|
||||
private fun cleanUpFiles() {
|
||||
filesToUpload.forEach { file -> file.safeDelete() }
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -97,7 +97,10 @@ private fun MessageLikeEventContent.toContent(senderId: UserId): NotificationCon
|
||||
is MessageLikeEventContent.ReactionContent -> NotificationContent.MessageLike.ReactionContent(relatedEventId)
|
||||
MessageLikeEventContent.RoomEncrypted -> NotificationContent.MessageLike.RoomEncrypted
|
||||
is MessageLikeEventContent.RoomMessage -> {
|
||||
NotificationContent.MessageLike.RoomMessage(senderId, EventMessageMapper().mapMessageType(messageType))
|
||||
NotificationContent.MessageLike.RoomMessage(
|
||||
senderId,
|
||||
EventMessageMapper().mapMessageType(messageType)
|
||||
)
|
||||
}
|
||||
is MessageLikeEventContent.RoomRedaction -> NotificationContent.MessageLike.RoomRedaction(
|
||||
redactedEventId = redactedEventId?.let(::EventId),
|
||||
|
||||
+1
-10
@@ -8,19 +8,10 @@
|
||||
|
||||
package io.element.android.libraries.matrix.impl.paths
|
||||
|
||||
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||
import io.element.android.libraries.sessionstorage.api.SessionData
|
||||
import java.io.File
|
||||
|
||||
data class SessionPaths(
|
||||
val fileDirectory: File,
|
||||
val cacheDirectory: File,
|
||||
) {
|
||||
fun deleteRecursively() {
|
||||
fileDirectory.deleteRecursively()
|
||||
cacheDirectory.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun SessionData.getSessionPaths(): SessionPaths {
|
||||
return SessionPaths(
|
||||
fileDirectory = File(sessionPath),
|
||||
|
||||
+1
@@ -11,6 +11,7 @@ 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 io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
|
||||
|
||||
+9
-1
@@ -8,6 +8,7 @@
|
||||
|
||||
package io.element.android.libraries.matrix.impl.room
|
||||
|
||||
import io.element.android.appconfig.TimelineConfig
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.coroutine.childScope
|
||||
import io.element.android.libraries.core.extensions.mapFailure
|
||||
@@ -218,11 +219,18 @@ class JoinedRustRoom(
|
||||
RoomMessageEventMessageType.IMAGE,
|
||||
RoomMessageEventMessageType.VIDEO,
|
||||
RoomMessageEventMessageType.AUDIO,
|
||||
RoomMessageEventMessageType.GALLERY,
|
||||
)
|
||||
)
|
||||
is CreateTimelineParams.Focused,
|
||||
CreateTimelineParams.PinnedOnly,
|
||||
is CreateTimelineParams.Threaded -> TimelineFilter.All
|
||||
is CreateTimelineParams.Threaded -> {
|
||||
RustTimelineEventFilterFactory().create(
|
||||
joinRule = roomInfoFlow.value.joinRule,
|
||||
isEncrypted = roomInfoFlow.value.isEncrypted,
|
||||
excludedStateTypes = TimelineConfig.excludedEvents,
|
||||
)?.let(TimelineFilter::EventFilter) ?: TimelineFilter.All
|
||||
}
|
||||
}
|
||||
|
||||
val internalIdPrefix = when (createTimelineParams) {
|
||||
|
||||
+4
-3
@@ -9,7 +9,8 @@
|
||||
package io.element.android.libraries.matrix.impl.room
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
||||
import org.matrix.rustcomponents.sdk.MessageLikeEventType
|
||||
import uniffi.ruma_events.MessageLikeEventType
|
||||
import uniffi.ruma_events.messageLikeEventTypeFromString
|
||||
|
||||
fun MessageEventType.map(): MessageLikeEventType = when (this) {
|
||||
MessageEventType.Audio -> MessageLikeEventType.Audio
|
||||
@@ -51,7 +52,7 @@ fun MessageEventType.map(): MessageLikeEventType = when (this) {
|
||||
MessageEventType.UnstablePollStart -> MessageLikeEventType.UnstablePollStart
|
||||
MessageEventType.Video -> MessageLikeEventType.Video
|
||||
MessageEventType.Voice -> MessageLikeEventType.Voice
|
||||
is MessageEventType.Other -> MessageLikeEventType.Other(type)
|
||||
is MessageEventType.Other -> messageLikeEventTypeFromString(type)
|
||||
}
|
||||
|
||||
fun MessageLikeEventType.map(): MessageEventType = when (this) {
|
||||
@@ -94,5 +95,5 @@ fun MessageLikeEventType.map(): MessageEventType = when (this) {
|
||||
MessageLikeEventType.RtcDecline -> MessageEventType.RtcDecline
|
||||
MessageLikeEventType.Video -> MessageEventType.Video
|
||||
MessageLikeEventType.Voice -> MessageEventType.Voice
|
||||
is MessageLikeEventType.Other -> MessageEventType.Other(v1)
|
||||
is MessageLikeEventType.Custom -> MessageEventType.Other(type = toString())
|
||||
}
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ class RoomContentForwarder(
|
||||
val content = (messageLikeContent.kind as? MsgLikeKind.Message)?.content
|
||||
?: throw ForwardEventException(toRoomIds)
|
||||
|
||||
val targetRooms = toRoomIds.mapNotNull { roomId -> roomListService.roomOrNull(roomId.value) }
|
||||
val targetRooms = toRoomIds.toSet().mapNotNull { roomId -> roomListService.roomOrNull(roomId.value) }
|
||||
val failedForwardingTo = mutableSetOf<RoomId>()
|
||||
targetRooms.parallelMap { room ->
|
||||
room.use { targetRoom ->
|
||||
|
||||
-6
@@ -200,12 +200,6 @@ class RustBaseRoom(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun clearEventCacheStorage(): Result<Unit> = withContext(roomDispatcher) {
|
||||
runCatchingExceptions {
|
||||
innerRoom.clearEventCacheStorage()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setIsFavorite(isFavorite: Boolean): Result<Unit> = withContext(roomDispatcher) {
|
||||
runCatchingExceptions {
|
||||
innerRoom.setIsFavourite(isFavorite, null)
|
||||
|
||||
+13
-7
@@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.matrix.api.roomlist.awaitLoaded
|
||||
import io.element.android.libraries.matrix.impl.room.join.map
|
||||
import io.element.android.libraries.matrix.impl.room.preview.RoomPreviewInfoMapper
|
||||
import io.element.android.libraries.matrix.impl.roomlist.roomOrNull
|
||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction
|
||||
@@ -41,6 +42,7 @@ import org.matrix.rustcomponents.sdk.TimelineConfiguration
|
||||
import org.matrix.rustcomponents.sdk.TimelineFilter
|
||||
import org.matrix.rustcomponents.sdk.TimelineFocus
|
||||
import timber.log.Timber
|
||||
import uniffi.matrix_sdk_base.EncryptionState
|
||||
import uniffi.matrix_sdk_ui.TimelineReadReceiptTracking
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import org.matrix.rustcomponents.sdk.RoomListService as InnerRoomListService
|
||||
@@ -66,12 +68,6 @@ class RustRoomFactory(
|
||||
private val mutex = Mutex()
|
||||
private val isDestroyed: AtomicBoolean = AtomicBoolean(false)
|
||||
|
||||
private val eventFilters = TimelineConfig.excludedEvents
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.let { listStateEventType ->
|
||||
timelineEventFilterFactory.create(listStateEventType)
|
||||
}
|
||||
|
||||
suspend fun destroy() {
|
||||
withContext(NonCancellable + dispatcher) {
|
||||
mutex.withLock {
|
||||
@@ -128,10 +124,20 @@ class RustRoomFactory(
|
||||
operation = "sdkRoom.timelineWithConfiguration",
|
||||
description = "Get timeline from the SDK",
|
||||
) {
|
||||
val isEncrypted = when (roomInfo.encryptionState) {
|
||||
EncryptionState.ENCRYPTED -> true
|
||||
EncryptionState.NOT_ENCRYPTED -> false
|
||||
EncryptionState.UNKNOWN -> null
|
||||
}
|
||||
val timelineFilter = timelineEventFilterFactory.create(
|
||||
joinRule = roomInfo.joinRule?.map(),
|
||||
isEncrypted = isEncrypted,
|
||||
excludedStateTypes = TimelineConfig.excludedEvents,
|
||||
)
|
||||
sdkRoom.timelineWithConfiguration(
|
||||
TimelineConfiguration(
|
||||
focus = TimelineFocus.Live(hideThreadedEvents = hideThreadedEvents),
|
||||
filter = eventFilters?.let(TimelineFilter::EventFilter) ?: TimelineFilter.All,
|
||||
filter = timelineFilter?.let(TimelineFilter::EventFilter) ?: TimelineFilter.All,
|
||||
internalIdPrefix = "live",
|
||||
dateDividerMode = DateDividerMode.DAILY,
|
||||
trackReadReceipts = TimelineReadReceiptTracking.ALL_EVENTS,
|
||||
|
||||
+8
-5
@@ -9,7 +9,8 @@
|
||||
package io.element.android.libraries.matrix.impl.room
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.StateEventType
|
||||
import org.matrix.rustcomponents.sdk.StateEventType as RustStateEventType
|
||||
import uniffi.ruma_events.stateEventTypeFromString
|
||||
import uniffi.ruma_events.StateEventType as RustStateEventType
|
||||
|
||||
fun StateEventType.map(): RustStateEventType = when (this) {
|
||||
StateEventType.PolicyRuleRoom -> RustStateEventType.PolicyRuleRoom
|
||||
@@ -23,7 +24,7 @@ fun StateEventType.map(): RustStateEventType = when (this) {
|
||||
StateEventType.RoomGuestAccess -> RustStateEventType.RoomGuestAccess
|
||||
StateEventType.RoomHistoryVisibility -> RustStateEventType.RoomHistoryVisibility
|
||||
StateEventType.RoomJoinRules -> RustStateEventType.RoomJoinRules
|
||||
StateEventType.RoomMemberEvent -> RustStateEventType.RoomMemberEvent
|
||||
StateEventType.RoomMemberEvent -> RustStateEventType.RoomMember
|
||||
StateEventType.RoomName -> RustStateEventType.RoomName
|
||||
StateEventType.RoomPinnedEvents -> RustStateEventType.RoomPinnedEvents
|
||||
StateEventType.RoomPowerLevels -> RustStateEventType.RoomPowerLevels
|
||||
@@ -37,7 +38,8 @@ fun StateEventType.map(): RustStateEventType = when (this) {
|
||||
StateEventType.MemberHints -> RustStateEventType.MemberHints
|
||||
StateEventType.RoomImagePack -> RustStateEventType.RoomImagePack
|
||||
StateEventType.RoomLanguage -> RustStateEventType.RoomLanguage
|
||||
is StateEventType.Custom -> RustStateEventType.Custom(type)
|
||||
StateEventType.RoomPolicy -> RustStateEventType.RoomPolicy
|
||||
is StateEventType.Custom -> stateEventTypeFromString(type)
|
||||
}
|
||||
|
||||
fun RustStateEventType.map(): StateEventType = when (this) {
|
||||
@@ -52,7 +54,7 @@ fun RustStateEventType.map(): StateEventType = when (this) {
|
||||
RustStateEventType.RoomGuestAccess -> StateEventType.RoomGuestAccess
|
||||
RustStateEventType.RoomHistoryVisibility -> StateEventType.RoomHistoryVisibility
|
||||
RustStateEventType.RoomJoinRules -> StateEventType.RoomJoinRules
|
||||
RustStateEventType.RoomMemberEvent -> StateEventType.RoomMemberEvent
|
||||
RustStateEventType.RoomMember -> StateEventType.RoomMemberEvent
|
||||
RustStateEventType.RoomName -> StateEventType.RoomName
|
||||
RustStateEventType.RoomPinnedEvents -> StateEventType.RoomPinnedEvents
|
||||
RustStateEventType.RoomPowerLevels -> StateEventType.RoomPowerLevels
|
||||
@@ -66,5 +68,6 @@ fun RustStateEventType.map(): StateEventType = when (this) {
|
||||
RustStateEventType.MemberHints -> StateEventType.MemberHints
|
||||
RustStateEventType.RoomImagePack -> StateEventType.RoomImagePack
|
||||
RustStateEventType.RoomLanguage -> StateEventType.RoomLanguage
|
||||
is RustStateEventType.Custom -> StateEventType.Custom(value)
|
||||
RustStateEventType.RoomPolicy -> StateEventType.RoomPolicy
|
||||
is RustStateEventType.Custom -> StateEventType.Custom(this.toString())
|
||||
}
|
||||
|
||||
+32
-7
@@ -11,20 +11,45 @@ package io.element.android.libraries.matrix.impl.room
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.matrix.api.room.StateEventType
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import org.matrix.rustcomponents.sdk.FilterTimelineEventCondition
|
||||
import org.matrix.rustcomponents.sdk.FilterTimelineEventType
|
||||
import org.matrix.rustcomponents.sdk.TimelineEventFilter
|
||||
import uniffi.matrix_sdk_ui.MembershipChangeFilter
|
||||
|
||||
interface TimelineEventFilterFactory {
|
||||
fun create(listStateEventType: List<StateEventType>): TimelineEventFilter
|
||||
fun create(
|
||||
joinRule: JoinRule?,
|
||||
isEncrypted: Boolean?,
|
||||
excludedStateTypes: List<StateEventType>
|
||||
): TimelineEventFilter?
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class RustTimelineEventFilterFactory : TimelineEventFilterFactory {
|
||||
override fun create(listStateEventType: List<StateEventType>): TimelineEventFilter {
|
||||
return TimelineEventFilter.excludeEventTypes(
|
||||
listStateEventType.map { stateEventType ->
|
||||
FilterTimelineEventType.State(stateEventType.map())
|
||||
}
|
||||
)
|
||||
override fun create(
|
||||
joinRule: JoinRule?,
|
||||
isEncrypted: Boolean?,
|
||||
excludedStateTypes: List<StateEventType>
|
||||
): TimelineEventFilter? {
|
||||
val excludedEventTypes = excludedStateTypes.map {
|
||||
FilterTimelineEventCondition.EventType(FilterTimelineEventType.State(it.map()))
|
||||
}
|
||||
// If the room is publicly joinable and not encrypted, we also want to exclude membership changes and profile changes,
|
||||
// as they will pollute the timelines since they're quite common and not add much value.
|
||||
val excludedMembershipChanges = if (joinRule !is JoinRule.Invite && isEncrypted == false) {
|
||||
listOf(
|
||||
FilterTimelineEventCondition.MembershipChange(MembershipChangeFilter.JOIN),
|
||||
FilterTimelineEventCondition.MembershipChange(MembershipChangeFilter.LEAVE),
|
||||
FilterTimelineEventCondition.ProfileChange,
|
||||
)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
return if (excludedEventTypes.isNotEmpty() || excludedMembershipChanges.isNotEmpty()) {
|
||||
TimelineEventFilter.exclude(excludedEventTypes + excludedMembershipChanges)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ package io.element.android.libraries.matrix.impl.storage
|
||||
import io.element.android.libraries.androidutils.crypto.ClientSecret
|
||||
import io.element.android.libraries.core.data.ByteUnit
|
||||
import io.element.android.libraries.core.data.megaBytes
|
||||
import io.element.android.libraries.matrix.impl.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||
import org.matrix.rustcomponents.sdk.ClientBuilder
|
||||
import org.matrix.rustcomponents.sdk.SqliteStoreBuilder as SdkSqliteStoreBuilder
|
||||
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ package io.element.android.libraries.matrix.impl.storage
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.matrix.impl.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||
|
||||
interface SqliteStoreBuilderProvider {
|
||||
fun provide(sessionPaths: SessionPaths): SqliteStoreBuilder
|
||||
|
||||
+39
-16
@@ -15,6 +15,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.media.AudioInfo
|
||||
import io.element.android.libraries.matrix.api.media.FileInfo
|
||||
import io.element.android.libraries.matrix.api.media.GalleryItemInfo
|
||||
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
|
||||
import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||
@@ -30,6 +31,7 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.timeline.TimelineException
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
|
||||
import io.element.android.libraries.matrix.impl.media.GalleryMediaUploadHandlerImpl
|
||||
import io.element.android.libraries.matrix.impl.media.MediaUploadHandlerImpl
|
||||
import io.element.android.libraries.matrix.impl.media.map
|
||||
import io.element.android.libraries.matrix.impl.poll.toInner
|
||||
@@ -38,8 +40,6 @@ import io.element.android.libraries.matrix.impl.room.location.into
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper
|
||||
import io.element.android.libraries.matrix.impl.timeline.postprocessor.FilterEmptyDayPostProcessor
|
||||
import io.element.android.libraries.matrix.impl.timeline.postprocessor.FilterPublicMembershipChangesPostProcessor
|
||||
import io.element.android.libraries.matrix.impl.timeline.postprocessor.LastForwardIndicatorsPostProcessor
|
||||
import io.element.android.libraries.matrix.impl.timeline.postprocessor.LoadingIndicatorsPostProcessor
|
||||
import io.element.android.libraries.matrix.impl.timeline.postprocessor.RoomBeginningPostProcessor
|
||||
@@ -69,6 +69,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.EditedContent
|
||||
import org.matrix.rustcomponents.sdk.FormattedBody
|
||||
import org.matrix.rustcomponents.sdk.GalleryUploadParameters
|
||||
import org.matrix.rustcomponents.sdk.MessageFormat
|
||||
import org.matrix.rustcomponents.sdk.PollData
|
||||
import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle
|
||||
@@ -125,8 +126,6 @@ class RustTimeline(
|
||||
private val loadingIndicatorsPostProcessor = LoadingIndicatorsPostProcessor(systemClock)
|
||||
private val lastForwardIndicatorsPostProcessor = LastForwardIndicatorsPostProcessor(mode)
|
||||
private val typingNotificationPostProcessor = TypingNotificationPostProcessor(mode)
|
||||
private val publicMembershipChangesPostProcessor = FilterPublicMembershipChangesPostProcessor()
|
||||
private val emptyDayPostProcessor = FilterEmptyDayPostProcessor()
|
||||
|
||||
private data class RoomTimelineInfo(
|
||||
val roomCreators: ImmutableList<UserId>,
|
||||
@@ -252,18 +251,6 @@ class RustTimeline(
|
||||
hasMoreToLoadBackwards = backwardPaginationStatus.hasMoreToLoad,
|
||||
)
|
||||
}
|
||||
// This should be the first post processor after room beginning.
|
||||
.let { items ->
|
||||
publicMembershipChangesPostProcessor.process(
|
||||
items = items,
|
||||
joinRule = joinRule,
|
||||
isEncrypted = isEncrypted,
|
||||
)
|
||||
}
|
||||
// After removing public membership changes, we might end up with empty days, so we need to filter them out.
|
||||
.let { items ->
|
||||
emptyDayPostProcessor.process(items)
|
||||
}
|
||||
.let { items ->
|
||||
loadingIndicatorsPostProcessor.process(
|
||||
items = items,
|
||||
@@ -551,6 +538,42 @@ class RustTimeline(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sendGallery(
|
||||
items: List<GalleryItemInfo>,
|
||||
caption: String?,
|
||||
formattedCaption: String?,
|
||||
inReplyToEventId: EventId?,
|
||||
): Result<MediaUploadHandler> {
|
||||
Timber.d("Sending gallery with ${items.size} items")
|
||||
val allFiles = items.flatMap { item ->
|
||||
when (item) {
|
||||
is GalleryItemInfo.Image -> listOfNotNull(item.file, item.thumbnailFile)
|
||||
is GalleryItemInfo.Video -> listOfNotNull(item.file, item.thumbnailFile)
|
||||
is GalleryItemInfo.Audio -> listOf(item.file)
|
||||
is GalleryItemInfo.MediaFile -> listOf(item.file)
|
||||
}
|
||||
}
|
||||
return sendGalleryAttachment(allFiles) {
|
||||
inner.sendGallery(
|
||||
params = GalleryUploadParameters(
|
||||
caption = caption,
|
||||
formattedCaption = formattedCaption?.let {
|
||||
FormattedBody(body = it, format = MessageFormat.Html)
|
||||
},
|
||||
mentions = null,
|
||||
inReplyTo = inReplyToEventId?.value,
|
||||
),
|
||||
itemInfos = items.map { it.map() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendGalleryAttachment(files: List<File>, handle: () -> org.matrix.rustcomponents.sdk.SendGalleryJoinHandle): Result<MediaUploadHandler> {
|
||||
return runCatchingExceptions {
|
||||
GalleryMediaUploadHandlerImpl(files, handle())
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun createPoll(
|
||||
question: String,
|
||||
answers: List<String>,
|
||||
|
||||
+61
-5
@@ -13,6 +13,8 @@ import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageT
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.GalleryItemType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.GalleryMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType
|
||||
@@ -31,12 +33,10 @@ import org.matrix.rustcomponents.sdk.MessageType
|
||||
import org.matrix.rustcomponents.sdk.MsgLikeKind
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import org.matrix.rustcomponents.sdk.FormattedBody as RustFormattedBody
|
||||
import org.matrix.rustcomponents.sdk.GalleryItemType as RustGalleryItemType
|
||||
import org.matrix.rustcomponents.sdk.MessageFormat as RustMessageFormat
|
||||
import org.matrix.rustcomponents.sdk.MessageType as RustMessageType
|
||||
|
||||
// https://github.com/Johennes/matrix-spec-proposals/blob/johannes/msgtype-galleries/proposals/4274-inline-media-galleries.md#unstable-prefix
|
||||
private const val MSG_TYPE_GALLERY_UNSTABLE = "dm.filament.gallery"
|
||||
|
||||
class EventMessageMapper {
|
||||
private val inReplyToMapper by lazy { InReplyToMapper(TimelineEventContentMapper()) }
|
||||
|
||||
@@ -124,8 +124,64 @@ class EventMessageMapper {
|
||||
OtherMessageType(type.msgtype, type.body)
|
||||
}
|
||||
is MessageType.Gallery -> {
|
||||
// TODO expose the GalleryType.
|
||||
OtherMessageType(MSG_TYPE_GALLERY_UNSTABLE, type.content.body)
|
||||
GalleryMessageType(
|
||||
body = type.content.body,
|
||||
formatted = type.content.formatted?.map(),
|
||||
items = type.content.itemtypes.map { mapGalleryItemType(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapGalleryItemType(type: RustGalleryItemType): GalleryItemType = when (type) {
|
||||
is RustGalleryItemType.Image -> {
|
||||
GalleryItemType.Image(
|
||||
content = ImageMessageType(
|
||||
filename = type.content.filename,
|
||||
caption = type.content.caption,
|
||||
formattedCaption = type.content.formattedCaption?.map(),
|
||||
source = type.content.source.map(),
|
||||
info = type.content.info?.map(),
|
||||
)
|
||||
)
|
||||
}
|
||||
is RustGalleryItemType.Audio -> {
|
||||
GalleryItemType.Audio(
|
||||
content = AudioMessageType(
|
||||
filename = type.content.filename,
|
||||
caption = type.content.caption,
|
||||
formattedCaption = type.content.formattedCaption?.map(),
|
||||
source = type.content.source.map(),
|
||||
info = type.content.info?.map(),
|
||||
)
|
||||
)
|
||||
}
|
||||
is RustGalleryItemType.Video -> {
|
||||
GalleryItemType.Video(
|
||||
content = VideoMessageType(
|
||||
filename = type.content.filename,
|
||||
caption = type.content.caption,
|
||||
formattedCaption = type.content.formattedCaption?.map(),
|
||||
source = type.content.source.map(),
|
||||
info = type.content.info?.map(),
|
||||
)
|
||||
)
|
||||
}
|
||||
is RustGalleryItemType.File -> {
|
||||
GalleryItemType.File(
|
||||
content = FileMessageType(
|
||||
filename = type.content.filename,
|
||||
caption = type.content.caption,
|
||||
formattedCaption = type.content.formattedCaption?.map(),
|
||||
source = type.content.source.map(),
|
||||
info = type.content.info?.map(),
|
||||
)
|
||||
)
|
||||
}
|
||||
is RustGalleryItemType.Other -> {
|
||||
GalleryItemType.Other(
|
||||
itemType = type.itemtype,
|
||||
body = type.body,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations 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.timeline.postprocessor
|
||||
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||
|
||||
/**
|
||||
* Post-processor to filter out day separators for days that don't contain any events.
|
||||
*/
|
||||
class FilterEmptyDayPostProcessor {
|
||||
/**
|
||||
* Filters out day separators from [items] for days that don't contain any events.
|
||||
*/
|
||||
fun process(items: List<MatrixTimelineItem>): List<MatrixTimelineItem> = buildList {
|
||||
// The timeline is ordered by ascending timestamp, so events that happened during a day appear after the day separator for that day.
|
||||
// We can use this to determine if a day separator should be kept or not by traversing the list in reverse.
|
||||
var hasEvent = false
|
||||
for (item in items.asReversed()) {
|
||||
if (item is MatrixTimelineItem.Event) {
|
||||
hasEvent = true
|
||||
add(item)
|
||||
} else if (item is MatrixTimelineItem.Virtual && item.virtual is VirtualTimelineItem.DayDivider) {
|
||||
if (hasEvent) {
|
||||
add(item)
|
||||
hasEvent = false
|
||||
}
|
||||
} else {
|
||||
add(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Then reverse the result to restore the original order, minus the empty day separators.
|
||||
.asReversed()
|
||||
}
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations 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.timeline.postprocessor
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||
|
||||
/**
|
||||
* Post-processor to filter out public membership changes for non-encrypted, publicly joinable rooms.
|
||||
*/
|
||||
class FilterPublicMembershipChangesPostProcessor {
|
||||
/**
|
||||
* Filters out public membership changes from [items] if the room is publicly joinable and not encrypted.
|
||||
*/
|
||||
fun process(
|
||||
items: List<MatrixTimelineItem>,
|
||||
joinRule: JoinRule?,
|
||||
isEncrypted: Boolean?,
|
||||
): List<MatrixTimelineItem> {
|
||||
return if (joinRule !is JoinRule.Invite && isEncrypted == false) {
|
||||
filterMembershipEvents(items)
|
||||
} else {
|
||||
items
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterMembershipEvents(items: List<MatrixTimelineItem>): List<MatrixTimelineItem> = items.filter { item ->
|
||||
val eventContent = (item as? MatrixTimelineItem.Event)?.event?.content ?: return@filter true
|
||||
when (eventContent) {
|
||||
is RoomMembershipContent -> eventContent.change != null && eventContent.change !in listOf(MembershipChange.JOINED, MembershipChange.LEFT)
|
||||
is ProfileChangeContent -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 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.util
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.impl.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.impl.paths.getSessionPaths
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
|
||||
class SessionPathsProvider(
|
||||
private val sessionStore: SessionStore,
|
||||
) {
|
||||
suspend fun provides(sessionId: SessionId): SessionPaths? {
|
||||
val sessionData = sessionStore.getSession(sessionId.value) ?: return null
|
||||
return sessionData.getSessionPaths()
|
||||
}
|
||||
}
|
||||
-2
@@ -12,7 +12,6 @@ import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.impl.auth.FakeProxyProvider
|
||||
import io.element.android.libraries.matrix.impl.auth.FakeUserCertificatesProvider
|
||||
import io.element.android.libraries.matrix.impl.room.FakeTimelineEventFilterFactory
|
||||
import io.element.android.libraries.matrix.impl.storage.FakeSqliteStoreBuilderProvider
|
||||
import io.element.android.libraries.network.useragent.SimpleUserAgentProvider
|
||||
@@ -58,7 +57,6 @@ fun TestScope.createRustMatrixClientFactory(
|
||||
coroutineDispatchers = testCoroutineDispatchers(),
|
||||
sessionStore = sessionStore,
|
||||
userAgentProvider = SimpleUserAgentProvider(),
|
||||
userCertificatesProvider = FakeUserCertificatesProvider(),
|
||||
proxyProvider = FakeProxyProvider(),
|
||||
clock = FakeSystemClock(),
|
||||
analyticsService = FakeAnalyticsService(),
|
||||
|
||||
+2
@@ -13,6 +13,7 @@ package io.element.android.libraries.matrix.impl
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.core.data.bytes
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||
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.FakeTimelineEventFilterFactory
|
||||
@@ -141,6 +142,7 @@ class RustMatrixClientTest {
|
||||
),
|
||||
) = RustMatrixClient(
|
||||
innerClient = client,
|
||||
sessionPaths = SessionPaths(fileDirectory = File("files"), cacheDirectory = File("cache")),
|
||||
sessionStore = sessionStore,
|
||||
appCoroutineScope = backgroundScope,
|
||||
sessionDelegate = aRustClientSessionDelegate(
|
||||
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations 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.auth
|
||||
|
||||
import io.element.android.libraries.matrix.impl.certificates.UserCertificatesProvider
|
||||
|
||||
class FakeUserCertificatesProvider : UserCertificatesProvider {
|
||||
override fun provides(): List<ByteArray> {
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
-1
@@ -49,6 +49,5 @@ class RustHomeserverLoginCompatibilityCheckerTest {
|
||||
FakeFfiClient(homeserverLoginDetailsResult = result)
|
||||
}
|
||||
},
|
||||
userCertificatesProvider = FakeUserCertificatesProvider(),
|
||||
)
|
||||
}
|
||||
|
||||
+105
@@ -8,12 +8,26 @@
|
||||
|
||||
package io.element.android.libraries.matrix.impl.fixtures.factories
|
||||
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiMediaSource
|
||||
import io.element.android.libraries.matrix.test.A_MESSAGE
|
||||
import org.matrix.rustcomponents.sdk.AudioInfo
|
||||
import org.matrix.rustcomponents.sdk.AudioMessageContent
|
||||
import org.matrix.rustcomponents.sdk.FileInfo
|
||||
import org.matrix.rustcomponents.sdk.FileMessageContent
|
||||
import org.matrix.rustcomponents.sdk.FormattedBody
|
||||
import org.matrix.rustcomponents.sdk.GalleryItemType
|
||||
import org.matrix.rustcomponents.sdk.GalleryMessageContent
|
||||
import org.matrix.rustcomponents.sdk.ImageInfo
|
||||
import org.matrix.rustcomponents.sdk.ImageMessageContent
|
||||
import org.matrix.rustcomponents.sdk.MediaSource
|
||||
import org.matrix.rustcomponents.sdk.MessageLikeEventContent
|
||||
import org.matrix.rustcomponents.sdk.MessageType
|
||||
import org.matrix.rustcomponents.sdk.TextMessageContent
|
||||
import org.matrix.rustcomponents.sdk.TimelineEventContent
|
||||
import org.matrix.rustcomponents.sdk.UnstableAudioDetailsContent
|
||||
import org.matrix.rustcomponents.sdk.UnstableVoiceContent
|
||||
import org.matrix.rustcomponents.sdk.VideoInfo
|
||||
import org.matrix.rustcomponents.sdk.VideoMessageContent
|
||||
|
||||
internal fun aRustTimelineEventContentMessageLike(
|
||||
content: MessageLikeEventContent = aRustMessageLikeEventContentRoomMessage(),
|
||||
@@ -42,3 +56,94 @@ internal fun aRustTextMessageContent(
|
||||
body = body,
|
||||
formatted = formatted,
|
||||
)
|
||||
|
||||
internal fun aRustMessageTypeGallery(
|
||||
content: GalleryMessageContent = aRustGalleryMessageContent(),
|
||||
) = MessageType.Gallery(content = content)
|
||||
|
||||
internal fun aRustGalleryMessageContent(
|
||||
body: String = "A gallery",
|
||||
formatted: FormattedBody? = null,
|
||||
itemTypes: List<GalleryItemType> = listOf(aRustGalleryItemTypeImage()),
|
||||
) = GalleryMessageContent(body = body, formatted = formatted, itemtypes = itemTypes)
|
||||
|
||||
internal fun aRustGalleryItemTypeImage(
|
||||
content: ImageMessageContent = aRustImageMessageContent(),
|
||||
) = GalleryItemType.Image(content = content)
|
||||
|
||||
internal fun aRustGalleryItemTypeAudio(
|
||||
content: AudioMessageContent = aRustAudioMessageContent(),
|
||||
) = GalleryItemType.Audio(content = content)
|
||||
|
||||
internal fun aRustGalleryItemTypeVideo(
|
||||
content: VideoMessageContent = aRustVideoMessageContent(),
|
||||
) = GalleryItemType.Video(content = content)
|
||||
|
||||
internal fun aRustGalleryItemTypeFile(
|
||||
content: FileMessageContent = aRustFileMessageContent(),
|
||||
) = GalleryItemType.File(content = content)
|
||||
|
||||
internal fun aRustGalleryItemTypeOther(
|
||||
itemType: String = "m.unknown",
|
||||
body: String = "unknown item",
|
||||
) = GalleryItemType.Other(itemtype = itemType, body = body)
|
||||
|
||||
internal fun aRustImageMessageContent(
|
||||
filename: String = "image.jpg",
|
||||
caption: String? = null,
|
||||
formattedCaption: FormattedBody? = null,
|
||||
source: MediaSource = FakeFfiMediaSource("mxc://server/image"),
|
||||
info: ImageInfo? = null,
|
||||
) = ImageMessageContent(
|
||||
filename = filename,
|
||||
caption = caption,
|
||||
formattedCaption = formattedCaption,
|
||||
source = source,
|
||||
info = info,
|
||||
)
|
||||
|
||||
internal fun aRustAudioMessageContent(
|
||||
filename: String = "audio.mp3",
|
||||
caption: String? = null,
|
||||
formattedCaption: FormattedBody? = null,
|
||||
source: MediaSource = FakeFfiMediaSource("mxc://server/audio"),
|
||||
info: AudioInfo? = null,
|
||||
audio: UnstableAudioDetailsContent? = null,
|
||||
voice: UnstableVoiceContent? = null,
|
||||
) = AudioMessageContent(
|
||||
filename = filename,
|
||||
caption = caption,
|
||||
formattedCaption = formattedCaption,
|
||||
source = source,
|
||||
info = info,
|
||||
audio = audio,
|
||||
voice = voice,
|
||||
)
|
||||
|
||||
internal fun aRustVideoMessageContent(
|
||||
filename: String = "video.mp4",
|
||||
caption: String? = null,
|
||||
formattedCaption: FormattedBody? = null,
|
||||
source: MediaSource = FakeFfiMediaSource("mxc://server/video"),
|
||||
info: VideoInfo? = null,
|
||||
) = VideoMessageContent(
|
||||
filename = filename,
|
||||
caption = caption,
|
||||
formattedCaption = formattedCaption,
|
||||
source = source,
|
||||
info = info,
|
||||
)
|
||||
|
||||
internal fun aRustFileMessageContent(
|
||||
filename: String = "document.pdf",
|
||||
caption: String? = null,
|
||||
formattedCaption: FormattedBody? = null,
|
||||
source: MediaSource = FakeFfiMediaSource("mxc://server/file"),
|
||||
info: FileInfo? = null,
|
||||
) = FileMessageContent(
|
||||
filename = filename,
|
||||
caption = caption,
|
||||
formattedCaption = formattedCaption,
|
||||
source = source,
|
||||
info = info,
|
||||
)
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations 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.MediaSource
|
||||
import org.matrix.rustcomponents.sdk.NoHandle
|
||||
|
||||
internal class FakeFfiMediaSource(
|
||||
private val fakeUrl: String = "mxc://server/media",
|
||||
) : MediaSource(NoHandle) {
|
||||
override fun url(): String = fakeUrl
|
||||
override fun toJson(): String = """{"url":"$fakeUrl"}"""
|
||||
}
|
||||
+2
-4
@@ -10,12 +10,10 @@ package io.element.android.libraries.matrix.impl.keys
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.androidutils.crypto.ClientSecret
|
||||
import io.element.android.tests.testutils.robolectric.RobolectricTest
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class DefaultSecretGeneratorTest {
|
||||
class DefaultSecretGeneratorTest : RobolectricTest() {
|
||||
@Test
|
||||
fun `check that generated passphrase has the expected length`() {
|
||||
val secretGenerator = DefaultSecretGenerator()
|
||||
|
||||
+1
-1
@@ -10,8 +10,8 @@ package io.element.android.libraries.matrix.impl.mapper
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.auth.external.ExternalSession
|
||||
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSession
|
||||
import io.element.android.libraries.matrix.impl.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.test.A_DEVICE_ID
|
||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL_2
|
||||
|
||||
+2
-4
@@ -10,12 +10,10 @@ package io.element.android.libraries.matrix.impl.permalink
|
||||
|
||||
import android.net.Uri
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.tests.testutils.robolectric.RobolectricTest
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class DefaultMatrixToConverterTest {
|
||||
class DefaultMatrixToConverterTest : RobolectricTest() {
|
||||
@Test
|
||||
fun `converting a matrix-to url does nothing`() {
|
||||
val url = Uri.parse("https://matrix.to/#/#element-android:matrix.org")
|
||||
|
||||
+2
-1
@@ -9,11 +9,12 @@
|
||||
package io.element.android.libraries.matrix.impl.room
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.StateEventType
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimelineEventFilter
|
||||
import org.matrix.rustcomponents.sdk.TimelineEventFilter
|
||||
|
||||
class FakeTimelineEventFilterFactory : TimelineEventFilterFactory {
|
||||
override fun create(listStateEventType: List<StateEventType>): TimelineEventFilter {
|
||||
override fun create(joinRule: JoinRule?, isEncrypted: Boolean?, excludedStateTypes: List<StateEventType>): TimelineEventFilter {
|
||||
return FakeFfiTimelineEventFilter()
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -10,10 +10,12 @@ package io.element.android.libraries.matrix.impl.room
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import org.matrix.rustcomponents.sdk.MessageLikeEventType
|
||||
import uniffi.ruma_events.MessageLikeEventType
|
||||
|
||||
class MessageEventTypeKtTest {
|
||||
@Ignore("Can't use MessageLikeEventType as that will try accessing the Rust SDK")
|
||||
@Test
|
||||
fun `map Rust type should result to correct Kotlin type`() {
|
||||
assertThat(MessageLikeEventType.CallAnswer.map()).isEqualTo(MessageEventType.CallAnswer)
|
||||
|
||||
+11
-5
@@ -10,10 +10,12 @@ package io.element.android.libraries.matrix.impl.room
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.room.StateEventType
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import org.matrix.rustcomponents.sdk.StateEventType as RustStateEventType
|
||||
import uniffi.ruma_events.StateEventType as RustStateEventType
|
||||
|
||||
class StateEventTypeTest {
|
||||
@Ignore("Can't use RustStateEventType as that will try accessing the Rust SDK")
|
||||
@Test
|
||||
fun `mapping Rust type should work`() {
|
||||
assertThat(RustStateEventType.CallMember.map()).isEqualTo(StateEventType.CallMember)
|
||||
@@ -27,9 +29,10 @@ class StateEventTypeTest {
|
||||
assertThat(RustStateEventType.RoomGuestAccess.map()).isEqualTo(StateEventType.RoomGuestAccess)
|
||||
assertThat(RustStateEventType.RoomHistoryVisibility.map()).isEqualTo(StateEventType.RoomHistoryVisibility)
|
||||
assertThat(RustStateEventType.RoomJoinRules.map()).isEqualTo(StateEventType.RoomJoinRules)
|
||||
assertThat(RustStateEventType.RoomMemberEvent.map()).isEqualTo(StateEventType.RoomMemberEvent)
|
||||
assertThat(RustStateEventType.RoomMember.map()).isEqualTo(StateEventType.RoomMemberEvent)
|
||||
assertThat(RustStateEventType.RoomName.map()).isEqualTo(StateEventType.RoomName)
|
||||
assertThat(RustStateEventType.RoomPinnedEvents.map()).isEqualTo(StateEventType.RoomPinnedEvents)
|
||||
assertThat(RustStateEventType.RoomPolicy.map()).isEqualTo(StateEventType.RoomPolicy)
|
||||
assertThat(RustStateEventType.RoomPowerLevels.map()).isEqualTo(StateEventType.RoomPowerLevels)
|
||||
assertThat(RustStateEventType.RoomServerAcl.map()).isEqualTo(StateEventType.RoomServerAcl)
|
||||
assertThat(RustStateEventType.RoomThirdPartyInvite.map()).isEqualTo(StateEventType.RoomThirdPartyInvite)
|
||||
@@ -37,7 +40,8 @@ class StateEventTypeTest {
|
||||
assertThat(RustStateEventType.RoomTopic.map()).isEqualTo(StateEventType.RoomTopic)
|
||||
assertThat(RustStateEventType.SpaceChild.map()).isEqualTo(StateEventType.SpaceChild)
|
||||
assertThat(RustStateEventType.SpaceParent.map()).isEqualTo(StateEventType.SpaceParent)
|
||||
assertThat(RustStateEventType.Custom("foo").map()).isEqualTo(StateEventType.Custom("foo"))
|
||||
// Can't test Custom as it will try calling the Rust code, which is not available in unit tests
|
||||
// assertThat(stateEventTypeFromString("foo").map()).isEqualTo(StateEventType.Custom("foo"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -53,9 +57,10 @@ class StateEventTypeTest {
|
||||
assertThat(StateEventType.RoomGuestAccess.map()).isEqualTo(RustStateEventType.RoomGuestAccess)
|
||||
assertThat(StateEventType.RoomHistoryVisibility.map()).isEqualTo(RustStateEventType.RoomHistoryVisibility)
|
||||
assertThat(StateEventType.RoomJoinRules.map()).isEqualTo(RustStateEventType.RoomJoinRules)
|
||||
assertThat(StateEventType.RoomMemberEvent.map()).isEqualTo(RustStateEventType.RoomMemberEvent)
|
||||
assertThat(StateEventType.RoomMemberEvent.map()).isEqualTo(RustStateEventType.RoomMember)
|
||||
assertThat(StateEventType.RoomName.map()).isEqualTo(RustStateEventType.RoomName)
|
||||
assertThat(StateEventType.RoomPinnedEvents.map()).isEqualTo(RustStateEventType.RoomPinnedEvents)
|
||||
assertThat(StateEventType.RoomPolicy.map()).isEqualTo(RustStateEventType.RoomPolicy)
|
||||
assertThat(StateEventType.RoomPowerLevels.map()).isEqualTo(RustStateEventType.RoomPowerLevels)
|
||||
assertThat(StateEventType.RoomServerAcl.map()).isEqualTo(RustStateEventType.RoomServerAcl)
|
||||
assertThat(StateEventType.RoomThirdPartyInvite.map()).isEqualTo(RustStateEventType.RoomThirdPartyInvite)
|
||||
@@ -63,6 +68,7 @@ class StateEventTypeTest {
|
||||
assertThat(StateEventType.RoomTopic.map()).isEqualTo(RustStateEventType.RoomTopic)
|
||||
assertThat(StateEventType.SpaceChild.map()).isEqualTo(RustStateEventType.SpaceChild)
|
||||
assertThat(StateEventType.SpaceParent.map()).isEqualTo(RustStateEventType.SpaceParent)
|
||||
assertThat(StateEventType.Custom("foo").map()).isEqualTo(RustStateEventType.Custom("foo"))
|
||||
// Can't test Custom as it will try calling the Rust code, which is not available in unit tests
|
||||
// assertThat(StateEventType.Custom("foo").map()).isEqualTo(stateEventTypeFromString("foo"))
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
|
||||
package io.element.android.libraries.matrix.impl.storage
|
||||
|
||||
import io.element.android.libraries.matrix.impl.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||
|
||||
class FakeSqliteStoreBuilderProvider : SqliteStoreBuilderProvider {
|
||||
override fun provide(sessionPaths: SessionPaths): SqliteStoreBuilder {
|
||||
|
||||
+318
@@ -0,0 +1,318 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations 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.timeline.item.event
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.GalleryItemType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.GalleryMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType
|
||||
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustAudioMessageContent
|
||||
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustFileMessageContent
|
||||
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustGalleryItemTypeAudio
|
||||
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustGalleryItemTypeFile
|
||||
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustGalleryItemTypeImage
|
||||
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustGalleryItemTypeOther
|
||||
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustGalleryItemTypeVideo
|
||||
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustGalleryMessageContent
|
||||
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustImageMessageContent
|
||||
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustMessageTypeGallery
|
||||
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustVideoMessageContent
|
||||
import org.junit.Test
|
||||
import org.matrix.rustcomponents.sdk.EmoteMessageContent
|
||||
import org.matrix.rustcomponents.sdk.LocationContent
|
||||
import org.matrix.rustcomponents.sdk.MessageType
|
||||
import org.matrix.rustcomponents.sdk.NoticeMessageContent
|
||||
import org.matrix.rustcomponents.sdk.TextMessageContent
|
||||
import org.matrix.rustcomponents.sdk.UnstableVoiceContent
|
||||
import org.matrix.rustcomponents.sdk.AssetType as RustAssetType
|
||||
|
||||
class EventMessageMapperTest {
|
||||
private val sut = EventMessageMapper()
|
||||
|
||||
@Test
|
||||
fun `mapMessageType with Text returns TextMessageType`() {
|
||||
val result = sut.mapMessageType(
|
||||
MessageType.Text(
|
||||
content = TextMessageContent(body = "Hello", formatted = null)
|
||||
)
|
||||
)
|
||||
assertThat(result).isEqualTo(TextMessageType(body = "Hello", formatted = null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mapMessageType with Notice returns NoticeMessageType`() {
|
||||
val result = sut.mapMessageType(
|
||||
MessageType.Notice(content = NoticeMessageContent(body = "A notice", formatted = null))
|
||||
)
|
||||
assertThat(result).isEqualTo(NoticeMessageType(body = "A notice", formatted = null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mapMessageType with Emote returns EmoteMessageType`() {
|
||||
val result = sut.mapMessageType(
|
||||
MessageType.Emote(content = EmoteMessageContent(body = "An emote", formatted = null))
|
||||
)
|
||||
assertThat(result).isEqualTo(EmoteMessageType(body = "An emote", formatted = null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mapMessageType with Other returns OtherMessageType`() {
|
||||
val result = sut.mapMessageType(
|
||||
MessageType.Other(msgtype = "m.custom", body = "custom body")
|
||||
)
|
||||
assertThat(result).isEqualTo(OtherMessageType(msgType = "m.custom", body = "custom body"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mapMessageType with Location returns LocationMessageType`() {
|
||||
val result = sut.mapMessageType(
|
||||
MessageType.Location(
|
||||
content = LocationContent(
|
||||
body = "Location body",
|
||||
geoUri = "geo:51.5,-0.1",
|
||||
description = "London",
|
||||
zoomLevel = null,
|
||||
asset = RustAssetType.PIN,
|
||||
)
|
||||
)
|
||||
)
|
||||
assertThat(result).isEqualTo(
|
||||
LocationMessageType(
|
||||
body = "Location body",
|
||||
geoUri = "geo:51.5,-0.1",
|
||||
description = "London",
|
||||
assetType = AssetType.PIN,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mapMessageType with Audio without voice returns AudioMessageType`() {
|
||||
val result = sut.mapMessageType(
|
||||
MessageType.Audio(content = aRustAudioMessageContent(filename = "audio.mp3", voice = null))
|
||||
)
|
||||
assertThat(result).isEqualTo(
|
||||
AudioMessageType(
|
||||
filename = "audio.mp3",
|
||||
caption = null,
|
||||
formattedCaption = null,
|
||||
source = MediaSource(url = "mxc://server/audio", json = """{"url":"mxc://server/audio"}"""),
|
||||
info = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mapMessageType with Audio with voice returns VoiceMessageType`() {
|
||||
val result = sut.mapMessageType(
|
||||
MessageType.Audio(content = aRustAudioMessageContent(filename = "voice.ogg", voice = UnstableVoiceContent()))
|
||||
)
|
||||
assertThat(result).isEqualTo(
|
||||
VoiceMessageType(
|
||||
filename = "voice.ogg",
|
||||
caption = null,
|
||||
formattedCaption = null,
|
||||
source = MediaSource(url = "mxc://server/audio", json = """{"url":"mxc://server/audio"}"""),
|
||||
info = null,
|
||||
details = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mapMessageType with File returns FileMessageType`() {
|
||||
val result = sut.mapMessageType(
|
||||
MessageType.File(content = aRustFileMessageContent(filename = "document.pdf"))
|
||||
)
|
||||
assertThat(result).isEqualTo(
|
||||
FileMessageType(
|
||||
filename = "document.pdf",
|
||||
caption = null,
|
||||
formattedCaption = null,
|
||||
source = MediaSource(url = "mxc://server/file", json = """{"url":"mxc://server/file"}"""),
|
||||
info = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mapMessageType with Image returns ImageMessageType`() {
|
||||
val result = sut.mapMessageType(
|
||||
MessageType.Image(content = aRustImageMessageContent(filename = "image.jpg"))
|
||||
)
|
||||
assertThat(result).isEqualTo(
|
||||
ImageMessageType(
|
||||
filename = "image.jpg",
|
||||
caption = null,
|
||||
formattedCaption = null,
|
||||
source = MediaSource(url = "mxc://server/image", json = """{"url":"mxc://server/image"}"""),
|
||||
info = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mapMessageType with Video returns VideoMessageType`() {
|
||||
val result = sut.mapMessageType(
|
||||
MessageType.Video(content = aRustVideoMessageContent(filename = "video.mp4"))
|
||||
)
|
||||
assertThat(result).isEqualTo(
|
||||
VideoMessageType(
|
||||
filename = "video.mp4",
|
||||
caption = null,
|
||||
formattedCaption = null,
|
||||
source = MediaSource(url = "mxc://server/video", json = """{"url":"mxc://server/video"}"""),
|
||||
info = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mapMessageType with Gallery with Image item returns GalleryMessageType with Image`() {
|
||||
val result = sut.mapMessageType(
|
||||
aRustMessageTypeGallery(
|
||||
content = aRustGalleryMessageContent(
|
||||
body = "A gallery",
|
||||
itemTypes = listOf(aRustGalleryItemTypeImage(aRustImageMessageContent(filename = "image.jpg"))),
|
||||
)
|
||||
)
|
||||
)
|
||||
assertThat(result).isEqualTo(
|
||||
GalleryMessageType(
|
||||
body = "A gallery",
|
||||
formatted = null,
|
||||
items = listOf(
|
||||
GalleryItemType.Image(
|
||||
content = ImageMessageType(
|
||||
filename = "image.jpg",
|
||||
caption = null,
|
||||
formattedCaption = null,
|
||||
source = MediaSource(url = "mxc://server/image", json = """{"url":"mxc://server/image"}"""),
|
||||
info = null,
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mapMessageType with Gallery with Audio item returns GalleryMessageType with Audio`() {
|
||||
val result = sut.mapMessageType(
|
||||
aRustMessageTypeGallery(
|
||||
content = aRustGalleryMessageContent(
|
||||
itemTypes = listOf(aRustGalleryItemTypeAudio(aRustAudioMessageContent(filename = "audio.mp3")))
|
||||
)
|
||||
)
|
||||
)
|
||||
assertThat(result).isEqualTo(
|
||||
GalleryMessageType(
|
||||
body = "A gallery",
|
||||
formatted = null,
|
||||
items = listOf(
|
||||
GalleryItemType.Audio(
|
||||
content = AudioMessageType(
|
||||
filename = "audio.mp3",
|
||||
caption = null,
|
||||
formattedCaption = null,
|
||||
source = MediaSource(url = "mxc://server/audio", json = """{"url":"mxc://server/audio"}"""),
|
||||
info = null,
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mapMessageType with Gallery with Video item returns GalleryMessageType with Video`() {
|
||||
val result = sut.mapMessageType(
|
||||
aRustMessageTypeGallery(
|
||||
content = aRustGalleryMessageContent(
|
||||
itemTypes = listOf(aRustGalleryItemTypeVideo(aRustVideoMessageContent(filename = "video.mp4")))
|
||||
)
|
||||
)
|
||||
)
|
||||
assertThat(result).isEqualTo(
|
||||
GalleryMessageType(
|
||||
body = "A gallery",
|
||||
formatted = null,
|
||||
items = listOf(
|
||||
GalleryItemType.Video(
|
||||
content = VideoMessageType(
|
||||
filename = "video.mp4",
|
||||
caption = null,
|
||||
formattedCaption = null,
|
||||
source = MediaSource(url = "mxc://server/video", json = """{"url":"mxc://server/video"}"""),
|
||||
info = null,
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mapMessageType with Gallery with File item returns GalleryMessageType with File`() {
|
||||
val result = sut.mapMessageType(
|
||||
aRustMessageTypeGallery(
|
||||
content = aRustGalleryMessageContent(
|
||||
itemTypes = listOf(aRustGalleryItemTypeFile(aRustFileMessageContent(filename = "document.pdf")))
|
||||
)
|
||||
)
|
||||
)
|
||||
assertThat(result).isEqualTo(
|
||||
GalleryMessageType(
|
||||
body = "A gallery",
|
||||
formatted = null,
|
||||
items = listOf(
|
||||
GalleryItemType.File(
|
||||
content = FileMessageType(
|
||||
filename = "document.pdf",
|
||||
caption = null,
|
||||
formattedCaption = null,
|
||||
source = MediaSource(url = "mxc://server/file", json = """{"url":"mxc://server/file"}"""),
|
||||
info = null,
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mapMessageType with Gallery with Other item returns GalleryMessageType with Other`() {
|
||||
val result = sut.mapMessageType(
|
||||
aRustMessageTypeGallery(
|
||||
content = aRustGalleryMessageContent(
|
||||
itemTypes = listOf(aRustGalleryItemTypeOther(itemType = "m.custom", body = "custom item"))
|
||||
)
|
||||
)
|
||||
)
|
||||
assertThat(result).isEqualTo(
|
||||
GalleryMessageType(
|
||||
body = "A gallery",
|
||||
formatted = null,
|
||||
items = listOf(
|
||||
GalleryItemType.Other(itemType = "m.custom", body = "custom item")
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
-118
@@ -1,118 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations 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.timeline.postprocessor
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||
import org.junit.Test
|
||||
|
||||
private const val TODAY = 1_779_779_967_000
|
||||
private const val YESTERDAY = TODAY - 24 * 60 * 60 * 1000
|
||||
private const val DAY_BEFORE_YESTERDAY = YESTERDAY - 24 * 60 * 60 * 1000
|
||||
|
||||
class FilterEmptyDayPostProcessorTest {
|
||||
private val anEvent = MatrixTimelineItem.Event(
|
||||
uniqueId = UniqueId("event"),
|
||||
event = anEventTimelineItem(),
|
||||
)
|
||||
|
||||
private fun aDaySeparator(timestmap: Long) = MatrixTimelineItem.Virtual(
|
||||
uniqueId = UniqueId("day_$timestmap"),
|
||||
virtual = VirtualTimelineItem.DayDivider(timestmap)
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `filterEmptyDaySeparators keeps day separator with events after it`() {
|
||||
val items = listOf(
|
||||
aDaySeparator(TODAY),
|
||||
anEvent,
|
||||
)
|
||||
val result = FilterEmptyDayPostProcessor().process(items)
|
||||
assertThat(result).hasSize(2)
|
||||
assertThat(result[0]).isEqualTo(aDaySeparator(TODAY))
|
||||
assertThat(result[1]).isEqualTo(anEvent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `filterEmptyDaySeparators removes day separator with no events after it`() {
|
||||
val items = listOf(
|
||||
aDaySeparator(YESTERDAY),
|
||||
aDaySeparator(TODAY),
|
||||
)
|
||||
val result = FilterEmptyDayPostProcessor().process(items)
|
||||
assertThat(result).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `filterEmptyDaySeparators removes second day separator and keeps first when only first has events`() {
|
||||
val items = listOf(
|
||||
aDaySeparator(YESTERDAY),
|
||||
anEvent,
|
||||
aDaySeparator(TODAY),
|
||||
)
|
||||
val result = FilterEmptyDayPostProcessor().process(items)
|
||||
assertThat(result).hasSize(2)
|
||||
assertThat(result[0]).isEqualTo(aDaySeparator(YESTERDAY))
|
||||
assertThat(result[1]).isEqualTo(anEvent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `filterEmptyDaySeparators handles multiple day separators in a row with no events`() {
|
||||
val items = listOf(
|
||||
aDaySeparator(TODAY),
|
||||
aDaySeparator(YESTERDAY),
|
||||
aDaySeparator(DAY_BEFORE_YESTERDAY),
|
||||
)
|
||||
val result = FilterEmptyDayPostProcessor().process(items)
|
||||
assertThat(result).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `filterEmptyDaySeparators keeps all items when no day separators`() {
|
||||
val items = listOf(
|
||||
anEvent.copy(uniqueId = UniqueId("event2")),
|
||||
anEvent,
|
||||
)
|
||||
val result = FilterEmptyDayPostProcessor().process(items)
|
||||
assertThat(result).hasSize(2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `filterEmptyDaySeparators removes day separator preceded by non-event virtual item`() {
|
||||
val readMarker = MatrixTimelineItem.Virtual(
|
||||
uniqueId = UniqueId("readMarker"),
|
||||
virtual = VirtualTimelineItem.ReadMarker
|
||||
)
|
||||
val items = listOf(
|
||||
aDaySeparator(TODAY),
|
||||
readMarker,
|
||||
)
|
||||
val result = FilterEmptyDayPostProcessor().process(items)
|
||||
assertThat(result).hasSize(1)
|
||||
assertThat(result[0]).isEqualTo(readMarker)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `filterEmptyDaySeparators keeps day separator when non-event virtual items are between separator and event`() {
|
||||
val readMarker = MatrixTimelineItem.Virtual(
|
||||
uniqueId = UniqueId("readMarker"),
|
||||
virtual = VirtualTimelineItem.ReadMarker
|
||||
)
|
||||
val items = listOf(
|
||||
aDaySeparator(TODAY),
|
||||
readMarker,
|
||||
anEvent,
|
||||
)
|
||||
val result = FilterEmptyDayPostProcessor().process(items)
|
||||
assertThat(result).hasSize(3)
|
||||
assertThat(result[0]).isEqualTo(aDaySeparator(TODAY))
|
||||
}
|
||||
}
|
||||
-75
@@ -1,75 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations 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.timeline.postprocessor
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import org.junit.Test
|
||||
|
||||
class FilterPublicMembershipChangesPostProcessorTest {
|
||||
@Test
|
||||
fun `processor removes join, leave, and profile events in unencrypted public rooms`() {
|
||||
val timelineItems = listOf(
|
||||
roomCreateEvent,
|
||||
roomCreatorJoinEvent,
|
||||
otherMemberJoinEvent,
|
||||
messageEvent,
|
||||
otherMemberLeaveEvent,
|
||||
profileChangeEvent,
|
||||
)
|
||||
val expected = listOf(
|
||||
roomCreateEvent,
|
||||
messageEvent,
|
||||
)
|
||||
val processor = FilterPublicMembershipChangesPostProcessor()
|
||||
val processedItems = processor.process(
|
||||
timelineItems,
|
||||
joinRule = JoinRule.Public,
|
||||
isEncrypted = false,
|
||||
)
|
||||
assertThat(processedItems).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `processor keeps all events in encrypted public rooms`() {
|
||||
val timelineItems = listOf(
|
||||
roomCreateEvent,
|
||||
roomCreatorJoinEvent,
|
||||
otherMemberJoinEvent,
|
||||
messageEvent,
|
||||
otherMemberLeaveEvent,
|
||||
profileChangeEvent,
|
||||
)
|
||||
val processor = FilterPublicMembershipChangesPostProcessor()
|
||||
val processedItems = processor.process(
|
||||
timelineItems,
|
||||
joinRule = JoinRule.Public,
|
||||
isEncrypted = true,
|
||||
)
|
||||
assertThat(processedItems).isEqualTo(timelineItems)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `processor keeps membership events in invite-only rooms`() {
|
||||
val timelineItems = listOf(
|
||||
roomCreateEvent,
|
||||
roomCreatorJoinEvent,
|
||||
otherMemberJoinEvent,
|
||||
messageEvent,
|
||||
otherMemberLeaveEvent,
|
||||
profileChangeEvent,
|
||||
)
|
||||
val processor = FilterPublicMembershipChangesPostProcessor()
|
||||
val processedItems = processor.process(
|
||||
timelineItems,
|
||||
joinRule = JoinRule.Invite,
|
||||
isEncrypted = null,
|
||||
)
|
||||
assertThat(processedItems).isEqualTo(timelineItems)
|
||||
}
|
||||
}
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 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.util
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
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
|
||||
|
||||
class SessionPathsProviderTest {
|
||||
@Test
|
||||
fun `if session is not found, provides returns null`() = runTest {
|
||||
val sut = SessionPathsProvider(InMemorySessionStore())
|
||||
val result = sut.provides(A_SESSION_ID)
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if session is found, provides returns the data`() = runTest {
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user