Add some interfaces for matrix module
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -97,7 +97,7 @@ class Matrix @Inject constructor(
|
||||
}
|
||||
|
||||
private fun createMatrixClient(client: Client): MatrixClient {
|
||||
return MatrixClient(
|
||||
return RustMatrixClient(
|
||||
client = client,
|
||||
sessionStore = sessionStore,
|
||||
coroutineScope = coroutineScope,
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user