Start implementing logic for room summary list
This commit is contained in:
@@ -1,10 +1,15 @@
|
||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
package io.element.android.x
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.ramcosta.composedestinations.DestinationsNavHost
|
||||
import com.ramcosta.composedestinations.rememberNavHostEngine
|
||||
import io.element.android.x.designsystem.ElementXTheme
|
||||
@@ -32,6 +37,7 @@ private fun MainScreen(viewModel: MainViewModel) {
|
||||
val startRoute = runBlocking {
|
||||
if (!viewModel.hasSession()) LoginScreenNavigationDestination else NavGraphs.root.startRoute
|
||||
}
|
||||
|
||||
DestinationsNavHost(
|
||||
engine = engine,
|
||||
navController = navController,
|
||||
|
||||
@@ -12,17 +12,19 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.compose.collectAsState
|
||||
import com.airbnb.mvrx.compose.mavericksViewModel
|
||||
import io.element.android.x.ui.theme.components.Avatar
|
||||
import org.matrix.rustcomponents.sdk.Room
|
||||
import io.element.android.x.designsystem.components.Avatar
|
||||
import io.element.android.x.matrix.core.RoomId
|
||||
import io.element.android.x.matrix.room.RoomSummary
|
||||
|
||||
@Composable
|
||||
fun RoomListScreen(
|
||||
viewModel: RoomListViewModel = mavericksViewModel(),
|
||||
onRoomClicked: (String) -> Unit = { },
|
||||
onLogoutClicked: () -> Unit = { },
|
||||
onRoomClicked: (RoomId) -> Unit = { }
|
||||
) {
|
||||
val state by viewModel.collectAsState()
|
||||
RoomListContent(state, onRoomClicked, onLogoutClicked)
|
||||
@@ -31,8 +33,8 @@ fun RoomListScreen(
|
||||
@Composable
|
||||
fun RoomListContent(
|
||||
state: RoomListViewState,
|
||||
onRoomClicked: (String) -> Unit,
|
||||
onLogoutClicked: () -> Unit
|
||||
onRoomClicked: (RoomId) -> Unit,
|
||||
onLogoutClicked: () -> Unit,
|
||||
) {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
Column(
|
||||
@@ -62,7 +64,8 @@ fun RoomListTopBar(state: RoomListViewState, onLogoutClicked: () -> Unit) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val matrixUser = state.user
|
||||
Avatar(data = matrixUser.avatarData)
|
||||
@@ -83,19 +86,24 @@ fun RoomListTopBar(state: RoomListViewState, onLogoutClicked: () -> Unit) {
|
||||
@Composable
|
||||
private fun RoomItem(
|
||||
modifier: Modifier = Modifier,
|
||||
room: Room,
|
||||
onClick: (String) -> Unit
|
||||
room: RoomSummary,
|
||||
onClick: (RoomId) -> Unit
|
||||
) {
|
||||
if (room !is RoomSummary.Filled) {
|
||||
return
|
||||
}
|
||||
val details = room.details
|
||||
Row(verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier
|
||||
.clickable {
|
||||
onClick(room.id())
|
||||
onClick(room.details.roomId)
|
||||
}
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp)
|
||||
) {
|
||||
Column(modifier = modifier.padding(8.dp)) {
|
||||
Text(text = "Room: ${room.name() ?: room.id()}")
|
||||
Text(text = if (room.isDirect()) "Direct" else "Room")
|
||||
Text(fontSize = 18.sp, text = details.name.orEmpty())
|
||||
Text(text = details.lastMessage?.toString().orEmpty(), maxLines = 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,19 +4,14 @@ import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.MavericksViewModel
|
||||
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.MatrixInstance
|
||||
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
|
||||
|
||||
class RoomListViewModel(initialState: RoomListViewState) :
|
||||
MavericksViewModel<RoomListViewState>(initialState), MatrixClient.SlidingSyncListener {
|
||||
MavericksViewModel<RoomListViewState>(initialState) {
|
||||
|
||||
private var sync: StoppableSpawn? = null
|
||||
private val matrix = MatrixInstance.getInstance()
|
||||
|
||||
init {
|
||||
@@ -33,18 +28,26 @@ class RoomListViewModel(initialState: RoomListViewState) :
|
||||
private fun handleInit() {
|
||||
viewModelScope.launch {
|
||||
val client = getClient()
|
||||
val url = client.avatarUrl()
|
||||
val mediaSource = mediaSourceFromUrl(url)
|
||||
client.startSync()
|
||||
val userAvatarUrl = client.loadUserAvatarURLString().getOrNull()
|
||||
val userDisplayName = client.loadUserDisplayName().getOrNull()
|
||||
val avatarData = userAvatarUrl?.let {
|
||||
mediaSourceFromUrl(it)
|
||||
}?.let {
|
||||
client.loadMediaContentForSource(it)
|
||||
}
|
||||
setState {
|
||||
copy(
|
||||
user = MatrixUser(
|
||||
username = tryOrNull { client.username() } ?: "Room list",
|
||||
avatarUrl = mediaSource.url(),
|
||||
avatarData = client.loadMedia2(url)
|
||||
username = userDisplayName,
|
||||
avatarUrl = userAvatarUrl,
|
||||
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()!!
|
||||
}
|
||||
|
||||
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() {
|
||||
super.onCleared()
|
||||
sync?.cancel()
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,13 @@ package io.element.android.x.features.roomlist
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.MavericksState
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import io.element.android.x.matrix.room.RoomSummary
|
||||
import org.matrix.rustcomponents.sdk.Room
|
||||
import org.matrix.rustcomponents.sdk.UpdateSummary
|
||||
|
||||
data class RoomListViewState(
|
||||
val user: MatrixUser = MatrixUser(),
|
||||
val rooms: Async<List<Room>> = Uninitialized,
|
||||
val summary: Async<UpdateSummary> = Uninitialized,
|
||||
val rooms: Async<List<RoomSummary>> = Uninitialized,
|
||||
val canLoadMore: Boolean = false,
|
||||
val logoutAction: Async<Unit> = Uninitialized,
|
||||
) : MavericksState
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
package io.element.android.x.ui.theme.components
|
||||
package io.element.android.x.designsystem.components
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.Image
|
||||
@@ -9,6 +9,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
|
||||
@@ -18,7 +19,7 @@ import coil.compose.rememberAsyncImagePainter
|
||||
@Composable
|
||||
fun Avatar(
|
||||
data: List<UByte>?,
|
||||
size: Int = 48,
|
||||
size: Dp = 48.dp,
|
||||
) {
|
||||
Image(
|
||||
painter = rememberAsyncImagePainter(
|
||||
@@ -28,7 +29,7 @@ fun Avatar(
|
||||
}),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(size.dp)
|
||||
.size(size)
|
||||
.clip(CircleShape)
|
||||
.border(1.5.dp, MaterialTheme.colorScheme.primary, CircleShape)
|
||||
)
|
||||
|
||||
@@ -29,6 +29,7 @@ android {
|
||||
|
||||
dependencies {
|
||||
api(name: 'matrix-rust-sdk', ext: 'aar')
|
||||
implementation project(":libraries:core")
|
||||
implementation "net.java.dev.jna:jna:5.10.0@aar"
|
||||
implementation 'androidx.datastore:datastore-core:1.0.0'
|
||||
implementation 'androidx.datastore:datastore-preferences:1.0.0'
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user