Add some interfaces for matrix module
This commit is contained in:
parent
12e9402474
commit
b622ad6000
11 changed files with 545 additions and 414 deletions
|
|
@ -27,8 +27,6 @@ import io.element.android.x.matrix.room.MatrixRoom
|
||||||
@MergeSubcomponent(RoomScope::class)
|
@MergeSubcomponent(RoomScope::class)
|
||||||
interface RoomComponent : NodeFactoriesBindings {
|
interface RoomComponent : NodeFactoriesBindings {
|
||||||
|
|
||||||
fun matrixRoom(): MatrixRoom
|
|
||||||
|
|
||||||
@Subcomponent.Builder
|
@Subcomponent.Builder
|
||||||
interface Builder {
|
interface Builder {
|
||||||
@BindsInstance
|
@BindsInstance
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,6 @@ import io.element.android.x.matrix.MatrixClient
|
||||||
@MergeSubcomponent(SessionScope::class)
|
@MergeSubcomponent(SessionScope::class)
|
||||||
interface SessionComponent : NodeFactoriesBindings, RoomComponent.ParentBindings {
|
interface SessionComponent : NodeFactoriesBindings, RoomComponent.ParentBindings {
|
||||||
|
|
||||||
fun matrixClient(): MatrixClient
|
|
||||||
|
|
||||||
@Subcomponent.Builder
|
@Subcomponent.Builder
|
||||||
interface Builder {
|
interface Builder {
|
||||||
@BindsInstance
|
@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.diff.MatrixTimelineItemsDiffCallback
|
||||||
import io.element.android.x.features.messages.timeline.model.AggregatedReaction
|
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.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.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.TimelineItemContent
|
||||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemEmoteContent
|
import io.element.android.x.features.messages.timeline.model.content.TimelineItemEmoteContent
|
||||||
import io.element.android.x.features.messages.timeline.model.content.TimelineItemEncryptedContent
|
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.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import io.element.android.x.architecture.Presenter
|
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.MatrixClient
|
||||||
import io.element.android.x.matrix.core.EventId
|
import io.element.android.x.matrix.core.EventId
|
||||||
import io.element.android.x.matrix.room.MatrixRoom
|
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 io.element.android.x.matrix.ui.MatrixItemHelper
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -42,15 +42,15 @@ import javax.inject.Inject
|
||||||
private const val PAGINATION_COUNT = 50
|
private const val PAGINATION_COUNT = 50
|
||||||
|
|
||||||
class TimelinePresenter @Inject constructor(
|
class TimelinePresenter @Inject constructor(
|
||||||
private val appCoroutineScope: CoroutineScope,
|
coroutineDispatchers: CoroutineDispatchers,
|
||||||
private val client: MatrixClient,
|
client: MatrixClient,
|
||||||
private val room: MatrixRoom
|
room: MatrixRoom,
|
||||||
) : Presenter<TimelineState> {
|
) : Presenter<TimelineState> {
|
||||||
|
|
||||||
private val timeline = room.timeline()
|
private val timeline = room.timeline()
|
||||||
private val matrixItemHelper = MatrixItemHelper(client)
|
private val matrixItemHelper = MatrixItemHelper(client)
|
||||||
private val timelineItemsFactory =
|
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 {
|
private class TimelineCallback(private val coroutineScope: CoroutineScope, private val timelineItemsFactory: TimelineItemsFactory) : MatrixTimeline.Callback {
|
||||||
override fun onPushedTimelineItem(timelineItem: MatrixTimelineItem) {
|
override fun onPushedTimelineItem(timelineItem: MatrixTimelineItem) {
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ class Matrix @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createMatrixClient(client: Client): MatrixClient {
|
private fun createMatrixClient(client: Client): MatrixClient {
|
||||||
return MatrixClient(
|
return RustMatrixClient(
|
||||||
client = client,
|
client = client,
|
||||||
sessionStore = sessionStore,
|
sessionStore = sessionStore,
|
||||||
coroutineScope = coroutineScope,
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -16,187 +16,30 @@
|
||||||
|
|
||||||
package io.element.android.x.matrix
|
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.RoomId
|
||||||
import io.element.android.x.matrix.core.SessionId
|
import io.element.android.x.matrix.core.SessionId
|
||||||
import io.element.android.x.matrix.core.UserId
|
import io.element.android.x.matrix.core.UserId
|
||||||
import io.element.android.x.matrix.media.MediaResolver
|
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.MatrixRoom
|
||||||
import io.element.android.x.matrix.room.RoomSummaryDataSource
|
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.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.Closeable
|
||||||
import java.io.File
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
|
|
||||||
class MatrixClient internal constructor(
|
interface MatrixClient : Closeable {
|
||||||
private val client: Client,
|
val sessionId: SessionId
|
||||||
private val sessionStore: SessionStore,
|
fun getRoom(roomId: RoomId): MatrixRoom?
|
||||||
private val coroutineScope: CoroutineScope,
|
fun startSync()
|
||||||
private val dispatchers: CoroutineDispatchers,
|
fun stopSync()
|
||||||
private val baseDirectory: File,
|
fun roomSummaryDataSource(): RoomSummaryDataSource
|
||||||
) : Closeable {
|
fun mediaResolver(): MediaResolver
|
||||||
|
suspend fun logout()
|
||||||
val sessionId: SessionId = client.session().sessionId()
|
fun userId(): UserId
|
||||||
|
suspend fun loadUserDisplayName(): Result<String>
|
||||||
private val clientDelegate = object : ClientDelegate {
|
suspend fun loadUserAvatarURLString(): Result<String>
|
||||||
override fun didReceiveAuthError(isSoftLogout: Boolean) {
|
suspend fun loadMediaContentForSource(source: MediaSource): Result<ByteArray>
|
||||||
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)
|
|
||||||
suspend fun loadMediaThumbnailForSource(
|
suspend fun loadMediaThumbnailForSource(
|
||||||
source: MediaSource,
|
source: MediaSource,
|
||||||
width: Long,
|
width: Long,
|
||||||
height: Long
|
height: Long
|
||||||
): Result<ByteArray> =
|
): 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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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
|
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.EventId
|
||||||
import io.element.android.x.matrix.core.RoomId
|
import io.element.android.x.matrix.core.RoomId
|
||||||
import io.element.android.x.matrix.timeline.MatrixTimeline
|
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.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?
|
val name: String?
|
||||||
get() {
|
|
||||||
return slidingSyncRoom.name()
|
|
||||||
}
|
|
||||||
|
|
||||||
val bestName: String
|
val bestName: String
|
||||||
get() {
|
|
||||||
return name?.takeIf { it.isNotEmpty() } ?: room.id()
|
|
||||||
}
|
|
||||||
|
|
||||||
val displayName: String
|
val displayName: String
|
||||||
get() {
|
|
||||||
return room.displayName()
|
|
||||||
}
|
|
||||||
|
|
||||||
val topic: String?
|
val topic: String?
|
||||||
get() {
|
|
||||||
return room.topic()
|
|
||||||
}
|
|
||||||
|
|
||||||
val avatarUrl: String?
|
val avatarUrl: String?
|
||||||
get() {
|
|
||||||
return room.avatarUrl()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun userDisplayName(userId: String): Result<String?> =
|
fun syncUpdateFlow(): Flow<Long>
|
||||||
withContext(coroutineDispatchers.io) {
|
|
||||||
runCatching {
|
|
||||||
room.memberDisplayName(userId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun userAvatarUrl(userId: String): Result<String?> =
|
fun timeline(): MatrixTimeline
|
||||||
withContext(coroutineDispatchers.io) {
|
|
||||||
runCatching {
|
|
||||||
room.memberAvatarUrl(userId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun sendMessage(message: String): Result<Unit> = withContext(coroutineDispatchers.io) {
|
suspend fun userDisplayName(userId: String): Result<String?>
|
||||||
val transactionId = genTransactionId()
|
|
||||||
val content = messageEventContentFromMarkdown(message)
|
|
||||||
runCatching {
|
|
||||||
room.send(content, transactionId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun editMessage(originalEventId: EventId, message: String): Result<Unit> = withContext(coroutineDispatchers.io) {
|
suspend fun userAvatarUrl(userId: String): Result<String?>
|
||||||
val transactionId = genTransactionId()
|
|
||||||
// val content = messageEventContentFromMarkdown(message)
|
|
||||||
runCatching {
|
|
||||||
room.edit(/* TODO use content */ message, originalEventId.value, transactionId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun replyMessage(eventId: EventId, message: String): Result<Unit> = withContext(coroutineDispatchers.io) {
|
suspend fun sendMessage(message: String): Result<Unit>
|
||||||
val transactionId = genTransactionId()
|
|
||||||
// val content = messageEventContentFromMarkdown(message)
|
|
||||||
runCatching {
|
|
||||||
room.sendReply(/* TODO use content */ message, eventId.value, transactionId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun redactEvent(eventId: EventId, reason: String? = null) = withContext(coroutineDispatchers.io) {
|
suspend fun editMessage(originalEventId: EventId, message: String): Result<Unit>
|
||||||
val transactionId = genTransactionId()
|
|
||||||
runCatching {
|
suspend fun replyMessage(eventId: EventId, message: String): Result<Unit>
|
||||||
room.redact(eventId.value, reason, transactionId)
|
|
||||||
}
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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
|
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.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.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 org.matrix.rustcomponents.sdk.TimelineListener
|
||||||
import timber.log.Timber
|
|
||||||
import java.util.Collections
|
|
||||||
|
|
||||||
class MatrixTimeline(
|
interface MatrixTimeline {
|
||||||
private val matrixRoom: MatrixRoom,
|
var callback: Callback?
|
||||||
private val room: Room,
|
val hasMoreToLoad: Boolean
|
||||||
private val slidingSyncRoom: SlidingSyncRoom,
|
|
||||||
private val coroutineScope: CoroutineScope,
|
|
||||||
private val coroutineDispatchers: CoroutineDispatchers,
|
|
||||||
) : TimelineListener {
|
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun onUpdatedTimelineItem(timelineItem: MatrixTimelineItem) = Unit
|
fun onUpdatedTimelineItem(timelineItem: MatrixTimelineItem) = Unit
|
||||||
fun onPushedTimelineItem(timelineItem: MatrixTimelineItem) = Unit
|
fun onPushedTimelineItem(timelineItem: MatrixTimelineItem) = Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
var callback: Callback? = null
|
fun timelineItems(): Flow<List<MatrixTimelineItem>>
|
||||||
|
suspend fun paginateBackwards(count: Int): Result<Unit>
|
||||||
private val paginationOutcome = MutableStateFlow(PaginationOutcome(true))
|
fun addListener(timelineListener: TimelineListener)
|
||||||
private val timelineItems: MutableStateFlow<List<MatrixTimelineItem>> =
|
fun initialize()
|
||||||
MutableStateFlow(emptyList())
|
fun dispose()
|
||||||
|
|
||||||
@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()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param message markdown message
|
* @param message markdown message
|
||||||
*/
|
*/
|
||||||
suspend fun sendMessage(message: String): Result<Unit> {
|
suspend fun sendMessage(message: String): Result<Unit>
|
||||||
return matrixRoom.sendMessage(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun editMessage(originalEventId: EventId, message: String): Result<Unit> {
|
suspend fun editMessage(originalEventId: EventId, message: String): Result<Unit>
|
||||||
return matrixRoom.editMessage(originalEventId, message = message)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun replyMessage(inReplyToEventId: 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue