Merge branch 'develop' into feature/fga/set_user_status
This commit is contained in:
+8
@@ -45,6 +45,7 @@ import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibilit
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
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.scanner.ContentScanner
|
||||
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.SyncState
|
||||
@@ -153,6 +154,7 @@ class RustMatrixClient(
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val workManagerScheduler: WorkManagerScheduler,
|
||||
override val contentScanner: ContentScanner?,
|
||||
) : MatrixClient {
|
||||
override val sessionId: UserId = UserId(innerClient.userId())
|
||||
override val deviceId: DeviceId = DeviceId(innerClient.deviceId())
|
||||
@@ -831,6 +833,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...")
|
||||
|
||||
+23
@@ -17,11 +17,14 @@ 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.core.UserId
|
||||
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.api.scanner.ContentScannerUrlProvider
|
||||
import io.element.android.libraries.matrix.impl.analytics.UtdTracker
|
||||
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
|
||||
import io.element.android.libraries.matrix.impl.scanner.RustContentScanner
|
||||
import io.element.android.libraries.matrix.impl.storage.SqliteStoreBuilderProvider
|
||||
import io.element.android.libraries.matrix.impl.util.anonymizedTokens
|
||||
import io.element.android.libraries.network.useragent.UserAgentProvider
|
||||
@@ -34,6 +37,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.ContentScanner
|
||||
import org.matrix.rustcomponents.sdk.CrossProcessLockConfig
|
||||
import org.matrix.rustcomponents.sdk.RequestConfig
|
||||
import org.matrix.rustcomponents.sdk.Session
|
||||
@@ -66,6 +70,7 @@ class RustMatrixClientFactory(
|
||||
private val clientBuilderProvider: ClientBuilderProvider,
|
||||
private val sqliteStoreBuilderProvider: SqliteStoreBuilderProvider,
|
||||
private val workManagerScheduler: WorkManagerScheduler,
|
||||
private val contentScannerUrlProvider: ContentScannerUrlProvider,
|
||||
) {
|
||||
private val sessionDelegate = RustClientSessionDelegate(
|
||||
sessionStore = sessionStore,
|
||||
@@ -113,6 +118,23 @@ class RustMatrixClientFactory(
|
||||
|
||||
client.setUtdDelegate(UtdTracker(analyticsService))
|
||||
|
||||
val domainName = UserId(client.userId()).domainName
|
||||
// If a content scanner URL is available for the homeserver, create a RustContentScanner and set it on the client.
|
||||
// This allows the SDK to use the content scanner for automatic media scanning.
|
||||
// If no content scanner URL is available, the contentScanner will be null.
|
||||
val contentScanner = domainName?.let {
|
||||
contentScannerUrlProvider.getContentScannerUrl(domainName)
|
||||
.getOrNull()
|
||||
?.let { contentScannerUrl ->
|
||||
val contentScanner = ContentScanner(contentScannerUrl)
|
||||
client.setContentScanner(contentScanner)
|
||||
RustContentScanner(
|
||||
client = client,
|
||||
rustScanner = contentScanner,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val syncService = client.syncService()
|
||||
.withSharePos(true)
|
||||
.withOfflineMode()
|
||||
@@ -132,6 +154,7 @@ class RustMatrixClientFactory(
|
||||
featureFlagService = featureFlagService,
|
||||
analyticsService = analyticsService,
|
||||
workManagerScheduler = workManagerScheduler,
|
||||
contentScanner = contentScanner,
|
||||
).also {
|
||||
Timber.tag("RustMatrixClient").i("Creating Client with access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'")
|
||||
}
|
||||
|
||||
+6
@@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.notificationsettings.Notification
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.matrix.api.scanner.ContentScanner
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceService
|
||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
@@ -96,4 +97,9 @@ object SessionMatrixModule {
|
||||
fun providesHomeserverCapabilitiesProvider(matrixClient: MatrixClient): HomeserverCapabilitiesProvider {
|
||||
return matrixClient.homeserverCapabilities()
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun providesContentScanner(matrixClient: MatrixClient): ContentScanner? {
|
||||
return matrixClient.contentScanner
|
||||
}
|
||||
}
|
||||
|
||||
+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
-2
@@ -9,11 +9,13 @@
|
||||
package io.element.android.libraries.matrix.impl.media
|
||||
|
||||
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.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
||||
import io.element.android.libraries.matrix.api.media.MediaFile
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.impl.exception.mapClientException
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.Client
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
@@ -37,7 +39,7 @@ class RustMediaLoader(
|
||||
source.toRustMediaSource().use { source ->
|
||||
innerClient.getMediaContent(source)
|
||||
}
|
||||
}
|
||||
}.mapFailure { it.mapClientException() }
|
||||
}
|
||||
|
||||
override suspend fun loadMediaThumbnail(
|
||||
@@ -54,7 +56,7 @@ class RustMediaLoader(
|
||||
height = height.toULong()
|
||||
)
|
||||
}
|
||||
}
|
||||
}.mapFailure { it.mapClientException() }
|
||||
}
|
||||
|
||||
override suspend fun downloadMediaFile(
|
||||
|
||||
+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
@@ -219,6 +219,7 @@ class JoinedRustRoom(
|
||||
RoomMessageEventMessageType.IMAGE,
|
||||
RoomMessageEventMessageType.VIDEO,
|
||||
RoomMessageEventMessageType.AUDIO,
|
||||
RoomMessageEventMessageType.GALLERY,
|
||||
)
|
||||
)
|
||||
is CreateTimelineParams.Focused,
|
||||
|
||||
+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())
|
||||
}
|
||||
|
||||
+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())
|
||||
}
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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.scanner
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.matrix.api.scanner.ContentScannerUrlProvider
|
||||
|
||||
/**
|
||||
* Default FOSS implementation of [ContentScannerUrlProvider] that returns `null` for the content scanner URL.
|
||||
*/
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultContentScannerUrlProvider : ContentScannerUrlProvider {
|
||||
override suspend fun getContentScannerUrl(homeserver: String): Result<String?> = Result.success(null)
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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.scanner
|
||||
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.scanner.ContentScanner
|
||||
import org.matrix.rustcomponents.sdk.Client
|
||||
import org.matrix.rustcomponents.sdk.NoHandle
|
||||
import org.matrix.rustcomponents.sdk.ContentScanner as RustScanner
|
||||
import org.matrix.rustcomponents.sdk.MediaSource as RustMediaSource
|
||||
|
||||
class RustContentScanner(
|
||||
private val client: Client,
|
||||
private val rustScanner: RustScanner,
|
||||
) : ContentScanner {
|
||||
override suspend fun scan(mediaSource: MediaSource): Result<Boolean> {
|
||||
return runCatchingExceptions {
|
||||
rustScanner.scan(client, mediaSource.toRustMediaSource()).clean
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun MediaSource.toRustMediaSource(): RustMediaSource {
|
||||
val json = this.json
|
||||
return try {
|
||||
if (json != null) {
|
||||
RustMediaSource.fromJson(json)
|
||||
} else {
|
||||
RustMediaSource.fromUrl(safeUrl)
|
||||
}
|
||||
} catch (e: LinkageError) {
|
||||
// Used for tests, since we can't instantiate an actual `RustMediaSource` because the native library can't be loaded
|
||||
val isTest = runCatchingExceptions { Class.forName("org.junit.Test") }.isSuccess
|
||||
if (isTest) RustMediaSource(NoHandle) else throw e
|
||||
}
|
||||
}
|
||||
+57
@@ -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
|
||||
@@ -67,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
|
||||
@@ -535,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>,
|
||||
@@ -621,6 +660,24 @@ class RustTimeline(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun isEventLoaded(eventId: EventId): Boolean = withContext(dispatcher) {
|
||||
val isInLoadedItems = _timelineItems.replayCache.firstOrNull().orEmpty().any { timelineItem ->
|
||||
timelineItem is MatrixTimelineItem.Event && timelineItem.eventId == eventId
|
||||
}
|
||||
if (isInLoadedItems) {
|
||||
// Displayed events are caught above. The SDK-level list still holds events the display
|
||||
// layer later drops, so this avoids the FFI call for in-window-but-not-rendered events.
|
||||
true
|
||||
} else {
|
||||
// getEventTimelineItemByEventId throws when the event isn't in the loaded window.
|
||||
// EventTimelineItem is a Disposable wrapping a native handle, so release it once we've
|
||||
// confirmed presence — otherwise the handle lingers until the GC cleaner runs.
|
||||
runCatchingExceptions {
|
||||
inner.getEventTimelineItemByEventId(eventId.value).use { /* presence is all we need */ }
|
||||
}.isSuccess
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun pinEvent(eventId: EventId): Result<Boolean> = withContext(dispatcher) {
|
||||
runCatchingExceptions {
|
||||
inner.pinEvent(eventId = eventId.value)
|
||||
|
||||
+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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -11,6 +11,7 @@ package io.element.android.libraries.matrix.impl
|
||||
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.api.scanner.ContentScannerUrlProvider
|
||||
import io.element.android.libraries.matrix.impl.auth.FakeProxyProvider
|
||||
import io.element.android.libraries.matrix.impl.room.FakeTimelineEventFilterFactory
|
||||
import io.element.android.libraries.matrix.impl.storage.FakeSqliteStoreBuilderProvider
|
||||
@@ -51,6 +52,7 @@ fun TestScope.createRustMatrixClientFactory(
|
||||
),
|
||||
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
|
||||
workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(),
|
||||
contentScannerUrlProvider: ContentScannerUrlProvider = { Result.success(null) },
|
||||
) = RustMatrixClientFactory(
|
||||
cacheDirectory = cacheDirectory,
|
||||
appCoroutineScope = backgroundScope,
|
||||
@@ -65,4 +67,5 @@ fun TestScope.createRustMatrixClientFactory(
|
||||
clientBuilderProvider = clientBuilderProvider,
|
||||
sqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(),
|
||||
workManagerScheduler = workManagerScheduler,
|
||||
contentScannerUrlProvider = contentScannerUrlProvider,
|
||||
)
|
||||
|
||||
+2
@@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.test.A_DEVICE_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_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.matrix.test.scanner.FakeContentScanner
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.aSessionData
|
||||
@@ -156,5 +157,6 @@ class RustMatrixClientTest {
|
||||
featureFlagService = FakeFeatureFlagService(),
|
||||
analyticsService = FakeAnalyticsService(),
|
||||
workManagerScheduler = FakeWorkManagerScheduler(submitLambda = {}),
|
||||
contentScanner = FakeContentScanner(),
|
||||
)
|
||||
}
|
||||
|
||||
+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"}"""
|
||||
}
|
||||
+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"))
|
||||
}
|
||||
}
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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.scanner
|
||||
|
||||
import org.matrix.rustcomponents.sdk.Client
|
||||
import org.matrix.rustcomponents.sdk.ContentScanner
|
||||
import org.matrix.rustcomponents.sdk.MediaSource
|
||||
import org.matrix.rustcomponents.sdk.NoHandle
|
||||
import uniffi.matrix_sdk_contentscanner.MediaScanResponse
|
||||
|
||||
class FakeFfiContentScanner(
|
||||
private val scan: suspend (client: Client, mediaSource: MediaSource) -> MediaScanResponse = { _, _ ->
|
||||
MediaScanResponse(clean = true, info = "Just peachy")
|
||||
},
|
||||
) : ContentScanner(NoHandle) {
|
||||
override suspend fun scan(client: Client, mediaSource: MediaSource): MediaScanResponse {
|
||||
return scan.invoke(client, mediaSource)
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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.scanner
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient
|
||||
import io.element.android.libraries.matrix.test.media.aMediaSource
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import uniffi.matrix_sdk_contentscanner.MediaScanResponse
|
||||
|
||||
class RustContentScannerTest {
|
||||
@Test
|
||||
fun `test successful valid scan`() = runTest {
|
||||
val scanner = RustContentScanner(
|
||||
client = FakeFfiClient(),
|
||||
rustScanner = FakeFfiContentScanner(),
|
||||
)
|
||||
|
||||
scanner.scan(aMediaSource()).run {
|
||||
assertThat(isSuccess).isTrue()
|
||||
assertThat(getOrNull()).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test successful invalid scan`() = runTest {
|
||||
val scanner = RustContentScanner(
|
||||
client = FakeFfiClient(),
|
||||
rustScanner = FakeFfiContentScanner(scan = { _, _ -> MediaScanResponse(clean = false, info = "Not clean") })
|
||||
)
|
||||
|
||||
scanner.scan(aMediaSource()).run {
|
||||
assertThat(isSuccess).isTrue()
|
||||
assertThat(getOrNull()).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test failed scan`() = runTest {
|
||||
val scanner = RustContentScanner(
|
||||
client = FakeFfiClient(),
|
||||
rustScanner = FakeFfiContentScanner(scan = { _, _ -> throw IllegalStateException("BOOM") })
|
||||
)
|
||||
|
||||
scanner.scan(aMediaSource()).run {
|
||||
assertThat(isFailure).isTrue()
|
||||
assertThat(getOrNull()).isNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
+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")
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user