Split module matrix to matrix.api with interfaces and data classes and matrix.impl with Rust implementation.

This commit is contained in:
Benoit Marty 2023-03-02 16:14:55 +01:00 committed by Benoit Marty
parent 6677f80abe
commit b8467e547c
56 changed files with 256 additions and 78 deletions

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

View file

@ -0,0 +1,218 @@
/*
* 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.
* 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.libraries.matrix.impl
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.MatrixClient
import io.element.android.libraries.matrix.core.RoomId
import io.element.android.libraries.matrix.core.UserId
import io.element.android.libraries.matrix.impl.media.RustMediaResolver
import io.element.android.libraries.matrix.impl.room.RustMatrixRoom
import io.element.android.libraries.matrix.impl.room.RustRoomSummaryDataSource
import io.element.android.libraries.matrix.impl.sync.SlidingSyncObserverProxy
import io.element.android.libraries.matrix.media.MediaResolver
import io.element.android.libraries.matrix.room.MatrixRoom
import io.element.android.libraries.matrix.room.RoomSummaryDataSource
import io.element.android.libraries.sessionstorage.SessionStore
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.SlidingSyncRequestListFilters
import org.matrix.rustcomponents.sdk.SlidingSyncViewBuilder
import org.matrix.rustcomponents.sdk.TaskHandle
import timber.log.Timber
import java.io.File
import java.util.concurrent.atomic.AtomicBoolean
class RustMatrixClient 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: UserId = UserId(client.userId())
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 slidingSyncFilters by lazy {
SlidingSyncRequestListFilters(
isDm = null,
spaces = emptyList(),
isEncrypted = null,
isInvite = false,
isTombstoned = false,
roomTypes = emptyList(),
notRoomTypes = listOf("m.space"),
roomNameLike = null,
tags = emptyList(),
notTags = emptyList()
)
}
private val visibleRoomsView = SlidingSyncViewBuilder()
.timelineLimit(limit = 1u)
.requiredState(
requiredState = listOf(
RequiredState(key = "m.room.avatar", value = ""),
RequiredState(key = "m.room.encryption", value = ""),
)
)
.filters(slidingSyncFilters)
.name(name = "CurrentlyVisibleRooms")
.sendUpdatesForItems(true)
.syncMode(mode = SlidingSyncMode.SELECTIVE)
.addRange(0u, 20u)
.build()
private val slidingSync = client
.slidingSync()
.homeserver("https://slidingsync.lab.matrix.org")
.withCommonExtensions()
.coldCache("ElementX")
.addView(visibleRoomsView)
.build()
private val slidingSyncObserverProxy = SlidingSyncObserverProxy(coroutineScope)
private val roomSummaryDataSource: RustRoomSummaryDataSource =
RustRoomSummaryDataSource(
slidingSyncObserverProxy.updateSummaryFlow,
slidingSync,
visibleRoomsView,
dispatchers,
::onRestartSync
)
private var slidingSyncObserverToken: TaskHandle? = null
private val mediaResolver = RustMediaResolver(this)
private val isSyncing = AtomicBoolean(false)
init {
client.setDelegate(clientDelegate)
roomSummaryDataSource.init()
slidingSync.setObserver(slidingSyncObserverProxy)
}
private fun onRestartSync() {
stopSync()
startSync()
}
override fun getRoom(roomId: RoomId): MatrixRoom? {
val slidingSyncRoom = slidingSync.getRoom(roomId.value) ?: return null
val fullRoom = slidingSyncRoom.fullRoom() ?: return null
return RustMatrixRoom(
slidingSyncUpdateFlow = slidingSyncObserverProxy.updateSummaryFlow,
slidingSyncRoom = slidingSyncRoom,
innerRoom = fullRoom,
coroutineScope = coroutineScope,
coroutineDispatchers = dispatchers
)
}
override fun roomSummaryDataSource(): RoomSummaryDataSource = roomSummaryDataSource
override fun mediaResolver(): MediaResolver = mediaResolver
override fun startSync() {
if (client.isSoftLogout()) return
if (isSyncing.compareAndSet(false, true)) {
slidingSyncObserverToken = slidingSync.sync()
}
}
override fun stopSync() {
if (isSyncing.compareAndSet(true, false)) {
slidingSyncObserverToken?.cancel()
}
}
override fun close() {
stopSync()
slidingSync.setObserver(null)
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.removeSession(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

@ -0,0 +1,129 @@
/*
* 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.
* 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.libraries.matrix.impl.auth
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.MatrixClient
import io.element.android.libraries.matrix.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.core.SessionId
import io.element.android.libraries.matrix.core.UserId
import io.element.android.libraries.matrix.impl.RustMatrixClient
import io.element.android.libraries.matrix.impl.util.logError
import io.element.android.libraries.matrix.session.SessionData
import io.element.android.libraries.sessionstorage.SessionStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.AuthenticationService
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.ClientBuilder
import org.matrix.rustcomponents.sdk.Session
import timber.log.Timber
import java.io.File
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class RustMatrixAuthenticationService @Inject constructor(
private val baseDirectory: File,
private val coroutineScope: CoroutineScope,
private val coroutineDispatchers: CoroutineDispatchers,
private val sessionStore: SessionStore,
private val authService: AuthenticationService,
) : MatrixAuthenticationService {
override fun isLoggedIn(): Flow<Boolean> {
return sessionStore.isLoggedIn()
}
override suspend fun getLatestSessionId(): SessionId? = withContext(coroutineDispatchers.io) {
sessionStore.getLatestSession()?.userId?.let { UserId(it) }
}
override suspend fun restoreSession(sessionId: SessionId) = withContext(coroutineDispatchers.io) {
sessionStore.getSession(sessionId.value)
?.let { sessionData ->
try {
ClientBuilder()
.basePath(baseDirectory.absolutePath)
.username(sessionData.userId)
.build().apply {
restoreSession(sessionData.toSession())
}
} catch (throwable: Throwable) {
logError(throwable)
null
}
}?.let {
createMatrixClient(it)
}
}
override fun getHomeserver(): String? = authService.homeserverDetails()?.url()
override fun getHomeserverOrDefault(): String = getHomeserver() ?: "matrix.org"
override suspend fun setHomeserver(homeserver: String) {
withContext(coroutineDispatchers.io) {
authService.configureHomeserver(homeserver)
}
}
override suspend fun login(username: String, password: String): SessionId =
withContext(coroutineDispatchers.io) {
val client = try {
authService.login(username, password, "ElementX Android", null)
} catch (failure: Throwable) {
Timber.e(failure, "Fail login")
throw failure
}
val session = client.session()
sessionStore.storeData(session.toSessionData())
SessionId(session.userId)
}
private fun createMatrixClient(client: Client): MatrixClient {
return RustMatrixClient(
client = client,
sessionStore = sessionStore,
coroutineScope = coroutineScope,
dispatchers = coroutineDispatchers,
baseDirectory = baseDirectory,
)
}
}
private fun SessionData.toSession() = Session(
accessToken = accessToken,
refreshToken = refreshToken,
userId = userId,
deviceId = deviceId,
homeserverUrl = homeserverUrl,
isSoftLogout = isSoftLogout,
slidingSyncProxy = slidingSyncProxy,
)
private fun Session.toSessionData() = SessionData(
userId = userId,
deviceId = deviceId,
accessToken = accessToken,
refreshToken = refreshToken,
homeserverUrl = homeserverUrl,
isSoftLogout = isSoftLogout,
slidingSyncProxy = slidingSyncProxy,
)

View file

@ -0,0 +1,36 @@
/*
* 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.
* 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.libraries.matrix.impl.di
import com.squareup.anvil.annotations.ContributesTo
import dagger.Module
import dagger.Provides
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn
import org.matrix.rustcomponents.sdk.AuthenticationService
import java.io.File
@Module
@ContributesTo(AppScope::class)
object MatrixModule {
@Provides
@SingleIn(AppScope::class)
fun providesRustAuthenticationService(baseDirectory: File): AuthenticationService {
return AuthenticationService(baseDirectory.absolutePath, null, null)
}
}

View file

@ -0,0 +1,43 @@
/*
* 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.
* 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.libraries.matrix.impl.media
import io.element.android.libraries.matrix.MatrixClient
import io.element.android.libraries.matrix.media.MediaResolver
import org.matrix.rustcomponents.sdk.mediaSourceFromUrl
internal class RustMediaResolver(private val client: MatrixClient) : MediaResolver {
override suspend fun resolve(url: String?, kind: MediaResolver.Kind): ByteArray? {
if (url.isNullOrEmpty()) return null
val mediaSource = mediaSourceFromUrl(url)
return resolve(MediaResolver.Meta(mediaSource, kind))
}
override suspend fun resolve(meta: MediaResolver.Meta): ByteArray? {
val source = meta.source ?: return null
val kind = meta.kind
return when (kind) {
is MediaResolver.Kind.Content -> client.loadMediaContentForSource(source)
is MediaResolver.Kind.Thumbnail -> client.loadMediaThumbnailForSource(
source,
kind.width.toLong(),
kind.height.toLong()
)
}.getOrNull()
}
}

View file

@ -0,0 +1,46 @@
/*
* 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.
* 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.libraries.matrix.impl.room
import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory
import io.element.android.libraries.matrix.core.RoomId
import io.element.android.libraries.matrix.room.RoomSummaryDetails
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.SlidingSyncRoom
class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFactory = RoomMessageFactory()) {
fun create(slidingSyncRoom: SlidingSyncRoom, room: Room?): RoomSummaryDetails {
val latestRoomMessage = slidingSyncRoom.latestRoomMessage()?.let {
roomMessageFactory.create(it)
}
val computedLastMessage = when {
latestRoomMessage == null -> null
slidingSyncRoom.isDm() == true -> latestRoomMessage.body
else -> "${latestRoomMessage.sender.value}: ${latestRoomMessage.body}"
}
return RoomSummaryDetails(
roomId = RoomId(slidingSyncRoom.roomId()),
name = slidingSyncRoom.name() ?: slidingSyncRoom.roomId(),
isDirect = slidingSyncRoom.isDm() ?: false,
avatarURLString = room?.avatarUrl(),
unreadNotificationCount = slidingSyncRoom.unreadNotifications().notificationCount().toInt(),
lastMessage = computedLastMessage,
lastMessageTimestamp = latestRoomMessage?.originServerTs
)
}
}

View file

@ -0,0 +1,143 @@
/*
* 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.
* 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.libraries.matrix.impl.room
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
import io.element.android.libraries.matrix.core.EventId
import io.element.android.libraries.matrix.core.RoomId
import io.element.android.libraries.matrix.room.MatrixRoom
import io.element.android.libraries.matrix.timeline.MatrixTimeline
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 innerRoom: Room,
private val coroutineScope: CoroutineScope,
private val coroutineDispatchers: CoroutineDispatchers,
) : MatrixRoom {
override fun syncUpdateFlow(): Flow<Long> {
return slidingSyncUpdateFlow
.filter {
it.rooms.contains(innerRoom.id())
}
.map {
System.currentTimeMillis()
}
.onStart { emit(System.currentTimeMillis()) }
}
override fun timeline(): MatrixTimeline {
return RustMatrixTimeline(
matrixRoom = this,
innerRoom = innerRoom,
slidingSyncRoom = slidingSyncRoom,
coroutineScope = coroutineScope,
coroutineDispatchers = coroutineDispatchers
)
}
override val roomId = RoomId(innerRoom.id())
override val name: String?
get() {
return slidingSyncRoom.name()
}
override val bestName: String
get() {
return name?.takeIf { it.isNotEmpty() } ?: innerRoom.id()
}
override val displayName: String
get() {
return innerRoom.displayName()
}
override val topic: String?
get() {
return innerRoom.topic()
}
override val avatarUrl: String?
get() {
return innerRoom.avatarUrl()
}
override suspend fun fetchMembers(): Result<Unit> = withContext(coroutineDispatchers.io) {
runCatching {
innerRoom.fetchMembers()
}
}
override suspend fun userDisplayName(userId: String): Result<String?> =
withContext(coroutineDispatchers.io) {
runCatching {
innerRoom.memberDisplayName(userId)
}
}
override suspend fun userAvatarUrl(userId: String): Result<String?> =
withContext(coroutineDispatchers.io) {
runCatching {
innerRoom.memberAvatarUrl(userId)
}
}
override suspend fun sendMessage(message: String): Result<Unit> = withContext(coroutineDispatchers.io) {
val transactionId = genTransactionId()
val content = messageEventContentFromMarkdown(message)
runCatching {
innerRoom.send(content, transactionId)
}
}
override suspend fun editMessage(originalEventId: EventId, message: String): Result<Unit> = withContext(coroutineDispatchers.io) {
val transactionId = genTransactionId()
// val content = messageEventContentFromMarkdown(message)
runCatching {
innerRoom.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 {
innerRoom.sendReply(/* TODO use content */ message, eventId.value, transactionId)
}
}
override suspend fun redactEvent(eventId: EventId, reason: String?) = withContext(coroutineDispatchers.io) {
val transactionId = genTransactionId()
runCatching {
innerRoom.redact(eventId.value, reason, transactionId)
}
}
}

View file

@ -0,0 +1,194 @@
/*
* 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.
* 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.libraries.matrix.impl.room
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.impl.sync.roomListDiff
import io.element.android.libraries.matrix.impl.sync.state
import io.element.android.libraries.matrix.room.RoomSummary
import io.element.android.libraries.matrix.room.RoomSummaryDataSource
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.RoomListEntry
import org.matrix.rustcomponents.sdk.SlidingSync
import org.matrix.rustcomponents.sdk.SlidingSyncState
import org.matrix.rustcomponents.sdk.SlidingSyncView
import org.matrix.rustcomponents.sdk.SlidingSyncViewRoomsListDiff
import org.matrix.rustcomponents.sdk.UpdateSummary
import timber.log.Timber
import java.io.Closeable
import java.util.UUID
internal class RustRoomSummaryDataSource(
private val slidingSyncUpdateFlow: Flow<UpdateSummary>,
private val slidingSync: SlidingSync,
private val slidingSyncView: SlidingSyncView,
private val coroutineDispatchers: CoroutineDispatchers,
private val onRestartSync: () -> Unit,
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
) : RoomSummaryDataSource, Closeable {
private val coroutineScope = CoroutineScope(SupervisorJob() + coroutineDispatchers.io)
private val roomSummaries = MutableStateFlow<List<RoomSummary>>(emptyList())
private val state = MutableStateFlow(SlidingSyncState.COLD)
fun init() {
coroutineScope.launch {
updateRoomSummaries {
addAll(
slidingSyncView.currentRoomsList().map(::buildSummaryForRoomListEntry)
)
}
}
slidingSyncUpdateFlow
.onEach {
didReceiveSyncUpdate(it)
}.launchIn(coroutineScope)
slidingSyncView.roomListDiff(coroutineScope)
.onEach { diffs ->
updateRoomSummaries {
applyDiff(diffs)
}
}
.launchIn(coroutineScope)
slidingSyncView.state(coroutineScope)
.onEach { slidingSyncState ->
Timber.v("New sliding sync state: $slidingSyncState")
state.value = slidingSyncState
}.launchIn(coroutineScope)
}
override fun close() {
coroutineScope.cancel()
}
//@OptIn(FlowPreview::class)
override fun roomSummaries(): StateFlow<List<RoomSummary>> {
return roomSummaries
}
override fun setSlidingSyncRange(range: IntRange) {
Timber.v("setVisibleRange=$range")
slidingSyncView.setRange(range.first.toUInt(), range.last.toUInt())
onRestartSync()
}
private suspend fun didReceiveSyncUpdate(summary: UpdateSummary) {
Timber.v("UpdateRooms with identifiers: ${summary.rooms}")
if (state.value != SlidingSyncState.LIVE) {
return
}
updateRoomSummaries {
for (identifier in summary.rooms) {
val index = indexOfFirst { it.identifier() == identifier }
if (index == -1) {
continue
}
val updatedRoomSummary = buildRoomSummaryForIdentifier(identifier)
set(index, updatedRoomSummary)
}
}
}
private fun MutableList<RoomSummary>.applyDiff(diff: SlidingSyncViewRoomsListDiff) {
fun MutableList<RoomSummary>.fillUntil(untilIndex: Int) {
repeat((size - 1 until untilIndex).count()) {
add(buildEmptyRoomSummary())
}
}
Timber.v("ApplyDiff: $diff for list with size: $size")
when (diff) {
is SlidingSyncViewRoomsListDiff.Append -> {
val roomSummaries = diff.values.map {
buildSummaryForRoomListEntry(it)
}
addAll(roomSummaries)
}
is SlidingSyncViewRoomsListDiff.PushBack -> {
val roomSummary = buildSummaryForRoomListEntry(diff.value)
add(roomSummary)
}
is SlidingSyncViewRoomsListDiff.PushFront -> {
val roomSummary = buildSummaryForRoomListEntry(diff.value)
add(0, roomSummary)
}
is SlidingSyncViewRoomsListDiff.Set -> {
fillUntil(diff.index.toInt())
val roomSummary = buildSummaryForRoomListEntry(diff.value)
set(diff.index.toInt(), roomSummary)
}
is SlidingSyncViewRoomsListDiff.Insert -> {
val roomSummary = buildSummaryForRoomListEntry(diff.value)
add(diff.index.toInt(), roomSummary)
}
is SlidingSyncViewRoomsListDiff.Remove -> {
removeAt(diff.index.toInt())
}
is SlidingSyncViewRoomsListDiff.Reset -> {
clear()
addAll(diff.values.map { buildSummaryForRoomListEntry(it) })
}
SlidingSyncViewRoomsListDiff.PopBack -> {
removeFirstOrNull()
}
SlidingSyncViewRoomsListDiff.PopFront -> {
removeLastOrNull()
}
SlidingSyncViewRoomsListDiff.Clear -> {
clear()
}
}
}
private fun buildSummaryForRoomListEntry(entry: RoomListEntry): RoomSummary {
return when (entry) {
RoomListEntry.Empty -> buildEmptyRoomSummary()
is RoomListEntry.Invalidated -> buildRoomSummaryForIdentifier(entry.roomId)
is RoomListEntry.Filled -> buildRoomSummaryForIdentifier(entry.roomId)
}
}
private fun buildEmptyRoomSummary(): RoomSummary {
return RoomSummary.Empty(UUID.randomUUID().toString())
}
private fun buildRoomSummaryForIdentifier(identifier: String): RoomSummary {
val room = slidingSync.getRoom(identifier) ?: return RoomSummary.Empty(identifier)
return RoomSummary.Filled(
details = roomSummaryDetailsFactory.create(room, room.fullRoom())
)
}
private suspend fun updateRoomSummaries(block: MutableList<RoomSummary>.() -> Unit) =
withContext(coroutineDispatchers.diffUpdateDispatcher) {
val mutableRoomSummaries = roomSummaries.value.toMutableList()
block(mutableRoomSummaries)
roomSummaries.value = mutableRoomSummaries
}
}

View file

@ -0,0 +1,34 @@
/*
* 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.
* 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.libraries.matrix.impl.room.message
import io.element.android.libraries.matrix.core.EventId
import io.element.android.libraries.matrix.core.UserId
import io.element.android.libraries.matrix.room.message.RoomMessage
import org.matrix.rustcomponents.sdk.EventTimelineItem
class RoomMessageFactory {
fun create(eventTimelineItem: EventTimelineItem?): RoomMessage? {
eventTimelineItem ?: return null
return RoomMessage(
eventId = EventId(eventTimelineItem.eventId() ?: ""),
body = eventTimelineItem.content().asMessage()?.body() ?: "",
sender = UserId(eventTimelineItem.sender()),
originServerTs = eventTimelineItem.timestamp().toLong()
)
}
}

View file

@ -0,0 +1,44 @@
/*
* 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.libraries.matrix.impl.sync
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import org.matrix.rustcomponents.sdk.SlidingSyncObserver
import org.matrix.rustcomponents.sdk.UpdateSummary
// Sounds like a reasonable buffer size before it suspends emitting new items.
private const val BUFFER_SIZE = 64
class SlidingSyncObserverProxy(
private val coroutineScope: CoroutineScope,
) : SlidingSyncObserver {
private val updateSummaryMutableFlow =
MutableSharedFlow<UpdateSummary>(extraBufferCapacity = BUFFER_SIZE)
val updateSummaryFlow: SharedFlow<UpdateSummary> = updateSummaryMutableFlow.asSharedFlow()
override fun didReceiveSyncUpdate(summary: UpdateSummary) {
if (summary.rooms.isEmpty()) return
coroutineScope.launch {
updateSummaryMutableFlow.emit(summary)
}
}
}

View file

@ -0,0 +1,62 @@
/*
* 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.
* 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.libraries.matrix.impl.sync
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import org.matrix.rustcomponents.sdk.SlidingSyncState
import org.matrix.rustcomponents.sdk.SlidingSyncView
import org.matrix.rustcomponents.sdk.SlidingSyncViewRoomListObserver
import org.matrix.rustcomponents.sdk.SlidingSyncViewRoomsCountObserver
import org.matrix.rustcomponents.sdk.SlidingSyncViewRoomsListDiff
import org.matrix.rustcomponents.sdk.SlidingSyncViewStateObserver
fun SlidingSyncView.roomListDiff(scope: CoroutineScope): Flow<SlidingSyncViewRoomsListDiff> =
mxCallbackFlow {
val observer = object : SlidingSyncViewRoomListObserver {
override fun didReceiveUpdate(diff: SlidingSyncViewRoomsListDiff) {
scope.launch {
send(diff)
}
}
}
observeRoomList(observer)
}
fun SlidingSyncView.state(scope: CoroutineScope): Flow<SlidingSyncState> = mxCallbackFlow {
val observer = object : SlidingSyncViewStateObserver {
override fun didReceiveUpdate(newState: SlidingSyncState) {
scope.launch {
send(newState)
}
}
}
observeState(observer)
}
fun SlidingSyncView.roomsCount(scope: CoroutineScope): Flow<UInt> = mxCallbackFlow {
val observer = object : SlidingSyncViewRoomsCountObserver {
override fun didReceiveUpdate(count: UInt) {
scope.launch {
send(count)
}
}
}
observeRoomsCount(observer)
}

View file

@ -0,0 +1,120 @@
/*
* 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.
* 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.libraries.matrix.impl.timeline
import io.element.android.libraries.matrix.timeline.MatrixTimeline
import io.element.android.libraries.matrix.timeline.MatrixTimelineItem
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.TimelineChange
import org.matrix.rustcomponents.sdk.TimelineDiff
import org.matrix.rustcomponents.sdk.TimelineListener
import org.matrix.rustcomponents.sdk.VirtualTimelineItem
internal class MatrixTimelineDiffProcessor(
private val paginationState: MutableStateFlow<MatrixTimeline.PaginationState>,
private val timelineItems: MutableStateFlow<List<MatrixTimelineItem>>,
private val coroutineScope: CoroutineScope,
private val diffDispatcher: CoroutineDispatcher,
) : TimelineListener {
override fun onUpdate(update: TimelineDiff) {
coroutineScope.launch {
updateTimelineItems {
applyDiff(update)
}
when (val firstItem = timelineItems.value.firstOrNull()) {
is MatrixTimelineItem.Virtual -> updateBackPaginationState(firstItem.virtual)
else -> updateBackPaginationState(null)
}
}
}
private fun updateBackPaginationState(virtualItem: VirtualTimelineItem?) {
val currentPaginationState = paginationState.value
val newPaginationState = when (virtualItem) {
VirtualTimelineItem.LoadingIndicator -> currentPaginationState.copy(
isBackPaginating = true,
canBackPaginate = true
)
VirtualTimelineItem.TimelineStart -> currentPaginationState.copy(
isBackPaginating = false,
canBackPaginate = false
)
else -> currentPaginationState.copy(
isBackPaginating = false,
canBackPaginate = true
)
}
paginationState.value = newPaginationState
}
private suspend fun updateTimelineItems(block: MutableList<MatrixTimelineItem>.() -> Unit) =
withContext(diffDispatcher) {
val mutableTimelineItems = timelineItems.value.toMutableList()
block(mutableTimelineItems)
timelineItems.value = mutableTimelineItems
}
private fun MutableList<MatrixTimelineItem>.applyDiff(diff: TimelineDiff) {
when (diff.change()) {
TimelineChange.APPEND -> {
val items = diff.append()?.map { it.asMatrixTimelineItem() } ?: return
addAll(items)
}
TimelineChange.PUSH_BACK -> {
val item = diff.pushBack()?.asMatrixTimelineItem() ?: return
add(item)
}
TimelineChange.PUSH_FRONT -> {
val item = diff.pushFront()?.asMatrixTimelineItem() ?: return
add(0, item)
}
TimelineChange.SET -> {
val updateAtData = diff.set() ?: return
val item = updateAtData.item.asMatrixTimelineItem()
set(updateAtData.index.toInt(), item)
}
TimelineChange.INSERT -> {
val insertAtData = diff.insert() ?: return
val item = insertAtData.item.asMatrixTimelineItem()
add(insertAtData.index.toInt(), item)
}
TimelineChange.REMOVE -> {
val removeAtData = diff.remove() ?: return
removeAt(removeAtData.toInt())
}
TimelineChange.RESET -> {
clear()
val items = diff.reset()?.map { it.asMatrixTimelineItem() } ?: return
addAll(items)
}
TimelineChange.POP_FRONT -> {
removeFirstOrNull()
}
TimelineChange.POP_BACK -> {
removeLastOrNull()
}
TimelineChange.CLEAR -> {
clear()
}
}
}
}

View file

@ -0,0 +1,32 @@
/*
* 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.libraries.matrix.impl.timeline
import io.element.android.libraries.matrix.timeline.MatrixTimelineItem
import org.matrix.rustcomponents.sdk.TimelineItem
fun TimelineItem.asMatrixTimelineItem(): MatrixTimelineItem {
val asEvent = asEvent()
if (asEvent != null) {
return MatrixTimelineItem.Event(asEvent)
}
val asVirtual = asVirtual()
if (asVirtual != null) {
return MatrixTimelineItem.Virtual(asVirtual)
}
return MatrixTimelineItem.Other
}

View file

@ -0,0 +1,139 @@
/*
* 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.
* 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.libraries.matrix.impl.timeline
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.impl.util.TaskHandleBag
import io.element.android.libraries.matrix.core.EventId
import io.element.android.libraries.matrix.room.MatrixRoom
import io.element.android.libraries.matrix.timeline.MatrixTimeline
import io.element.android.libraries.matrix.timeline.MatrixTimelineItem
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.PaginationOptions
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.SlidingSyncRoom
import org.matrix.rustcomponents.sdk.TimelineItem
import org.matrix.rustcomponents.sdk.TimelineListener
import timber.log.Timber
class RustMatrixTimeline(
private val matrixRoom: MatrixRoom,
private val innerRoom: Room,
private val slidingSyncRoom: SlidingSyncRoom,
private val coroutineScope: CoroutineScope,
private val coroutineDispatchers: CoroutineDispatchers,
) : MatrixTimeline {
private val timelineItems: MutableStateFlow<List<MatrixTimelineItem>> =
MutableStateFlow(emptyList())
private val paginationState = MutableStateFlow(
MatrixTimeline.PaginationState(canBackPaginate = true, isBackPaginating = false)
)
private val innerTimelineListener = MatrixTimelineDiffProcessor(
paginationState = paginationState,
timelineItems = timelineItems,
coroutineScope = coroutineScope,
diffDispatcher = coroutineDispatchers.diffUpdateDispatcher
)
private val listenerTokens = TaskHandleBag()
override fun paginationState(): StateFlow<MatrixTimeline.PaginationState> {
return paginationState
}
@OptIn(FlowPreview::class)
override fun timelineItems(): Flow<List<MatrixTimelineItem>> {
return timelineItems.sample(50)
}
override fun initialize() {
Timber.v("Init timeline for room ${matrixRoom.roomId}")
coroutineScope.launch {
matrixRoom.fetchMembers()
.onFailure {
Timber.e(it, "Fail to fetch members for room ${matrixRoom.roomId}")
}.onSuccess {
Timber.v("Success fetching members for room ${matrixRoom.roomId}")
}
}
coroutineScope.launch {
val result = addListener(innerTimelineListener)
result
.onSuccess { timelineItems ->
val matrixTimelineItems = timelineItems.map { it.asMatrixTimelineItem() }
withContext(coroutineDispatchers.diffUpdateDispatcher) {
this@RustMatrixTimeline.timelineItems.value = matrixTimelineItems
}
}
.onFailure {
Timber.e("Failed adding timeline listener on room with identifier: ${matrixRoom.roomId})")
}
}
}
override fun dispose() {
Timber.v("Dispose timeline for room ${matrixRoom.roomId}")
listenerTokens.dispose()
}
/**
* @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 suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit> = withContext(coroutineDispatchers.io) {
runCatching {
Timber.v("Start back paginating for room ${matrixRoom.roomId} ")
val paginationOptions = PaginationOptions.UntilNumItems(
eventLimit = requestSize.toUShort(),
items = untilNumberOfItems.toUShort()
)
innerRoom.paginateBackwards(paginationOptions)
}.onFailure {
Timber.e(it, "Fail to paginate for room ${matrixRoom.roomId}")
}.onSuccess {
Timber.v("Success back paginating for room ${matrixRoom.roomId}")
}
}
private suspend fun addListener(timelineListener: TimelineListener): Result<List<TimelineItem>> = withContext(coroutineDispatchers.io) {
runCatching {
val result = slidingSyncRoom.subscribeAndAddTimelineListener(timelineListener, null)
listenerTokens += result.taskHandle
result.items
}
}
}

View file

@ -0,0 +1,26 @@
/*
* 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.libraries.matrix.impl.tracing
import io.element.android.libraries.matrix.tracing.TracingConfiguration
import timber.log.Timber
fun setupTracing(tracingConfiguration: TracingConfiguration) {
val filter = tracingConfiguration.filter
Timber.v("Tracing config filter = $filter")
org.matrix.rustcomponents.sdk.setupTracing(filter)
}

View file

@ -0,0 +1,31 @@
/*
* 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.libraries.matrix.impl.util
import kotlinx.coroutines.channels.ProducerScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import org.matrix.rustcomponents.sdk.TaskHandle
internal fun <T> mxCallbackFlow(block: suspend ProducerScope<T>.() -> TaskHandle) =
callbackFlow {
val token: TaskHandle = block(this)
awaitClose {
token.cancel()
token.destroy()
}
}

View file

@ -0,0 +1,31 @@
/*
* 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.libraries.matrix.impl.util
import org.matrix.rustcomponents.sdk.ClientException
import timber.log.Timber
fun logError(throwable: Throwable) {
when (throwable) {
is ClientException.Generic -> {
Timber.e("Error ${throwable.msg}", throwable)
}
else -> {
Timber.e("Error", throwable)
}
}
}

View file

@ -0,0 +1,33 @@
/*
* 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.
* 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.libraries.matrix.impl.util
import org.matrix.rustcomponents.sdk.TaskHandle
import java.util.concurrent.CopyOnWriteArraySet
class TaskHandleBag(private val tokens: MutableSet<TaskHandle> = CopyOnWriteArraySet()) : Set<TaskHandle> by tokens {
operator fun plusAssign(taskHandle: TaskHandle?) {
if (taskHandle == null) return
tokens += taskHandle
}
fun dispose() {
tokens.forEach { it.cancel() }
tokens.clear()
}
}