First mapping of room. Still very dirty

This commit is contained in:
Benoit Marty 2022-10-12 15:38:11 +02:00
parent 58decf03d8
commit f906158f2b
11 changed files with 165 additions and 30 deletions

View file

@ -0,0 +1,14 @@
package io.element.android.x.core.data
import android.util.Log
inline fun <A> tryOrNull(message: String? = null, operation: () -> A): A? {
return try {
operation()
} catch (any: Throwable) {
if (message != null) {
Log.e("TAG", message, any)
}
null
}
}

View file

@ -28,7 +28,7 @@ android {
} }
dependencies { dependencies {
implementation(name: 'matrix-rust-sdk', ext: 'aar') api(name: 'matrix-rust-sdk', ext: 'aar')
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

@ -8,6 +8,8 @@ class MatrixClient internal constructor(
private val client: Client, private val client: Client,
private val sessionStore: SessionStore, private val sessionStore: SessionStore,
) { ) {
private val roomWrapper = RoomWrapper(client)
fun startSync() { fun startSync() {
val clientDelegate = object : ClientDelegate { val clientDelegate = object : ClientDelegate {
override fun didReceiveAuthError(isSoftLogout: Boolean) { override fun didReceiveAuthError(isSoftLogout: Boolean) {
@ -32,7 +34,7 @@ class MatrixClient internal constructor(
} }
} }
fun slidingSync(onSyncUpdate: (UpdateSummary) -> Unit): StoppableSpawn { fun slidingSync(listener: SlidingSyncListener): StoppableSpawn {
val slidingSyncView = SlidingSyncViewBuilder() val slidingSyncView = SlidingSyncViewBuilder()
.timelineLimit(limit = 10u) .timelineLimit(limit = 10u)
.requiredState(requiredState = listOf(RequiredState(key = "m.room.avatar", value = ""))) .requiredState(requiredState = listOf(RequiredState(key = "m.room.avatar", value = "")))
@ -49,7 +51,10 @@ class MatrixClient internal constructor(
slidingSync.setObserver(object : SlidingSyncObserver { slidingSync.setObserver(object : SlidingSyncObserver {
override fun didReceiveSyncUpdate(summary: UpdateSummary) { override fun didReceiveSyncUpdate(summary: UpdateSummary) {
Log.v(LOG_TAG, "didReceiveSyncUpdate=$summary") Log.v(LOG_TAG, "didReceiveSyncUpdate=$summary")
onSyncUpdate.invoke(summary) val rooms = summary.rooms.mapNotNull {
roomWrapper.getRoom(it)
}
listener.onSyncUpdate(summary, rooms)
} }
}) })
return slidingSync.sync() return slidingSync.sync()
@ -59,4 +64,11 @@ class MatrixClient internal constructor(
client.logout() client.logout()
sessionStore.reset() sessionStore.reset()
} }
fun username(): String = client.displayName()
fun avatarUrl(): String = client.avatarUrl()
interface SlidingSyncListener {
fun onSyncUpdate(summary: UpdateSummary, rooms: List<Room>)
}
} }

View file

@ -0,0 +1,15 @@
package io.element.android.x.sdk.matrix
import android.util.Log
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.Room
class RoomWrapper(
private val client: Client
) {
fun getRoom(roomId: String): Room? {
val rooms = client.rooms()
Log.d(LOG_TAG, "We have ${rooms.size} rooms")
return rooms.firstOrNull { it.id() == roomId }
}
}

View file

@ -7,13 +7,13 @@ import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.*
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue 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.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
@ -22,10 +22,10 @@ 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.ElementXTheme import io.element.android.x.ui.theme.ElementXTheme
import io.element.android.x.ui.theme.components.VectorButton import io.element.android.x.ui.theme.components.VectorButton
import io.element.android.x.ui.theme.components.VectorTextField
class LoginActivity : ComponentActivity() { class LoginActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -44,21 +44,34 @@ class LoginActivity : ComponentActivity() {
val viewModel: LoginViewModel = mavericksViewModel() val viewModel: LoginViewModel = mavericksViewModel()
val state by viewModel.collectAsState() val state by viewModel.collectAsState()
val isError = state.isLoggedIn is Fail val isError = state.isLoggedIn is Fail
VectorTextField(value = state.homeserver, OutlinedTextField(
value = state.homeserver,
onValueChange = { onValueChange = {
viewModel.handle(LoginActions.SetHomeserver(it)) viewModel.handle(LoginActions.SetHomeserver(it))
}) },
VectorTextField( keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Uri,
),
)
OutlinedTextField(
value = state.login, value = state.login,
onValueChange = { onValueChange = {
viewModel.handle(LoginActions.SetLogin(it)) viewModel.handle(LoginActions.SetLogin(it))
}) },
VectorTextField( keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
),
)
OutlinedTextField(
value = state.password, value = state.password,
onValueChange = { onValueChange = {
viewModel.handle(LoginActions.SetPassword(it)) viewModel.handle(LoginActions.SetPassword(it))
}, },
isError = isError isError = isError,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Send,
),
) )
if (isError) { if (isError) {
Text( Text(

View file

@ -58,4 +58,5 @@ dependencies {
implementation 'androidx.fragment:fragment-ktx:1.5.3' implementation 'androidx.fragment:fragment-ktx:1.5.3'
implementation 'com.airbnb.android:mavericks-compose:2.7.0' implementation 'com.airbnb.android:mavericks-compose:2.7.0'
implementation 'io.coil-kt:coil-compose:2.2.1'
} }

View file

@ -0,0 +1,6 @@
package io.element.android.x.ui.screen.roomlist
data class MatrixUser(
val username: String? = null,
val avatarUrl: String? = null,
)

View file

@ -1,6 +1,7 @@
package io.element.android.x.ui.screen.roomlist package io.element.android.x.ui.screen.roomlist
sealed interface RoomListActions { sealed interface RoomListActions {
object Logout : RoomListActions object Init : RoomListActions
object LoadMore : RoomListActions object LoadMore : RoomListActions
object Logout : RoomListActions
} }

View file

@ -3,13 +3,15 @@ package io.element.android.x.ui.screen.roomlist
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.compose.foundation.layout.Column import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExitToApp
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter
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
@ -17,12 +19,18 @@ import io.element.android.x.ui.theme.ElementXTheme
class RoomListActivity : ComponentActivity() { class RoomListActivity : ComponentActivity() {
private var initDone = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContent { setContent {
ElementXTheme { ElementXTheme {
val viewModel: RoomListViewModel = mavericksViewModel() val viewModel: RoomListViewModel = mavericksViewModel()
if (!initDone) {
initDone = true
viewModel.handle(RoomListActions.Init)
}
val state = viewModel.collectAsState() val state = viewModel.collectAsState()
// A surface container using the 'background' color from the theme // A surface container using the 'background' color from the theme
Surface( Surface(
@ -34,12 +42,13 @@ class RoomListActivity : ComponentActivity() {
Column( Column(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
OptionMenu(viewModel) OptionMenu(state.value.user, viewModel)
/* TODO val rooms = state.value.rooms
val state = viewModel.state.collectAsState().value if (rooms is Success) {
RoomListHeader() rooms().forEach {
RoomList() Text(text = "Room: ${it.name() ?: it.id()}")
*/ }
}
} }
} }
if (state.value.logoutAction is Success) { if (state.value.logoutAction is Success) {
@ -51,14 +60,25 @@ class RoomListActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun OptionMenu(viewModel: RoomListViewModel) { private fun OptionMenu(matrixUser: MatrixUser, viewModel: RoomListViewModel) {
TopAppBar( TopAppBar(
title = { Text("Room List") }, title = {
Row(
modifier = Modifier.fillMaxWidth()
) {
Image(
painter = rememberAsyncImagePainter(matrixUser.avatarUrl),
contentDescription = null,
modifier = Modifier.size(48.dp)
)
Text("${matrixUser.username}")
}
},
actions = { actions = {
Button( IconButton(
onClick = { viewModel.handle(RoomListActions.Logout) } onClick = { viewModel.handle(RoomListActions.Logout) }
) { ) {
Text(text = "logout") Icon(Icons.Default.ExitToApp, contentDescription = "logout")
} }
} }
) )

View file

@ -4,22 +4,43 @@ 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.sdk.matrix.MatrixClient import io.element.android.x.sdk.matrix.MatrixClient
import io.element.android.x.sdk.matrix.MatrixInstance import io.element.android.x.sdk.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
class RoomListViewModel(initialState: RoomListViewState) : class RoomListViewModel(initialState: RoomListViewState) :
MavericksViewModel<RoomListViewState>(initialState) { MavericksViewModel<RoomListViewState>(initialState), MatrixClient.SlidingSyncListener {
private var sync: StoppableSpawn? = null
private val matrix = MatrixInstance.getInstance() private val matrix = MatrixInstance.getInstance()
fun handle(action: RoomListActions) { fun handle(action: RoomListActions) {
when (action) { when (action) {
RoomListActions.Init -> handleInit()
RoomListActions.LoadMore -> TODO() RoomListActions.LoadMore -> TODO()
RoomListActions.Logout -> handleLogout() RoomListActions.Logout -> handleLogout()
} }
} }
private fun handleInit() {
viewModelScope.launch {
val client = getClient()
setState {
copy(
user = MatrixUser(
tryOrNull { client.username() } ?: "Room list",
tryOrNull { client.avatarUrl() } ?: "https://previews.123rf.com/images/lkeskinen/lkeskinen1802/lkeskinen180208322/95731150-exemple-de-tampon-%C3%A9tiquette-typographique-timbre-ou-ic%C3%B4ne.jpg",
)
)
}
sync = client.slidingSync(listener = this@RoomListViewModel)
}
}
private fun handleLogout() { private fun handleLogout() {
viewModelScope.launch { viewModelScope.launch {
setState { copy(logoutAction = Loading()) } setState { copy(logoutAction = Loading()) }
@ -35,4 +56,32 @@ class RoomListViewModel(initialState: RoomListViewState) :
private suspend fun getClient(): MatrixClient { private suspend fun getClient(): MatrixClient {
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() {
super.onCleared()
sync?.cancel()
}
} }

View file

@ -3,9 +3,13 @@ package io.element.android.x.ui.screen.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 org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.UpdateSummary
data class RoomListViewState( data class RoomListViewState(
val list: List<String> = emptyList(), val user: MatrixUser = MatrixUser(),
val rooms: Async<List<Room>> = 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