Add some interfaces for matrix module

This commit is contained in:
ganfra
2023-01-16 18:11:54 +01:00
parent 12e9402474
commit b622ad6000
11 changed files with 545 additions and 414 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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<TimelineState> {
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) {

View File

@@ -97,7 +97,7 @@ class Matrix @Inject constructor(
}
private fun createMatrixClient(client: Client): MatrixClient {
return MatrixClient(
return RustMatrixClient(
client = client,
sessionStore = sessionStore,
coroutineScope = coroutineScope,

View File

@@ -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<String> = withContext(dispatchers.io) {
runCatching {
client.displayName()
}
}
suspend fun loadUserAvatarURLString(): Result<String> = withContext(dispatchers.io) {
runCatching {
client.avatarUrl()
}
}
@OptIn(ExperimentalUnsignedTypes::class)
suspend fun loadMediaContentForSource(source: MediaSource): Result<ByteArray> =
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<String>
suspend fun loadUserAvatarURLString(): Result<String>
suspend fun loadMediaContentForSource(source: MediaSource): Result<ByteArray>
suspend fun loadMediaThumbnailForSource(
source: MediaSource,
width: Long,
height: Long
): Result<ByteArray> =
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<ByteArray>
}

View File

@@ -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<String> = withContext(dispatchers.io) {
runCatching {
client.displayName()
}
}
override suspend fun loadUserAvatarURLString(): Result<String> = withContext(dispatchers.io) {
runCatching {
client.avatarUrl()
}
}
@OptIn(ExperimentalUnsignedTypes::class)
override suspend fun loadMediaContentForSource(source: MediaSource): Result<ByteArray> =
withContext(dispatchers.io) {
runCatching {
client.getMediaContent(source).toUByteArray().toByteArray()
}
}
@OptIn(ExperimentalUnsignedTypes::class)
override suspend fun loadMediaThumbnailForSource(
source: MediaSource,
width: Long,
height: Long
): Result<ByteArray> =
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()
}
}

View File

@@ -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<UpdateSummary>,
private val slidingSyncRoom: SlidingSyncRoom,
private val room: Room,
private val coroutineScope: CoroutineScope,
private val coroutineDispatchers: CoroutineDispatchers,
) {
fun syncUpdateFlow(): Flow<Long> {
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<String?> =
withContext(coroutineDispatchers.io) {
runCatching {
room.memberDisplayName(userId)
}
}
fun syncUpdateFlow(): Flow<Long>
suspend fun userAvatarUrl(userId: String): Result<String?> =
withContext(coroutineDispatchers.io) {
runCatching {
room.memberAvatarUrl(userId)
}
}
fun timeline(): MatrixTimeline
suspend fun sendMessage(message: String): Result<Unit> = withContext(coroutineDispatchers.io) {
val transactionId = genTransactionId()
val content = messageEventContentFromMarkdown(message)
runCatching {
room.send(content, transactionId)
}
}
suspend fun userDisplayName(userId: String): Result<String?>
suspend fun editMessage(originalEventId: EventId, message: String): Result<Unit> = 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<String?>
suspend fun replyMessage(eventId: EventId, message: String): Result<Unit> = 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<Unit>
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<Unit>
suspend fun replyMessage(eventId: EventId, message: String): Result<Unit>
suspend fun redactEvent(eventId: EventId, reason: String? = null): Result<Unit>
}

View File

@@ -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<UpdateSummary>,
private val slidingSyncRoom: SlidingSyncRoom,
private val room: Room,
private val coroutineScope: CoroutineScope,
private val coroutineDispatchers: CoroutineDispatchers,
) : MatrixRoom {
override fun syncUpdateFlow(): Flow<Long> {
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<String?> =
withContext(coroutineDispatchers.io) {
runCatching {
room.memberDisplayName(userId)
}
}
override suspend fun userAvatarUrl(userId: String): Result<String?> =
withContext(coroutineDispatchers.io) {
runCatching {
room.memberAvatarUrl(userId)
}
}
override suspend fun sendMessage(message: String): Result<Unit> = withContext(coroutineDispatchers.io) {
val transactionId = genTransactionId()
val content = messageEventContentFromMarkdown(message)
runCatching {
room.send(content, transactionId)
}
}
override suspend fun editMessage(originalEventId: EventId, message: String): Result<Unit> = 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<Unit> = 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)
}
}
}

View File

@@ -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<List<MatrixTimelineItem>> =
MutableStateFlow(emptyList())
@OptIn(FlowPreview::class)
fun timelineItems(): Flow<List<MatrixTimelineItem>> {
return timelineItems.sample(50)
}
val hasMoreToLoad: Boolean
get() {
return paginationOutcome.value.moreMessages
}
private fun MutableList<MatrixTimelineItem>.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<Unit> = 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<MatrixTimelineItem>.() -> 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<List<MatrixTimelineItem>>
suspend fun paginateBackwards(count: Int): Result<Unit>
fun addListener(timelineListener: TimelineListener)
fun initialize()
fun dispose()
/**
* @param message markdown message
*/
suspend fun sendMessage(message: String): Result<Unit> {
return matrixRoom.sendMessage(message)
}
suspend fun sendMessage(message: String): Result<Unit>
suspend fun editMessage(originalEventId: EventId, message: String): Result<Unit> {
return matrixRoom.editMessage(originalEventId, message = message)
}
suspend fun editMessage(originalEventId: EventId, message: String): Result<Unit>
suspend fun replyMessage(inReplyToEventId: EventId, message: String): Result<Unit> {
return matrixRoom.replyMessage(inReplyToEventId, message)
}
override fun onUpdate(update: TimelineDiff) {
coroutineScope.launch {
updateTimelineItems {
applyDiff(update)
}
}
}
suspend fun replyMessage(inReplyToEventId: EventId, message: String): Result<Unit>
}

View File

@@ -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<List<MatrixTimelineItem>> =
MutableStateFlow(emptyList())
@OptIn(FlowPreview::class)
override fun timelineItems(): Flow<List<MatrixTimelineItem>> {
return timelineItems.sample(50)
}
override val hasMoreToLoad: Boolean
get() {
return paginationOutcome.value.moreMessages
}
private fun MutableList<MatrixTimelineItem>.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<Unit> = 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<MatrixTimelineItem>.() -> 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<Unit> {
return matrixRoom.sendMessage(message)
}
override suspend fun editMessage(originalEventId: EventId, message: String): Result<Unit> {
return matrixRoom.editMessage(originalEventId, message = message)
}
override suspend fun replyMessage(inReplyToEventId: EventId, message: String): Result<Unit> {
return matrixRoom.replyMessage(inReplyToEventId, message)
}
override fun onUpdate(update: TimelineDiff) {
coroutineScope.launch {
updateTimelineItems {
applyDiff(update)
}
}
}
}