Start implementing logic for room summary list
This commit is contained in:
parent
13eedb3cd3
commit
c1fcc51d09
20 changed files with 423 additions and 119 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 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)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 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
|
||||||
) {
|
) {
|
||||||
|
|
@ -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