ganfra
2 years ago
20 changed files with 418 additions and 114 deletions
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
package io.element.android.x.core.data |
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher |
||||
|
||||
data class CoroutineDispatchers( |
||||
val io: CoroutineDispatcher, |
||||
val computation: CoroutineDispatcher, |
||||
val main: CoroutineDispatcher, |
||||
) |
@ -1,78 +1,105 @@
@@ -1,78 +1,105 @@
|
||||
package io.element.android.x.matrix |
||||
|
||||
import android.util.Log |
||||
import io.element.android.x.matrix.store.SessionStore |
||||
import io.element.android.x.core.data.CoroutineDispatchers |
||||
import io.element.android.x.matrix.core.UserId |
||||
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 kotlinx.coroutines.withContext |
||||
import org.matrix.rustcomponents.sdk.* |
||||
import java.io.Closeable |
||||
|
||||
class MatrixClient internal constructor( |
||||
private val client: Client, |
||||
private val sessionStore: SessionStore, |
||||
) { |
||||
private val roomWrapper = RoomWrapper(client) |
||||
private val dispatchers: CoroutineDispatchers, |
||||
) : Closeable { |
||||
|
||||
fun startSync() { |
||||
val clientDelegate = object : ClientDelegate { |
||||
override fun didReceiveAuthError(isSoftLogout: Boolean) { |
||||
Log.v(LOG_TAG, "didReceiveAuthError()") |
||||
} |
||||
private val clientDelegate = object : ClientDelegate { |
||||
override fun didReceiveAuthError(isSoftLogout: Boolean) { |
||||
Log.v(LOG_TAG, "didReceiveAuthError()") |
||||
} |
||||
|
||||
override fun didReceiveSyncUpdate() { |
||||
Log.v(LOG_TAG, "didReceiveSyncUpdate()") |
||||
} |
||||
override fun didReceiveSyncUpdate() { |
||||
Log.v(LOG_TAG, "didReceiveSyncUpdate()") |
||||
} |
||||
|
||||
override fun didUpdateRestoreToken() { |
||||
Log.v(LOG_TAG, "didUpdateRestoreToken()") |
||||
} |
||||
override fun didUpdateRestoreToken() { |
||||
Log.v(LOG_TAG, "didUpdateRestoreToken()") |
||||
} |
||||
} |
||||
|
||||
client.setDelegate(clientDelegate) |
||||
Log.v(LOG_TAG, "DisplayName = ${client.displayName()}") |
||||
try { |
||||
client.fullSlidingSync() |
||||
} catch (failure: Throwable) { |
||||
Log.e(LOG_TAG, "fullSlidingSync() fail", failure) |
||||
private val slidingSyncObserver = object : SlidingSyncObserver { |
||||
override fun didReceiveSyncUpdate(summary: UpdateSummary) { |
||||
Log.v(LOG_TAG, "didReceiveSyncUpdate=$summary") |
||||
roomSummaryDataSource.updateRoomsWithIdentifiers(summary.rooms) |
||||
} |
||||
} |
||||
|
||||
fun slidingSync(listener: SlidingSyncListener): StoppableSpawn { |
||||
val slidingSyncView = SlidingSyncViewBuilder() |
||||
.timelineLimit(limit = 10u) |
||||
.requiredState(requiredState = listOf(RequiredState(key = "m.room.avatar", value = ""))) |
||||
.name(name = "HomeScreenView") |
||||
.syncMode(mode = SlidingSyncMode.FULL_SYNC) |
||||
.build() |
||||
|
||||
val slidingSync = client |
||||
.slidingSync() |
||||
.homeserver("https://slidingsync.lab.element.dev") |
||||
.addView(slidingSyncView) |
||||
.build() |
||||
|
||||
slidingSync.setObserver(object : SlidingSyncObserver { |
||||
override fun didReceiveSyncUpdate(summary: UpdateSummary) { |
||||
Log.v(LOG_TAG, "didReceiveSyncUpdate=$summary") |
||||
val rooms = summary.rooms.mapNotNull { |
||||
roomWrapper.getRoom(it) |
||||
} |
||||
listener.onSyncUpdate(summary, rooms) |
||||
} |
||||
}) |
||||
return slidingSync.sync() |
||||
private val slidingSyncView = SlidingSyncViewBuilder() |
||||
.timelineLimit(limit = 10u) |
||||
.requiredState(requiredState = listOf(RequiredState(key = "m.room.avatar", value = ""))) |
||||
.name(name = "HomeScreenView") |
||||
.syncMode(mode = SlidingSyncMode.FULL_SYNC) |
||||
.build() |
||||
|
||||
private val slidingSync = client |
||||
.slidingSync() |
||||
.homeserver("https://slidingsync.lab.element.dev") |
||||
.addView(slidingSyncView) |
||||
.build() |
||||
|
||||
private val roomSummaryDataSource: RustRoomSummaryDataSource = |
||||
RustRoomSummaryDataSource(slidingSync, slidingSyncView, dispatchers) |
||||
private var slidingSyncObserverToken: StoppableSpawn? = null |
||||
|
||||
init { |
||||
client.setDelegate(clientDelegate) |
||||
} |
||||
|
||||
fun startSync() { |
||||
slidingSync.setObserver(slidingSyncObserver) |
||||
slidingSyncObserverToken = slidingSync.sync() |
||||
} |
||||
|
||||
fun stopSync() { |
||||
slidingSync.setObserver(null) |
||||
slidingSyncObserverToken?.cancel() |
||||
} |
||||
|
||||
fun roomSummaryDataSource(): RoomSummaryDataSource = roomSummaryDataSource |
||||
|
||||
override fun close() { |
||||
stopSync() |
||||
client.setDelegate(null) |
||||
} |
||||
|
||||
suspend fun logout() { |
||||
suspend fun logout() = withContext(dispatchers.io) { |
||||
close() |
||||
client.logout() |
||||
sessionStore.reset() |
||||
} |
||||
|
||||
fun userId(): String = client.userId() |
||||
fun username(): String = client.displayName() |
||||
fun avatarUrl(): String = client.avatarUrl() |
||||
|
||||
fun loadMedia(source: MediaSource) = client.getMediaContent(source) |
||||
fun loadMedia2(mxcUrl: String) = client.getMediaContent(mediaSourceFromUrl(mxcUrl)) |
||||
fun userId(): UserId = UserId(client.userId()) |
||||
suspend fun loadUserDisplayName(): Result<String> = withContext(dispatchers.io) { |
||||
runCatching { |
||||
client.displayName() |
||||
} |
||||
} |
||||
|
||||
interface SlidingSyncListener { |
||||
fun onSyncUpdate(summary: UpdateSummary, rooms: List<Room>) |
||||
suspend fun loadUserAvatarURLString(): Result<String> = withContext(dispatchers.io) { |
||||
runCatching { |
||||
client.avatarUrl() |
||||
} |
||||
} |
||||
|
||||
suspend fun loadMediaContentForSource(source: MediaSource): Result<List<UByte>> = |
||||
withContext(dispatchers.io) { |
||||
runCatching { |
||||
client.getMediaContent(source) |
||||
} |
||||
} |
||||
|
||||
|
||||
} |
||||
|
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
package io.element.android.x.matrix.core |
||||
|
||||
@JvmInline |
||||
value class EventId(val value: String) |
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
package io.element.android.x.matrix.core |
||||
|
||||
@JvmInline |
||||
value class RoomId(val value: String) |
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
package io.element.android.x.matrix.core |
||||
|
||||
@JvmInline |
||||
value class UserId(val value: String) |
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
package io.element.android.x.matrix.room |
||||
|
||||
import io.element.android.x.matrix.core.RoomId |
||||
import org.matrix.rustcomponents.sdk.Room |
||||
|
||||
class MatrixRoom(private val room: Room) { |
||||
|
||||
val roomId = RoomId(room.id()) |
||||
|
||||
|
||||
} |
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
package io.element.android.x.matrix.room |
||||
|
||||
import io.element.android.x.matrix.core.RoomId |
||||
|
||||
sealed interface RoomSummary { |
||||
data class Empty(val identifier: String) : RoomSummary |
||||
data class Filled(val details: RoomSummaryDetails) : RoomSummary |
||||
|
||||
fun identifier(): String { |
||||
return when (this) { |
||||
is Empty -> identifier |
||||
is Filled -> details.roomId.value |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
data class RoomSummaryDetails( |
||||
val roomId: RoomId, |
||||
val name: String?, |
||||
val isDirect: Boolean, |
||||
val avatarURLString: String?, |
||||
val lastMessage: CharSequence?, |
||||
val lastMessageTimestamp: Long?, |
||||
val unreadNotificationCount: UInt, |
||||
) |
@ -0,0 +1,139 @@
@@ -0,0 +1,139 @@
|
||||
package io.element.android.x.matrix.room |
||||
|
||||
import io.element.android.x.core.data.CoroutineDispatchers |
||||
import io.element.android.x.matrix.core.RoomId |
||||
import io.element.android.x.matrix.room.message.RoomMessageFactory |
||||
import io.element.android.x.matrix.sync.roomListDiff |
||||
import io.element.android.x.matrix.sync.state |
||||
import kotlinx.coroutines.CoroutineDispatcher |
||||
import kotlinx.coroutines.CoroutineScope |
||||
import kotlinx.coroutines.SupervisorJob |
||||
import kotlinx.coroutines.flow.Flow |
||||
import kotlinx.coroutines.flow.MutableStateFlow |
||||
import kotlinx.coroutines.flow.launchIn |
||||
import kotlinx.coroutines.flow.onEach |
||||
import org.matrix.rustcomponents.sdk.* |
||||
import java.util.* |
||||
|
||||
interface RoomSummaryDataSource { |
||||
fun roomSummaries(): Flow<List<RoomSummary>> |
||||
} |
||||
|
||||
internal class RustRoomSummaryDataSource( |
||||
private val slidingSync: SlidingSync, |
||||
private val slidingSyncView: SlidingSyncView, |
||||
private val coroutineDispatchers: CoroutineDispatchers, |
||||
private val roomMessageFactory: RoomMessageFactory = RoomMessageFactory(), |
||||
) : RoomSummaryDataSource { |
||||
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + coroutineDispatchers.io) |
||||
|
||||
private val roomSummaries = MutableStateFlow<List<RoomSummary>>(emptyList()) |
||||
private val state = MutableStateFlow(SlidingSyncState.COLD) |
||||
|
||||
init { |
||||
slidingSyncView.roomListDiff() |
||||
.onEach { diff -> |
||||
updateRoomSummaries { |
||||
applyDiff(diff) |
||||
} |
||||
}.launchIn(coroutineScope) |
||||
|
||||
slidingSyncView.state() |
||||
.onEach { newRoomState -> |
||||
state.value = newRoomState |
||||
}.launchIn(coroutineScope) |
||||
} |
||||
|
||||
override fun roomSummaries(): Flow<List<RoomSummary>> { |
||||
return roomSummaries |
||||
} |
||||
|
||||
internal fun updateRoomsWithIdentifiers(identifiers: List<String>) { |
||||
if (state.value != SlidingSyncState.LIVE) { |
||||
return |
||||
} |
||||
val roomSummaryList = roomSummaries.value.toMutableList() |
||||
for (identifier in identifiers) { |
||||
val index = roomSummaryList.indexOfFirst { it.identifier() == identifier } |
||||
if (index == -1) { |
||||
continue |
||||
} |
||||
val updatedRoomSummary = buildRoomSummaryForIdentifier(identifier) |
||||
roomSummaryList[index] = updatedRoomSummary |
||||
} |
||||
roomSummaries.value = roomSummaryList |
||||
} |
||||
|
||||
private fun MutableList<RoomSummary>.applyDiff(diff: SlidingSyncViewRoomsListDiff) { |
||||
if (diff.isInvalidation()) { |
||||
return |
||||
} |
||||
when (diff) { |
||||
is SlidingSyncViewRoomsListDiff.Push -> { |
||||
val roomSummary = buildSummaryForRoomListEntry(diff.value) |
||||
add(roomSummary) |
||||
} |
||||
is SlidingSyncViewRoomsListDiff.UpdateAt -> { |
||||
val roomSummary = buildSummaryForRoomListEntry(diff.value) |
||||
set(diff.index.toInt(), roomSummary) |
||||
} |
||||
is SlidingSyncViewRoomsListDiff.InsertAt -> { |
||||
val roomSummary = buildSummaryForRoomListEntry(diff.value) |
||||
add(diff.index.toInt(), roomSummary) |
||||
} |
||||
is SlidingSyncViewRoomsListDiff.Move -> { |
||||
Collections.swap(this, diff.oldIndex.toInt(), diff.newIndex.toInt()) |
||||
} |
||||
is SlidingSyncViewRoomsListDiff.RemoveAt -> { |
||||
removeAt(diff.index.toInt()) |
||||
} |
||||
is SlidingSyncViewRoomsListDiff.Replace -> { |
||||
clear() |
||||
addAll(diff.values.map { buildSummaryForRoomListEntry(it) }) |
||||
} |
||||
} |
||||
} |
||||
|
||||
private fun buildSummaryForRoomListEntry(entry: RoomListEntry): RoomSummary { |
||||
return when (entry) { |
||||
RoomListEntry.Empty -> RoomSummary.Empty(UUID.randomUUID().toString()) |
||||
is RoomListEntry.Invalidated -> buildRoomSummaryForIdentifier(entry.roomId) |
||||
is RoomListEntry.Filled -> buildRoomSummaryForIdentifier(entry.roomId) |
||||
} |
||||
} |
||||
|
||||
private fun buildRoomSummaryForIdentifier(identifier: String): RoomSummary { |
||||
val room = slidingSync.getRoom(identifier) ?: return RoomSummary.Empty(identifier) |
||||
val latestRoomMessage = room.latestRoomMessage()?.let { |
||||
roomMessageFactory.create(it) |
||||
} |
||||
return RoomSummary.Filled( |
||||
details = RoomSummaryDetails( |
||||
roomId = RoomId(identifier), |
||||
name = room.name(), |
||||
isDirect = room.isDm() ?: false, |
||||
avatarURLString = room.fullRoom()?.avatarUrl(), |
||||
unreadNotificationCount = room.unreadNotifications().notificationCount(), |
||||
lastMessage = latestRoomMessage?.body, |
||||
lastMessageTimestamp = latestRoomMessage?.originServerTs |
||||
) |
||||
) |
||||
} |
||||
|
||||
private fun updateRoomSummaries(block: MutableList<RoomSummary>.() -> Unit) { |
||||
val mutableRoomSummaries = roomSummaries.value.toMutableList() |
||||
block(mutableRoomSummaries) |
||||
roomSummaries.value = mutableRoomSummaries |
||||
} |
||||
|
||||
} |
||||
|
||||
fun SlidingSyncViewRoomsListDiff.isInvalidation(): Boolean { |
||||
return when (this) { |
||||
is SlidingSyncViewRoomsListDiff.InsertAt -> this.value is RoomListEntry.Invalidated |
||||
is SlidingSyncViewRoomsListDiff.UpdateAt -> this.value is RoomListEntry.Invalidated |
||||
is SlidingSyncViewRoomsListDiff.Push -> this.value is RoomListEntry.Invalidated |
||||
else -> false |
||||
} |
||||
} |
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
package io.element.android.x.matrix.room.message |
||||
|
||||
import io.element.android.x.matrix.core.EventId |
||||
import io.element.android.x.matrix.core.UserId |
||||
|
||||
data class RoomMessage( |
||||
val eventId: EventId, |
||||
val body: String, |
||||
val sender: UserId, |
||||
val originServerTs: Long, |
||||
) |
@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
|
||||
package io.element.android.x.matrix.room.message |
||||
|
||||
import io.element.android.x.matrix.core.EventId |
||||
import io.element.android.x.matrix.core.UserId |
||||
import org.matrix.rustcomponents.sdk.AnyMessage |
||||
|
||||
class RoomMessageFactory { |
||||
|
||||
fun create(anyMessage: AnyMessage): RoomMessage? { |
||||
val textMessage = anyMessage.textMessage()?.baseMessage() ?: return null |
||||
return RoomMessage( |
||||
eventId = EventId(textMessage.id()), |
||||
body = textMessage.body(), |
||||
sender = UserId(textMessage.sender()), |
||||
originServerTs = textMessage.originServerTs().toLong() |
||||
) |
||||
} |
||||
|
||||
} |
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
package io.element.android.x.matrix.sync |
||||
|
||||
import kotlinx.coroutines.flow.Flow |
||||
import mxCallbackFlow |
||||
import org.matrix.rustcomponents.sdk.* |
||||
|
||||
fun SlidingSyncView.roomListDiff(): Flow<SlidingSyncViewRoomsListDiff> = mxCallbackFlow { |
||||
val observer = object : SlidingSyncViewRoomListObserver { |
||||
override fun didReceiveUpdate(diff: SlidingSyncViewRoomsListDiff) { |
||||
trySend(diff) |
||||
} |
||||
} |
||||
observeRoomList(observer) |
||||
} |
||||
|
||||
fun SlidingSyncView.state(): Flow<SlidingSyncState> = mxCallbackFlow { |
||||
val observer = object : SlidingSyncViewStateObserver { |
||||
override fun didReceiveUpdate(newState: SlidingSyncState) { |
||||
trySend(newState) |
||||
} |
||||
} |
||||
observeState(observer) |
||||
} |
||||
|
||||
fun SlidingSyncView.roomsCount(): Flow<UInt> = mxCallbackFlow { |
||||
val observer = object : SlidingSyncViewRoomsCountObserver { |
||||
override fun didReceiveUpdate(count: UInt) { |
||||
trySend(count) |
||||
} |
||||
} |
||||
observeRoomsCount(observer) |
||||
} |
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class) |
||||
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi |
||||
import kotlinx.coroutines.channels.ProducerScope |
||||
import kotlinx.coroutines.channels.awaitClose |
||||
import kotlinx.coroutines.flow.callbackFlow |
||||
import org.matrix.rustcomponents.sdk.StoppableSpawn |
||||
|
||||
internal fun <T> mxCallbackFlow(block: suspend ProducerScope<T>.() -> StoppableSpawn) = |
||||
callbackFlow { |
||||
val token: StoppableSpawn = block(this) |
||||
awaitClose { |
||||
token.cancel() |
||||
} |
||||
} |
Loading…
Reference in new issue