Start implementing logic for room summary list

This commit is contained in:
ganfra 2022-10-28 20:17:18 +02:00
parent 13eedb3cd3
commit c1fcc51d09
20 changed files with 423 additions and 119 deletions

View file

@ -1,10 +1,15 @@
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.x package io.element.android.x
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.ramcosta.composedestinations.DestinationsNavHost import com.ramcosta.composedestinations.DestinationsNavHost
import com.ramcosta.composedestinations.rememberNavHostEngine import com.ramcosta.composedestinations.rememberNavHostEngine
import io.element.android.x.designsystem.ElementXTheme import io.element.android.x.designsystem.ElementXTheme
@ -32,6 +37,7 @@ private fun MainScreen(viewModel: MainViewModel) {
val startRoute = runBlocking { val startRoute = runBlocking {
if (!viewModel.hasSession()) LoginScreenNavigationDestination else NavGraphs.root.startRoute if (!viewModel.hasSession()) LoginScreenNavigationDestination else NavGraphs.root.startRoute
} }
DestinationsNavHost( DestinationsNavHost(
engine = engine, engine = engine,
navController = navController, navController = navController,

View file

@ -12,17 +12,19 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.compose.collectAsState import com.airbnb.mvrx.compose.collectAsState
import com.airbnb.mvrx.compose.mavericksViewModel import com.airbnb.mvrx.compose.mavericksViewModel
import io.element.android.x.ui.theme.components.Avatar import io.element.android.x.designsystem.components.Avatar
import org.matrix.rustcomponents.sdk.Room import io.element.android.x.matrix.core.RoomId
import io.element.android.x.matrix.room.RoomSummary
@Composable @Composable
fun RoomListScreen( fun RoomListScreen(
viewModel: RoomListViewModel = mavericksViewModel(), viewModel: RoomListViewModel = mavericksViewModel(),
onRoomClicked: (String) -> Unit = { },
onLogoutClicked: () -> Unit = { }, onLogoutClicked: () -> Unit = { },
onRoomClicked: (RoomId) -> Unit = { }
) { ) {
val state by viewModel.collectAsState() val state by viewModel.collectAsState()
RoomListContent(state, onRoomClicked, onLogoutClicked) RoomListContent(state, onRoomClicked, onLogoutClicked)
@ -31,8 +33,8 @@ fun RoomListScreen(
@Composable @Composable
fun RoomListContent( fun RoomListContent(
state: RoomListViewState, state: RoomListViewState,
onRoomClicked: (String) -> Unit, onRoomClicked: (RoomId) -> Unit,
onLogoutClicked: () -> Unit onLogoutClicked: () -> Unit,
) { ) {
Surface(color = MaterialTheme.colorScheme.background) { Surface(color = MaterialTheme.colorScheme.background) {
Column( Column(
@ -62,7 +64,8 @@ fun RoomListTopBar(state: RoomListViewState, onLogoutClicked: () -> Unit) {
TopAppBar( TopAppBar(
title = { title = {
Row( Row(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) { ) {
val matrixUser = state.user val matrixUser = state.user
Avatar(data = matrixUser.avatarData) Avatar(data = matrixUser.avatarData)
@ -83,19 +86,24 @@ fun RoomListTopBar(state: RoomListViewState, onLogoutClicked: () -> Unit) {
@Composable @Composable
private fun RoomItem( private fun RoomItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
room: Room, room: RoomSummary,
onClick: (String) -> Unit onClick: (RoomId) -> Unit
) { ) {
if (room !is RoomSummary.Filled) {
return
}
val details = room.details
Row(verticalAlignment = Alignment.CenterVertically, Row(verticalAlignment = Alignment.CenterVertically,
modifier = modifier modifier = modifier
.clickable { .clickable {
onClick(room.id()) onClick(room.details.roomId)
} }
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 8.dp)
) { ) {
Column(modifier = modifier.padding(8.dp)) { Column(modifier = modifier.padding(8.dp)) {
Text(text = "Room: ${room.name() ?: room.id()}") Text(fontSize = 18.sp, text = details.name.orEmpty())
Text(text = if (room.isDirect()) "Direct" else "Room") Text(text = details.lastMessage?.toString().orEmpty(), maxLines = 2)
} }
} }
} }

View file

@ -4,19 +4,14 @@ import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MavericksViewModel import com.airbnb.mvrx.MavericksViewModel
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import io.element.android.x.core.data.tryOrNull
import io.element.android.x.matrix.MatrixClient import io.element.android.x.matrix.MatrixClient
import io.element.android.x.matrix.MatrixInstance import io.element.android.x.matrix.MatrixInstance
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.StoppableSpawn
import org.matrix.rustcomponents.sdk.UpdateSummary
import org.matrix.rustcomponents.sdk.mediaSourceFromUrl import org.matrix.rustcomponents.sdk.mediaSourceFromUrl
class RoomListViewModel(initialState: RoomListViewState) : class RoomListViewModel(initialState: RoomListViewState) :
MavericksViewModel<RoomListViewState>(initialState), MatrixClient.SlidingSyncListener { MavericksViewModel<RoomListViewState>(initialState) {
private var sync: StoppableSpawn? = null
private val matrix = MatrixInstance.getInstance() private val matrix = MatrixInstance.getInstance()
init { init {
@ -33,18 +28,26 @@ class RoomListViewModel(initialState: RoomListViewState) :
private fun handleInit() { private fun handleInit() {
viewModelScope.launch { viewModelScope.launch {
val client = getClient() val client = getClient()
val url = client.avatarUrl() client.startSync()
val mediaSource = mediaSourceFromUrl(url) val userAvatarUrl = client.loadUserAvatarURLString().getOrNull()
val userDisplayName = client.loadUserDisplayName().getOrNull()
val avatarData = userAvatarUrl?.let {
mediaSourceFromUrl(it)
}?.let {
client.loadMediaContentForSource(it)
}
setState { setState {
copy( copy(
user = MatrixUser( user = MatrixUser(
username = tryOrNull { client.username() } ?: "Room list", username = userDisplayName,
avatarUrl = mediaSource.url(), avatarUrl = userAvatarUrl,
avatarData = client.loadMedia2(url) avatarData = avatarData?.getOrNull()
) )
) )
} }
sync = client.slidingSync(listener = this@RoomListViewModel) client.roomSummaryDataSource().roomSummaries().execute {
copy(rooms = it)
}
} }
} }
@ -64,31 +67,7 @@ class RoomListViewModel(initialState: RoomListViewState) :
return matrix.restoreSession()!! return matrix.restoreSession()!!
} }
override fun onSyncUpdate(
summary: UpdateSummary,
rooms: List<Room>
) = withState { state ->
val list = state.rooms().orEmpty().toMutableList()
rooms.forEach { room ->
// Either replace or add the room
val idx = list.indexOfFirst { it.id() == room.id() }
if (idx == -1) {
list.add(room)
} else {
list[idx] = room
}
}
setState {
copy(
rooms = Success(list),
summary = Success(summary)
)
}
}
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
sync?.cancel()
} }
} }

View file

@ -3,13 +3,13 @@ package io.element.android.x.features.roomlist
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import io.element.android.x.matrix.room.RoomSummary
import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.UpdateSummary import org.matrix.rustcomponents.sdk.UpdateSummary
data class RoomListViewState( data class RoomListViewState(
val user: MatrixUser = MatrixUser(), val user: MatrixUser = MatrixUser(),
val rooms: Async<List<Room>> = Uninitialized, val rooms: Async<List<RoomSummary>> = Uninitialized,
val summary: Async<UpdateSummary> = Uninitialized,
val canLoadMore: Boolean = false, val canLoadMore: Boolean = false,
val logoutAction: Async<Unit> = Uninitialized, val logoutAction: Async<Unit> = Uninitialized,
) : MavericksState ) : MavericksState

View file

@ -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,
)

View file

@ -1,4 +1,4 @@
package io.element.android.x.ui.theme.components package io.element.android.x.designsystem.components
import android.util.Log import android.util.Log
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
@ -9,6 +9,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter import coil.compose.rememberAsyncImagePainter
@ -18,7 +19,7 @@ import coil.compose.rememberAsyncImagePainter
@Composable @Composable
fun Avatar( fun Avatar(
data: List<UByte>?, data: List<UByte>?,
size: Int = 48, size: Dp = 48.dp,
) { ) {
Image( Image(
painter = rememberAsyncImagePainter( painter = rememberAsyncImagePainter(
@ -28,7 +29,7 @@ fun Avatar(
}), }),
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.size(size.dp) .size(size)
.clip(CircleShape) .clip(CircleShape)
.border(1.5.dp, MaterialTheme.colorScheme.primary, CircleShape) .border(1.5.dp, MaterialTheme.colorScheme.primary, CircleShape)
) )

View file

@ -29,6 +29,7 @@ android {
dependencies { dependencies {
api(name: 'matrix-rust-sdk', ext: 'aar') api(name: 'matrix-rust-sdk', ext: 'aar')
implementation project(":libraries:core")
implementation "net.java.dev.jna:jna:5.10.0@aar" implementation "net.java.dev.jna:jna:5.10.0@aar"
implementation 'androidx.datastore:datastore-core:1.0.0' implementation 'androidx.datastore:datastore-core:1.0.0'
implementation 'androidx.datastore:datastore-preferences:1.0.0' implementation 'androidx.datastore:datastore-preferences:1.0.0'

View file

@ -1,8 +1,10 @@
package io.element.android.x.matrix package io.element.android.x.matrix
import android.content.Context 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 io.element.android.x.matrix.util.logError
import kotlinx.coroutines.Dispatchers
import org.matrix.rustcomponents.sdk.AuthenticationService import org.matrix.rustcomponents.sdk.AuthenticationService
import org.matrix.rustcomponents.sdk.ClientBuilder import org.matrix.rustcomponents.sdk.ClientBuilder
import java.io.File import java.io.File
@ -10,6 +12,12 @@ import java.io.File
class Matrix( class Matrix(
context: Context, context: Context,
) { ) {
private val coroutineDispatchers = CoroutineDispatchers(
io = Dispatchers.IO,
computation = Dispatchers.Default,
main = Dispatchers.Main
)
private val baseFolder = File(context.filesDir, "matrix") private val baseFolder = File(context.filesDir, "matrix")
private val sessionStore = SessionStore(context) private val sessionStore = SessionStore(context)
@ -17,18 +25,18 @@ class Matrix(
return sessionStore.getStoredData() return sessionStore.getStoredData()
?.let { sessionData -> ?.let { sessionData ->
try { try {
val client = ClientBuilder() ClientBuilder()
.basePath(baseFolder.absolutePath) .basePath(baseFolder.absolutePath)
.username(sessionData.userId) .username(sessionData.userId)
.build() .build().apply {
client.restoreLogin(sessionData.restoreToken) restoreLogin(sessionData.restoreToken)
client }
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
logError(throwable) logError(throwable)
null null
} }
}?.let { }?.let {
MatrixClient(it, sessionStore) MatrixClient(it, sessionStore, coroutineDispatchers)
} }
} }
@ -37,6 +45,6 @@ class Matrix(
authService.configureHomeserver(homeserver) authService.configureHomeserver(homeserver)
val client = authService.login(username, password, "MatrixRustSDKSample", null) val client = authService.login(username, password, "MatrixRustSDKSample", null)
sessionStore.storeData(SessionStore.SessionData(client.userId(), client.restoreToken())) sessionStore.storeData(SessionStore.SessionData(client.userId(), client.restoreToken()))
return MatrixClient(client, sessionStore) return MatrixClient(client, sessionStore, coroutineDispatchers)
} }
} }

View file

@ -1,78 +1,105 @@
package io.element.android.x.matrix package io.element.android.x.matrix
import android.util.Log 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 org.matrix.rustcomponents.sdk.*
import java.io.Closeable
class MatrixClient internal constructor( class MatrixClient internal constructor(
private val client: Client, private val client: Client,
private val sessionStore: SessionStore, private val sessionStore: SessionStore,
) { private val dispatchers: CoroutineDispatchers,
private val roomWrapper = RoomWrapper(client) ) : 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() { fun startSync() {
val clientDelegate = object : ClientDelegate { slidingSync.setObserver(slidingSyncObserver)
override fun didReceiveAuthError(isSoftLogout: Boolean) { slidingSyncObserverToken = slidingSync.sync()
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)
}
} }
fun slidingSync(listener: SlidingSyncListener): StoppableSpawn { fun stopSync() {
val slidingSyncView = SlidingSyncViewBuilder() slidingSync.setObserver(null)
.timelineLimit(limit = 10u) slidingSyncObserverToken?.cancel()
.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()
} }
suspend fun logout() { fun roomSummaryDataSource(): RoomSummaryDataSource = roomSummaryDataSource
override fun close() {
stopSync()
client.setDelegate(null)
}
suspend fun logout() = withContext(dispatchers.io) {
close()
client.logout() client.logout()
sessionStore.reset() sessionStore.reset()
} }
fun userId(): String = client.userId() fun userId(): UserId = UserId(client.userId())
fun username(): String = client.displayName() suspend fun loadUserDisplayName(): Result<String> = withContext(dispatchers.io) {
fun avatarUrl(): String = client.avatarUrl() runCatching {
client.displayName()
fun loadMedia(source: MediaSource) = client.getMediaContent(source) }
fun loadMedia2(mxcUrl: String) = client.getMediaContent(mediaSourceFromUrl(mxcUrl))
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)
}
}
} }

View file

@ -0,0 +1,4 @@
package io.element.android.x.matrix.core
@JvmInline
value class EventId(val value: String)

View file

@ -0,0 +1,4 @@
package io.element.android.x.matrix.core
@JvmInline
value class RoomId(val value: String)

View file

@ -0,0 +1,4 @@
package io.element.android.x.matrix.core
@JvmInline
value class UserId(val value: String)

View file

@ -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())
}

View file

@ -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,
)

View file

@ -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
}
}

View file

@ -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,
)

View file

@ -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()
)
}
}

View file

@ -1,4 +1,4 @@
package io.element.android.x.matrix.store package io.element.android.x.matrix.session
import android.content.Context import android.content.Context
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
@ -8,13 +8,13 @@ import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "elementx_sessions") private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "elementx_sessions")
private val userIdPreference = stringPreferencesKey("userId") private val userIdPreference = stringPreferencesKey("userId")
// TODO It contains the access token, so it has to be stored in a more secured storage. // 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. // I would expect the Rust SDK to provide a more obscure token.
private val restoreTokenPreference = stringPreferencesKey("restoreToken") private val restoreTokenPreference = stringPreferencesKey("restoreToken")
internal class SessionStore( internal class SessionStore(
context: Context context: Context
) { ) {

View file

@ -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)
}

View file

@ -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()
}
}