From b622ad60007bcb3a7ec03f2f89195b7ec1e3211f Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 16 Jan 2023 18:11:54 +0100 Subject: [PATCH] Add some interfaces for matrix module --- .../io/element/android/x/di/RoomComponent.kt | 2 - .../element/android/x/di/SessionComponent.kt | 2 - .../messages/timeline/TimelineItemsFactory.kt | 2 +- .../messages/timeline/TimelinePresenter.kt | 10 +- .../io/element/android/x/matrix/Matrix.kt | 2 +- .../element/android/x/matrix/MatrixClient.kt | 185 ++-------------- .../android/x/matrix/RustMatrixClient.kt | 202 ++++++++++++++++++ .../android/x/matrix/room/MatrixRoom.kt | 115 ++-------- .../android/x/matrix/room/RustMatrixRoom.kt | 136 ++++++++++++ .../x/matrix/timeline/MatrixTimeline.kt | 143 ++----------- .../x/matrix/timeline/RustMatrixTimeline.kt | 160 ++++++++++++++ 11 files changed, 545 insertions(+), 414 deletions(-) create mode 100644 libraries/matrix/src/main/java/io/element/android/x/matrix/RustMatrixClient.kt create mode 100644 libraries/matrix/src/main/java/io/element/android/x/matrix/room/RustMatrixRoom.kt create mode 100644 libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/RustMatrixTimeline.kt diff --git a/app/src/main/java/io/element/android/x/di/RoomComponent.kt b/app/src/main/java/io/element/android/x/di/RoomComponent.kt index d5964325ae..cfd7eee471 100644 --- a/app/src/main/java/io/element/android/x/di/RoomComponent.kt +++ b/app/src/main/java/io/element/android/x/di/RoomComponent.kt @@ -27,8 +27,6 @@ import io.element.android.x.matrix.room.MatrixRoom @MergeSubcomponent(RoomScope::class) interface RoomComponent : NodeFactoriesBindings { - fun matrixRoom(): MatrixRoom - @Subcomponent.Builder interface Builder { @BindsInstance diff --git a/app/src/main/java/io/element/android/x/di/SessionComponent.kt b/app/src/main/java/io/element/android/x/di/SessionComponent.kt index a9716e57c1..8da31df8eb 100644 --- a/app/src/main/java/io/element/android/x/di/SessionComponent.kt +++ b/app/src/main/java/io/element/android/x/di/SessionComponent.kt @@ -27,8 +27,6 @@ import io.element.android.x.matrix.MatrixClient @MergeSubcomponent(SessionScope::class) interface SessionComponent : NodeFactoriesBindings, RoomComponent.ParentBindings { - fun matrixClient(): MatrixClient - @Subcomponent.Builder interface Builder { @BindsInstance diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineItemsFactory.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineItemsFactory.kt index ed9594aa8c..0b8a3bfc48 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineItemsFactory.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelineItemsFactory.kt @@ -22,8 +22,8 @@ import io.element.android.x.features.messages.timeline.diff.CacheInvalidator import io.element.android.x.features.messages.timeline.diff.MatrixTimelineItemsDiffCallback import io.element.android.x.features.messages.timeline.model.AggregatedReaction import io.element.android.x.features.messages.timeline.model.MessagesItemGroupPosition -import io.element.android.x.features.messages.timeline.model.TimelineItemReactions import io.element.android.x.features.messages.timeline.model.TimelineItem +import io.element.android.x.features.messages.timeline.model.TimelineItemReactions import io.element.android.x.features.messages.timeline.model.content.TimelineItemContent import io.element.android.x.features.messages.timeline.model.content.TimelineItemEmoteContent import io.element.android.x.features.messages.timeline.model.content.TimelineItemEncryptedContent diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelinePresenter.kt b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelinePresenter.kt index 271c830c28..28db417053 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelinePresenter.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/timeline/TimelinePresenter.kt @@ -25,6 +25,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import io.element.android.x.architecture.Presenter +import io.element.android.x.core.coroutine.CoroutineDispatchers import io.element.android.x.matrix.MatrixClient import io.element.android.x.matrix.core.EventId import io.element.android.x.matrix.room.MatrixRoom @@ -33,7 +34,6 @@ import io.element.android.x.matrix.timeline.MatrixTimelineItem import io.element.android.x.matrix.ui.MatrixItemHelper import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -42,15 +42,15 @@ import javax.inject.Inject private const val PAGINATION_COUNT = 50 class TimelinePresenter @Inject constructor( - private val appCoroutineScope: CoroutineScope, - private val client: MatrixClient, - private val room: MatrixRoom + coroutineDispatchers: CoroutineDispatchers, + client: MatrixClient, + room: MatrixRoom, ) : Presenter { private val timeline = room.timeline() private val matrixItemHelper = MatrixItemHelper(client) private val timelineItemsFactory = - TimelineItemsFactory(matrixItemHelper, room, Dispatchers.Default) + TimelineItemsFactory(matrixItemHelper, room, coroutineDispatchers.computation) private class TimelineCallback(private val coroutineScope: CoroutineScope, private val timelineItemsFactory: TimelineItemsFactory) : MatrixTimeline.Callback { override fun onPushedTimelineItem(timelineItem: MatrixTimelineItem) { diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/Matrix.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/Matrix.kt index 3c20fd677f..71c649e13e 100644 --- a/libraries/matrix/src/main/java/io/element/android/x/matrix/Matrix.kt +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/Matrix.kt @@ -97,7 +97,7 @@ class Matrix @Inject constructor( } private fun createMatrixClient(client: Client): MatrixClient { - return MatrixClient( + return RustMatrixClient( client = client, sessionStore = sessionStore, coroutineScope = coroutineScope, diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/MatrixClient.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/MatrixClient.kt index 48bb8816d0..d0d446909b 100644 --- a/libraries/matrix/src/main/java/io/element/android/x/matrix/MatrixClient.kt +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/MatrixClient.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,187 +16,30 @@ package io.element.android.x.matrix -import io.element.android.x.core.coroutine.CoroutineDispatchers import io.element.android.x.matrix.core.RoomId import io.element.android.x.matrix.core.SessionId import io.element.android.x.matrix.core.UserId import io.element.android.x.matrix.media.MediaResolver -import io.element.android.x.matrix.media.RustMediaResolver import io.element.android.x.matrix.room.MatrixRoom import io.element.android.x.matrix.room.RoomSummaryDataSource -import io.element.android.x.matrix.room.RustRoomSummaryDataSource -import io.element.android.x.matrix.session.SessionStore -import io.element.android.x.matrix.session.sessionId -import io.element.android.x.matrix.sync.SlidingSyncObserverProxy -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.withContext -import org.matrix.rustcomponents.sdk.Client -import org.matrix.rustcomponents.sdk.ClientDelegate import org.matrix.rustcomponents.sdk.MediaSource -import org.matrix.rustcomponents.sdk.RequiredState -import org.matrix.rustcomponents.sdk.SlidingSyncMode -import org.matrix.rustcomponents.sdk.SlidingSyncViewBuilder -import org.matrix.rustcomponents.sdk.StoppableSpawn -import timber.log.Timber import java.io.Closeable -import java.io.File -import java.util.concurrent.atomic.AtomicBoolean -class MatrixClient internal constructor( - private val client: Client, - private val sessionStore: SessionStore, - private val coroutineScope: CoroutineScope, - private val dispatchers: CoroutineDispatchers, - private val baseDirectory: File, -) : Closeable { - - val sessionId: SessionId = client.session().sessionId() - - private val clientDelegate = object : ClientDelegate { - override fun didReceiveAuthError(isSoftLogout: Boolean) { - Timber.v("didReceiveAuthError()") - } - - override fun didReceiveSyncUpdate() { - Timber.v("didReceiveSyncUpdate()") - } - - override fun didUpdateRestoreToken() { - Timber.v("didUpdateRestoreToken()") - } - } - - private val slidingSyncView = SlidingSyncViewBuilder() - .timelineLimit(limit = 10u) - .requiredState( - requiredState = listOf( - RequiredState(key = "m.room.avatar", value = ""), - RequiredState(key = "m.room.encryption", value = ""), - ) - ) - .name(name = "HomeScreenView") - .syncMode(mode = SlidingSyncMode.SELECTIVE) - .addRange(0u, 30u) - .build() - - private val slidingSync = client - .slidingSync() - .homeserver("https://slidingsync.lab.element.dev") - .withCommonExtensions() - // .coldCache("ElementX") - .addView(slidingSyncView) - .build() - - private val slidingSyncObserverProxy = SlidingSyncObserverProxy(coroutineScope, dispatchers) - private val roomSummaryDataSource: RustRoomSummaryDataSource = - RustRoomSummaryDataSource( - slidingSyncObserverProxy.updateSummaryFlow, - slidingSync, - slidingSyncView, - dispatchers, - ::onRestartSync - ) - private var slidingSyncObserverToken: StoppableSpawn? = null - - private val mediaResolver = RustMediaResolver(this) - private val isSyncing = AtomicBoolean(false) - - init { - client.setDelegate(clientDelegate) - } - - private fun onRestartSync() { - slidingSyncObserverToken = slidingSync.sync() - } - - fun getRoom(roomId: RoomId): MatrixRoom? { - val slidingSyncRoom = slidingSync.getRoom(roomId.value) ?: return null - val room = slidingSyncRoom.fullRoom() ?: return null - return MatrixRoom( - slidingSyncUpdateFlow = slidingSyncObserverProxy.updateSummaryFlow, - slidingSyncRoom = slidingSyncRoom, - room = room, - coroutineScope = coroutineScope, - coroutineDispatchers = dispatchers - ) - } - - fun startSync() { - if (isSyncing.compareAndSet(false, true)) { - roomSummaryDataSource.startSync() - slidingSync.setObserver(slidingSyncObserverProxy) - slidingSyncObserverToken = slidingSync.sync() - } - } - - fun stopSync() { - if (isSyncing.compareAndSet(true, false)) { - roomSummaryDataSource.stopSync() - slidingSync.setObserver(null) - slidingSyncObserverToken?.cancel() - } - } - - fun roomSummaryDataSource(): RoomSummaryDataSource = roomSummaryDataSource - - fun mediaResolver(): MediaResolver = mediaResolver - - override fun close() { - stopSync() - roomSummaryDataSource.close() - client.setDelegate(null) - } - - suspend fun logout() = withContext(dispatchers.io) { - close() - try { - client.logout() - } catch (failure: Throwable) { - Timber.e(failure, "Fail to call logout on HS. Still delete local files.") - } - baseDirectory.deleteSessionDirectory(userID = client.userId()) - sessionStore.reset() - } - - fun userId(): UserId = UserId(client.userId()) - - suspend fun loadUserDisplayName(): Result = withContext(dispatchers.io) { - runCatching { - client.displayName() - } - } - - suspend fun loadUserAvatarURLString(): Result = withContext(dispatchers.io) { - runCatching { - client.avatarUrl() - } - } - - @OptIn(ExperimentalUnsignedTypes::class) - suspend fun loadMediaContentForSource(source: MediaSource): Result = - withContext(dispatchers.io) { - runCatching { - client.getMediaContent(source).toUByteArray().toByteArray() - } - } - - @OptIn(ExperimentalUnsignedTypes::class) +interface MatrixClient : Closeable { + val sessionId: SessionId + fun getRoom(roomId: RoomId): MatrixRoom? + fun startSync() + fun stopSync() + fun roomSummaryDataSource(): RoomSummaryDataSource + fun mediaResolver(): MediaResolver + suspend fun logout() + fun userId(): UserId + suspend fun loadUserDisplayName(): Result + suspend fun loadUserAvatarURLString(): Result + suspend fun loadMediaContentForSource(source: MediaSource): Result suspend fun loadMediaThumbnailForSource( source: MediaSource, width: Long, height: Long - ): Result = - withContext(dispatchers.io) { - runCatching { - client.getMediaThumbnail(source, width.toULong(), height.toULong()).toUByteArray() - .toByteArray() - } - } - - private fun File.deleteSessionDirectory(userID: String): Boolean { - // Rust sanitises the user ID replacing invalid characters with an _ - val sanitisedUserID = userID.replace(":", "_") - val sessionDirectory = File(this, sanitisedUserID) - return sessionDirectory.deleteRecursively() - } + ): Result } diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/RustMatrixClient.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/RustMatrixClient.kt new file mode 100644 index 0000000000..433b23df04 --- /dev/null +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/RustMatrixClient.kt @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.x.matrix + +import io.element.android.x.core.coroutine.CoroutineDispatchers +import io.element.android.x.matrix.core.RoomId +import io.element.android.x.matrix.core.SessionId +import io.element.android.x.matrix.core.UserId +import io.element.android.x.matrix.media.MediaResolver +import io.element.android.x.matrix.media.RustMediaResolver +import io.element.android.x.matrix.room.MatrixRoom +import io.element.android.x.matrix.room.RoomSummaryDataSource +import io.element.android.x.matrix.room.RustMatrixRoom +import io.element.android.x.matrix.room.RustRoomSummaryDataSource +import io.element.android.x.matrix.session.SessionStore +import io.element.android.x.matrix.session.sessionId +import io.element.android.x.matrix.sync.SlidingSyncObserverProxy +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.withContext +import org.matrix.rustcomponents.sdk.Client +import org.matrix.rustcomponents.sdk.ClientDelegate +import org.matrix.rustcomponents.sdk.MediaSource +import org.matrix.rustcomponents.sdk.RequiredState +import org.matrix.rustcomponents.sdk.SlidingSyncMode +import org.matrix.rustcomponents.sdk.SlidingSyncViewBuilder +import org.matrix.rustcomponents.sdk.StoppableSpawn +import timber.log.Timber +import java.io.File +import java.util.concurrent.atomic.AtomicBoolean + +internal class RustMatrixClient internal constructor( + private val client: Client, + private val sessionStore: SessionStore, + private val coroutineScope: CoroutineScope, + private val dispatchers: CoroutineDispatchers, + private val baseDirectory: File, +) : MatrixClient { + + override val sessionId: SessionId = client.session().sessionId() + + private val clientDelegate = object : ClientDelegate { + override fun didReceiveAuthError(isSoftLogout: Boolean) { + Timber.v("didReceiveAuthError()") + } + + override fun didReceiveSyncUpdate() { + Timber.v("didReceiveSyncUpdate()") + } + + override fun didUpdateRestoreToken() { + Timber.v("didUpdateRestoreToken()") + } + } + + private val slidingSyncView = SlidingSyncViewBuilder() + .timelineLimit(limit = 10u) + .requiredState( + requiredState = listOf( + RequiredState(key = "m.room.avatar", value = ""), + RequiredState(key = "m.room.encryption", value = ""), + ) + ) + .name(name = "HomeScreenView") + .syncMode(mode = SlidingSyncMode.SELECTIVE) + .addRange(0u, 30u) + .build() + + private val slidingSync = client + .slidingSync() + .homeserver("https://slidingsync.lab.element.dev") + .withCommonExtensions() + // .coldCache("ElementX") + .addView(slidingSyncView) + .build() + + private val slidingSyncObserverProxy = SlidingSyncObserverProxy(coroutineScope, dispatchers) + private val roomSummaryDataSource: RustRoomSummaryDataSource = + RustRoomSummaryDataSource( + slidingSyncObserverProxy.updateSummaryFlow, + slidingSync, + slidingSyncView, + dispatchers, + ::onRestartSync + ) + private var slidingSyncObserverToken: StoppableSpawn? = null + + private val mediaResolver = RustMediaResolver(this) + private val isSyncing = AtomicBoolean(false) + + init { + client.setDelegate(clientDelegate) + } + + private fun onRestartSync() { + slidingSyncObserverToken = slidingSync.sync() + } + + override fun getRoom(roomId: RoomId): MatrixRoom? { + val slidingSyncRoom = slidingSync.getRoom(roomId.value) ?: return null + val room = slidingSyncRoom.fullRoom() ?: return null + return RustMatrixRoom( + slidingSyncUpdateFlow = slidingSyncObserverProxy.updateSummaryFlow, + slidingSyncRoom = slidingSyncRoom, + room = room, + coroutineScope = coroutineScope, + coroutineDispatchers = dispatchers + ) + } + + override fun startSync() { + if (isSyncing.compareAndSet(false, true)) { + roomSummaryDataSource.startSync() + slidingSync.setObserver(slidingSyncObserverProxy) + slidingSyncObserverToken = slidingSync.sync() + } + } + + override fun stopSync() { + if (isSyncing.compareAndSet(true, false)) { + roomSummaryDataSource.stopSync() + slidingSync.setObserver(null) + slidingSyncObserverToken?.cancel() + } + } + + override fun roomSummaryDataSource(): RoomSummaryDataSource = roomSummaryDataSource + + override fun mediaResolver(): MediaResolver = mediaResolver + + override fun close() { + stopSync() + roomSummaryDataSource.close() + client.setDelegate(null) + } + + override suspend fun logout() = withContext(dispatchers.io) { + close() + try { + client.logout() + } catch (failure: Throwable) { + Timber.e(failure, "Fail to call logout on HS. Still delete local files.") + } + baseDirectory.deleteSessionDirectory(userID = client.userId()) + sessionStore.reset() + } + + override fun userId(): UserId = UserId(client.userId()) + + override suspend fun loadUserDisplayName(): Result = withContext(dispatchers.io) { + runCatching { + client.displayName() + } + } + + override suspend fun loadUserAvatarURLString(): Result = withContext(dispatchers.io) { + runCatching { + client.avatarUrl() + } + } + + @OptIn(ExperimentalUnsignedTypes::class) + override suspend fun loadMediaContentForSource(source: MediaSource): Result = + withContext(dispatchers.io) { + runCatching { + client.getMediaContent(source).toUByteArray().toByteArray() + } + } + + @OptIn(ExperimentalUnsignedTypes::class) + override suspend fun loadMediaThumbnailForSource( + source: MediaSource, + width: Long, + height: Long + ): Result = + withContext(dispatchers.io) { + runCatching { + client.getMediaThumbnail(source, width.toULong(), height.toULong()).toUByteArray() + .toByteArray() + } + } + + private fun File.deleteSessionDirectory(userID: String): Boolean { + // Rust sanitises the user ID replacing invalid characters with an _ + val sanitisedUserID = userID.replace(":", "_") + val sessionDirectory = File(this, sanitisedUserID) + return sessionDirectory.deleteRecursively() + } +} diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/room/MatrixRoom.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/MatrixRoom.kt index 681b8839ee..83238edc77 100644 --- a/libraries/matrix/src/main/java/io/element/android/x/matrix/room/MatrixRoom.kt +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/MatrixRoom.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,120 +16,33 @@ package io.element.android.x.matrix.room -import io.element.android.x.core.coroutine.CoroutineDispatchers import io.element.android.x.matrix.core.EventId import io.element.android.x.matrix.core.RoomId import io.element.android.x.matrix.timeline.MatrixTimeline -import kotlinx.coroutines.CoroutineScope +import io.element.android.x.matrix.timeline.RustMatrixTimeline import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.withContext -import org.matrix.rustcomponents.sdk.Room -import org.matrix.rustcomponents.sdk.SlidingSyncRoom -import org.matrix.rustcomponents.sdk.UpdateSummary -import org.matrix.rustcomponents.sdk.genTransactionId -import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown - -class MatrixRoom( - private val slidingSyncUpdateFlow: Flow, - private val slidingSyncRoom: SlidingSyncRoom, - private val room: Room, - private val coroutineScope: CoroutineScope, - private val coroutineDispatchers: CoroutineDispatchers, -) { - - fun syncUpdateFlow(): Flow { - return slidingSyncUpdateFlow - .filter { - it.rooms.contains(room.id()) - } - .map { - System.currentTimeMillis() - } - .onStart { emit(System.currentTimeMillis()) } - } - - fun timeline(): MatrixTimeline { - return MatrixTimeline( - matrixRoom = this, - room = room, - slidingSyncRoom = slidingSyncRoom, - coroutineScope = coroutineScope, - coroutineDispatchers = coroutineDispatchers - ) - } - - val roomId = RoomId(room.id()) +interface MatrixRoom { + val roomId: RoomId val name: String? - get() { - return slidingSyncRoom.name() - } - val bestName: String - get() { - return name?.takeIf { it.isNotEmpty() } ?: room.id() - } - val displayName: String - get() { - return room.displayName() - } - val topic: String? - get() { - return room.topic() - } - val avatarUrl: String? - get() { - return room.avatarUrl() - } - suspend fun userDisplayName(userId: String): Result = - withContext(coroutineDispatchers.io) { - runCatching { - room.memberDisplayName(userId) - } - } + fun syncUpdateFlow(): Flow - suspend fun userAvatarUrl(userId: String): Result = - withContext(coroutineDispatchers.io) { - runCatching { - room.memberAvatarUrl(userId) - } - } + fun timeline(): MatrixTimeline - suspend fun sendMessage(message: String): Result = withContext(coroutineDispatchers.io) { - val transactionId = genTransactionId() - val content = messageEventContentFromMarkdown(message) - runCatching { - room.send(content, transactionId) - } - } + suspend fun userDisplayName(userId: String): Result - suspend fun editMessage(originalEventId: EventId, message: String): Result = withContext(coroutineDispatchers.io) { - val transactionId = genTransactionId() - // val content = messageEventContentFromMarkdown(message) - runCatching { - room.edit(/* TODO use content */ message, originalEventId.value, transactionId) - } - } + suspend fun userAvatarUrl(userId: String): Result - suspend fun replyMessage(eventId: EventId, message: String): Result = withContext(coroutineDispatchers.io) { - val transactionId = genTransactionId() - // val content = messageEventContentFromMarkdown(message) - runCatching { - room.sendReply(/* TODO use content */ message, eventId.value, transactionId) - } - } + suspend fun sendMessage(message: String): Result - suspend fun redactEvent(eventId: EventId, reason: String? = null) = withContext(coroutineDispatchers.io) { - val transactionId = genTransactionId() - runCatching { - room.redact(eventId.value, reason, transactionId) - } - } + suspend fun editMessage(originalEventId: EventId, message: String): Result + + suspend fun replyMessage(eventId: EventId, message: String): Result + + suspend fun redactEvent(eventId: EventId, reason: String? = null): Result } diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/room/RustMatrixRoom.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/RustMatrixRoom.kt new file mode 100644 index 0000000000..7d7c2414fb --- /dev/null +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/RustMatrixRoom.kt @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.x.matrix.room + +import io.element.android.x.core.coroutine.CoroutineDispatchers +import io.element.android.x.matrix.core.EventId +import io.element.android.x.matrix.core.RoomId +import io.element.android.x.matrix.timeline.MatrixTimeline +import io.element.android.x.matrix.timeline.RustMatrixTimeline +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.withContext +import org.matrix.rustcomponents.sdk.Room +import org.matrix.rustcomponents.sdk.SlidingSyncRoom +import org.matrix.rustcomponents.sdk.UpdateSummary +import org.matrix.rustcomponents.sdk.genTransactionId +import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown + +class RustMatrixRoom( + private val slidingSyncUpdateFlow: Flow, + private val slidingSyncRoom: SlidingSyncRoom, + private val room: Room, + private val coroutineScope: CoroutineScope, + private val coroutineDispatchers: CoroutineDispatchers, +) : MatrixRoom { + + override fun syncUpdateFlow(): Flow { + return slidingSyncUpdateFlow + .filter { + it.rooms.contains(room.id()) + } + .map { + System.currentTimeMillis() + } + .onStart { emit(System.currentTimeMillis()) } + } + + override fun timeline(): MatrixTimeline { + return RustMatrixTimeline( + matrixRoom = this, + room = room, + slidingSyncRoom = slidingSyncRoom, + coroutineScope = coroutineScope, + coroutineDispatchers = coroutineDispatchers + ) + } + + override val roomId = RoomId(room.id()) + + override val name: String? + get() { + return slidingSyncRoom.name() + } + + override val bestName: String + get() { + return name?.takeIf { it.isNotEmpty() } ?: room.id() + } + + override val displayName: String + get() { + return room.displayName() + } + + override val topic: String? + get() { + return room.topic() + } + + override val avatarUrl: String? + get() { + return room.avatarUrl() + } + + override suspend fun userDisplayName(userId: String): Result = + withContext(coroutineDispatchers.io) { + runCatching { + room.memberDisplayName(userId) + } + } + + override suspend fun userAvatarUrl(userId: String): Result = + withContext(coroutineDispatchers.io) { + runCatching { + room.memberAvatarUrl(userId) + } + } + + override suspend fun sendMessage(message: String): Result = withContext(coroutineDispatchers.io) { + val transactionId = genTransactionId() + val content = messageEventContentFromMarkdown(message) + runCatching { + room.send(content, transactionId) + } + } + + override suspend fun editMessage(originalEventId: EventId, message: String): Result = withContext(coroutineDispatchers.io) { + val transactionId = genTransactionId() + // val content = messageEventContentFromMarkdown(message) + runCatching { + room.edit(/* TODO use content */ message, originalEventId.value, transactionId) + } + } + + override suspend fun replyMessage(eventId: EventId, message: String): Result = withContext(coroutineDispatchers.io) { + val transactionId = genTransactionId() + // val content = messageEventContentFromMarkdown(message) + runCatching { + room.sendReply(/* TODO use content */ message, eventId.value, transactionId) + } + } + + override suspend fun redactEvent(eventId: EventId, reason: String?) = withContext(coroutineDispatchers.io) { + val transactionId = genTransactionId() + runCatching { + room.redact(eventId.value, reason, transactionId) + } + } +} diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/MatrixTimeline.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/MatrixTimeline.kt index bf613ca761..2f383cebf2 100644 --- a/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/MatrixTimeline.kt +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/MatrixTimeline.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,150 +16,31 @@ package io.element.android.x.matrix.timeline -import io.element.android.x.core.coroutine.CoroutineDispatchers import io.element.android.x.matrix.core.EventId -import io.element.android.x.matrix.room.MatrixRoom -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.sample -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.matrix.rustcomponents.sdk.PaginationOutcome -import org.matrix.rustcomponents.sdk.Room -import org.matrix.rustcomponents.sdk.SlidingSyncRoom -import org.matrix.rustcomponents.sdk.TimelineChange -import org.matrix.rustcomponents.sdk.TimelineDiff import org.matrix.rustcomponents.sdk.TimelineListener -import timber.log.Timber -import java.util.Collections -class MatrixTimeline( - private val matrixRoom: MatrixRoom, - private val room: Room, - private val slidingSyncRoom: SlidingSyncRoom, - private val coroutineScope: CoroutineScope, - private val coroutineDispatchers: CoroutineDispatchers, -) : TimelineListener { +interface MatrixTimeline { + var callback: Callback? + val hasMoreToLoad: Boolean interface Callback { fun onUpdatedTimelineItem(timelineItem: MatrixTimelineItem) = Unit fun onPushedTimelineItem(timelineItem: MatrixTimelineItem) = Unit } - var callback: Callback? = null - - private val paginationOutcome = MutableStateFlow(PaginationOutcome(true)) - private val timelineItems: MutableStateFlow> = - MutableStateFlow(emptyList()) - - @OptIn(FlowPreview::class) - fun timelineItems(): Flow> { - return timelineItems.sample(50) - } - - val hasMoreToLoad: Boolean - get() { - return paginationOutcome.value.moreMessages - } - - private fun MutableList.applyDiff(diff: TimelineDiff) { - when (diff.change()) { - TimelineChange.PUSH -> { - Timber.v("Apply push on list with size: $size") - val item = diff.push()?.asMatrixTimelineItem() ?: return - callback?.onPushedTimelineItem(item) - add(item) - } - TimelineChange.UPDATE_AT -> { - val updateAtData = diff.updateAt() ?: return - Timber.v("Apply $updateAtData on list with size: $size") - val item = updateAtData.item.asMatrixTimelineItem() - callback?.onUpdatedTimelineItem(item) - set(updateAtData.index.toInt(), item) - } - TimelineChange.INSERT_AT -> { - val insertAtData = diff.insertAt() ?: return - Timber.v("Apply $insertAtData on list with size: $size") - val item = insertAtData.item.asMatrixTimelineItem() - add(insertAtData.index.toInt(), item) - } - TimelineChange.MOVE -> { - val moveData = diff.move() ?: return - Timber.v("Apply $moveData on list with size: $size") - Collections.swap(this, moveData.oldIndex.toInt(), moveData.newIndex.toInt()) - } - TimelineChange.REMOVE_AT -> { - val removeAtData = diff.removeAt() ?: return - Timber.v("Apply $removeAtData on list with size: $size") - removeAt(removeAtData.toInt()) - } - TimelineChange.REPLACE -> { - Timber.v("Apply REPLACE on list with size: $size") - clear() - val items = diff.replace()?.map { it.asMatrixTimelineItem() } ?: return - addAll(items) - } - TimelineChange.POP -> { - Timber.v("Apply POP on list with size: $size") - removeLast() - } - TimelineChange.CLEAR -> { - Timber.v("Apply CLEAR on list with size: $size") - clear() - } - } - } - - suspend fun paginateBackwards(count: Int): Result = withContext(coroutineDispatchers.io) { - if (!paginationOutcome.value.moreMessages) { - return@withContext Result.failure(IllegalStateException("no more message")) - } - runCatching { - paginationOutcome.value = room.paginateBackwards(count.toUShort()) - } - } - - private suspend fun updateTimelineItems(block: MutableList.() -> Unit) = - withContext(coroutineDispatchers.diffUpdateDispatcher) { - val mutableTimelineItems = timelineItems.value.toMutableList() - block(mutableTimelineItems) - timelineItems.value = mutableTimelineItems - } - - fun addListener(timelineListener: TimelineListener) { - slidingSyncRoom.addTimelineListener(timelineListener) - } - - fun initialize() { - addListener(this) - } - - fun dispose() { - slidingSyncRoom.removeTimeline() - } + fun timelineItems(): Flow> + suspend fun paginateBackwards(count: Int): Result + fun addListener(timelineListener: TimelineListener) + fun initialize() + fun dispose() /** * @param message markdown message */ - suspend fun sendMessage(message: String): Result { - return matrixRoom.sendMessage(message) - } + suspend fun sendMessage(message: String): Result - suspend fun editMessage(originalEventId: EventId, message: String): Result { - return matrixRoom.editMessage(originalEventId, message = message) - } + suspend fun editMessage(originalEventId: EventId, message: String): Result - suspend fun replyMessage(inReplyToEventId: EventId, message: String): Result { - return matrixRoom.replyMessage(inReplyToEventId, message) - } - - override fun onUpdate(update: TimelineDiff) { - coroutineScope.launch { - updateTimelineItems { - applyDiff(update) - } - } - } + suspend fun replyMessage(inReplyToEventId: EventId, message: String): Result } diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/RustMatrixTimeline.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/RustMatrixTimeline.kt new file mode 100644 index 0000000000..ecfce4af7a --- /dev/null +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/timeline/RustMatrixTimeline.kt @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.x.matrix.timeline + +import io.element.android.x.core.coroutine.CoroutineDispatchers +import io.element.android.x.matrix.core.EventId +import io.element.android.x.matrix.room.RustMatrixRoom +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.sample +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.matrix.rustcomponents.sdk.PaginationOutcome +import org.matrix.rustcomponents.sdk.Room +import org.matrix.rustcomponents.sdk.SlidingSyncRoom +import org.matrix.rustcomponents.sdk.TimelineChange +import org.matrix.rustcomponents.sdk.TimelineDiff +import org.matrix.rustcomponents.sdk.TimelineListener +import timber.log.Timber +import java.util.Collections + +class RustMatrixTimeline( + private val matrixRoom: RustMatrixRoom, + private val room: Room, + private val slidingSyncRoom: SlidingSyncRoom, + private val coroutineScope: CoroutineScope, + private val coroutineDispatchers: CoroutineDispatchers, +) : TimelineListener, MatrixTimeline { + + override var callback: MatrixTimeline.Callback? = null + + private val paginationOutcome = MutableStateFlow(PaginationOutcome(true)) + private val timelineItems: MutableStateFlow> = + MutableStateFlow(emptyList()) + + @OptIn(FlowPreview::class) + override fun timelineItems(): Flow> { + return timelineItems.sample(50) + } + + override val hasMoreToLoad: Boolean + get() { + return paginationOutcome.value.moreMessages + } + + private fun MutableList.applyDiff(diff: TimelineDiff) { + when (diff.change()) { + TimelineChange.PUSH -> { + Timber.v("Apply push on list with size: $size") + val item = diff.push()?.asMatrixTimelineItem() ?: return + callback?.onPushedTimelineItem(item) + add(item) + } + TimelineChange.UPDATE_AT -> { + val updateAtData = diff.updateAt() ?: return + Timber.v("Apply $updateAtData on list with size: $size") + val item = updateAtData.item.asMatrixTimelineItem() + callback?.onUpdatedTimelineItem(item) + set(updateAtData.index.toInt(), item) + } + TimelineChange.INSERT_AT -> { + val insertAtData = diff.insertAt() ?: return + Timber.v("Apply $insertAtData on list with size: $size") + val item = insertAtData.item.asMatrixTimelineItem() + add(insertAtData.index.toInt(), item) + } + TimelineChange.MOVE -> { + val moveData = diff.move() ?: return + Timber.v("Apply $moveData on list with size: $size") + Collections.swap(this, moveData.oldIndex.toInt(), moveData.newIndex.toInt()) + } + TimelineChange.REMOVE_AT -> { + val removeAtData = diff.removeAt() ?: return + Timber.v("Apply $removeAtData on list with size: $size") + removeAt(removeAtData.toInt()) + } + TimelineChange.REPLACE -> { + Timber.v("Apply REPLACE on list with size: $size") + clear() + val items = diff.replace()?.map { it.asMatrixTimelineItem() } ?: return + addAll(items) + } + TimelineChange.POP -> { + Timber.v("Apply POP on list with size: $size") + removeLast() + } + TimelineChange.CLEAR -> { + Timber.v("Apply CLEAR on list with size: $size") + clear() + } + } + } + + override suspend fun paginateBackwards(count: Int): Result = withContext(coroutineDispatchers.io) { + if (!paginationOutcome.value.moreMessages) { + return@withContext Result.failure(IllegalStateException("no more message")) + } + runCatching { + paginationOutcome.value = room.paginateBackwards(count.toUShort()) + } + } + + private suspend fun updateTimelineItems(block: MutableList.() -> Unit) = + withContext(coroutineDispatchers.diffUpdateDispatcher) { + val mutableTimelineItems = timelineItems.value.toMutableList() + block(mutableTimelineItems) + timelineItems.value = mutableTimelineItems + } + + override fun addListener(timelineListener: TimelineListener) { + slidingSyncRoom.addTimelineListener(timelineListener) + } + + override fun initialize() { + addListener(this) + } + + override fun dispose() { + slidingSyncRoom.removeTimeline() + } + + /** + * @param message markdown message + */ + override suspend fun sendMessage(message: String): Result { + return matrixRoom.sendMessage(message) + } + + override suspend fun editMessage(originalEventId: EventId, message: String): Result { + return matrixRoom.editMessage(originalEventId, message = message) + } + + override suspend fun replyMessage(inReplyToEventId: EventId, message: String): Result { + return matrixRoom.replyMessage(inReplyToEventId, message) + } + + override fun onUpdate(update: TimelineDiff) { + coroutineScope.launch { + updateTimelineItems { + applyDiff(update) + } + } + } +}