Start implementing logic for room summary list
This commit is contained in:
parent
7975ea16e4
commit
a609433a50
20 changed files with 423 additions and 119 deletions
|
|
@ -1,8 +1,10 @@
|
|||
package io.element.android.x.matrix
|
||||
|
||||
import android.content.Context
|
||||
import io.element.android.x.matrix.store.SessionStore
|
||||
import io.element.android.x.core.data.CoroutineDispatchers
|
||||
import io.element.android.x.matrix.session.SessionStore
|
||||
import io.element.android.x.matrix.util.logError
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.matrix.rustcomponents.sdk.AuthenticationService
|
||||
import org.matrix.rustcomponents.sdk.ClientBuilder
|
||||
import java.io.File
|
||||
|
|
@ -10,6 +12,12 @@ import java.io.File
|
|||
class Matrix(
|
||||
context: Context,
|
||||
) {
|
||||
|
||||
private val coroutineDispatchers = CoroutineDispatchers(
|
||||
io = Dispatchers.IO,
|
||||
computation = Dispatchers.Default,
|
||||
main = Dispatchers.Main
|
||||
)
|
||||
private val baseFolder = File(context.filesDir, "matrix")
|
||||
private val sessionStore = SessionStore(context)
|
||||
|
||||
|
|
@ -17,18 +25,18 @@ class Matrix(
|
|||
return sessionStore.getStoredData()
|
||||
?.let { sessionData ->
|
||||
try {
|
||||
val client = ClientBuilder()
|
||||
ClientBuilder()
|
||||
.basePath(baseFolder.absolutePath)
|
||||
.username(sessionData.userId)
|
||||
.build()
|
||||
client.restoreLogin(sessionData.restoreToken)
|
||||
client
|
||||
.build().apply {
|
||||
restoreLogin(sessionData.restoreToken)
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
logError(throwable)
|
||||
null
|
||||
}
|
||||
}?.let {
|
||||
MatrixClient(it, sessionStore)
|
||||
MatrixClient(it, sessionStore, coroutineDispatchers)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -37,6 +45,6 @@ class Matrix(
|
|||
authService.configureHomeserver(homeserver)
|
||||
val client = authService.login(username, password, "MatrixRustSDKSample", null)
|
||||
sessionStore.storeData(SessionStore.SessionData(client.userId(), client.restoreToken()))
|
||||
return MatrixClient(client, sessionStore)
|
||||
return MatrixClient(client, sessionStore, coroutineDispatchers)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
||||
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 didUpdateRestoreToken() {
|
||||
Log.v(LOG_TAG, "didUpdateRestoreToken()")
|
||||
}
|
||||
}
|
||||
|
||||
private val slidingSyncObserver = object : SlidingSyncObserver {
|
||||
override fun didReceiveSyncUpdate(summary: UpdateSummary) {
|
||||
Log.v(LOG_TAG, "didReceiveSyncUpdate=$summary")
|
||||
roomSummaryDataSource.updateRoomsWithIdentifiers(summary.rooms)
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
val clientDelegate = object : ClientDelegate {
|
||||
override fun didReceiveAuthError(isSoftLogout: Boolean) {
|
||||
Log.v(LOG_TAG, "didReceiveAuthError()")
|
||||
}
|
||||
|
||||
override fun didReceiveSyncUpdate() {
|
||||
Log.v(LOG_TAG, "didReceiveSyncUpdate()")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
slidingSync.setObserver(slidingSyncObserver)
|
||||
slidingSyncObserverToken = slidingSync.sync()
|
||||
}
|
||||
|
||||
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()
|
||||
fun stopSync() {
|
||||
slidingSync.setObserver(null)
|
||||
slidingSyncObserverToken?.cancel()
|
||||
}
|
||||
|
||||
suspend fun logout() {
|
||||
fun roomSummaryDataSource(): RoomSummaryDataSource = roomSummaryDataSource
|
||||
|
||||
override fun close() {
|
||||
stopSync()
|
||||
client.setDelegate(null)
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
interface SlidingSyncListener {
|
||||
fun onSyncUpdate(summary: UpdateSummary, rooms: List<Room>)
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadMediaContentForSource(source: MediaSource): Result<List<UByte>> =
|
||||
withContext(dispatchers.io) {
|
||||
runCatching {
|
||||
client.getMediaContent(source)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
package io.element.android.x.matrix.core
|
||||
|
||||
@JvmInline
|
||||
value class EventId(val value: String)
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package io.element.android.x.matrix.core
|
||||
|
||||
@JvmInline
|
||||
value class RoomId(val value: String)
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package io.element.android.x.matrix.core
|
||||
|
||||
@JvmInline
|
||||
value class UserId(val value: String)
|
||||
|
|
@ -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 @@
|
|||
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 @@
|
|||
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 @@
|
|||
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 @@
|
|||
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()
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package io.element.android.x.matrix.store
|
||||
package io.element.android.x.matrix.session
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
|
|
@ -8,13 +8,13 @@ import androidx.datastore.preferences.core.stringPreferencesKey
|
|||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
|
||||
|
||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "elementx_sessions")
|
||||
private val userIdPreference = stringPreferencesKey("userId")
|
||||
// TODO It contains the access token, so it has to be stored in a more secured storage.
|
||||
// I would expect the Rust SDK to provide a more obscure token.
|
||||
private val restoreTokenPreference = stringPreferencesKey("restoreToken")
|
||||
|
||||
|
||||
internal class SessionStore(
|
||||
context: Context
|
||||
) {
|
||||
|
|
@ -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 @@
|
|||
@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…
Add table
Add a link
Reference in a new issue