Fix bunch of issues around login/logout and introduces messages feature
This commit is contained in:
parent
bb88919bb3
commit
823cef8b7c
19 changed files with 314 additions and 89 deletions
|
|
@ -68,6 +68,7 @@ dependencies {
|
||||||
implementation project(":libraries:matrix")
|
implementation project(":libraries:matrix")
|
||||||
implementation project(":features:login")
|
implementation project(":features:login")
|
||||||
implementation project(":features:roomlist")
|
implementation project(":features:roomlist")
|
||||||
|
implementation project(":features:messages")
|
||||||
|
|
||||||
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.2.0"
|
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.2.0"
|
||||||
implementation 'io.github.raamcosta.compose-destinations:core:1.7.23-beta'
|
implementation 'io.github.raamcosta.compose-destinations:core:1.7.23-beta'
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,12 @@ private fun MainScreen(viewModel: MainViewModel) {
|
||||||
val engine = rememberNavHostEngine()
|
val engine = rememberNavHostEngine()
|
||||||
val navController = engine.rememberNavController()
|
val navController = engine.rememberNavController()
|
||||||
val startRoute = runBlocking {
|
val startRoute = runBlocking {
|
||||||
if (!viewModel.hasSession()) LoginScreenNavigationDestination else NavGraphs.root.startRoute
|
if (!viewModel.isLoggedIn()) {
|
||||||
|
LoginScreenNavigationDestination
|
||||||
|
} else {
|
||||||
|
viewModel.restoreSession()
|
||||||
|
NavGraphs.root.startRoute
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DestinationsNavHost(
|
DestinationsNavHost(
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,16 @@ package io.element.android.x
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import io.element.android.x.matrix.MatrixInstance
|
import io.element.android.x.matrix.MatrixInstance
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
||||||
class MainViewModel : ViewModel() {
|
class MainViewModel : ViewModel() {
|
||||||
private val matrix = MatrixInstance.getInstance()
|
private val matrix = MatrixInstance.getInstance()
|
||||||
|
|
||||||
suspend fun hasSession(): Boolean {
|
suspend fun isLoggedIn(): Boolean {
|
||||||
return matrix.restoreSession() != null
|
return matrix.isLoggedIn().first()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun restoreSession() {
|
||||||
|
matrix.restoreSession()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,17 +4,25 @@ import androidx.compose.runtime.Composable
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
import com.ramcosta.composedestinations.annotation.RootNavGraph
|
import com.ramcosta.composedestinations.annotation.RootNavGraph
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
|
import com.ramcosta.composedestinations.navigation.popUpTo
|
||||||
import io.element.android.x.destinations.LoginScreenNavigationDestination
|
import io.element.android.x.destinations.LoginScreenNavigationDestination
|
||||||
|
import io.element.android.x.destinations.MessagesScreenNavigationDestination
|
||||||
import io.element.android.x.destinations.RoomListScreenNavigationDestination
|
import io.element.android.x.destinations.RoomListScreenNavigationDestination
|
||||||
import io.element.android.x.features.login.LoginScreen
|
import io.element.android.x.features.login.LoginScreen
|
||||||
|
import io.element.android.x.features.messages.MessagesScreen
|
||||||
import io.element.android.x.features.roomlist.RoomListScreen
|
import io.element.android.x.features.roomlist.RoomListScreen
|
||||||
|
import io.element.android.x.matrix.core.RoomId
|
||||||
|
|
||||||
@Destination
|
@Destination
|
||||||
@Composable
|
@Composable
|
||||||
fun LoginScreenNavigation(navigator: DestinationsNavigator) {
|
fun LoginScreenNavigation(navigator: DestinationsNavigator) {
|
||||||
LoginScreen(
|
LoginScreen(
|
||||||
onLoginWithSuccess = {
|
onLoginWithSuccess = {
|
||||||
navigator.clearBackStack(RoomListScreenNavigationDestination)
|
navigator.navigate(RoomListScreenNavigationDestination){
|
||||||
|
popUpTo(LoginScreenNavigationDestination){
|
||||||
|
inclusive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -23,9 +31,24 @@ fun LoginScreenNavigation(navigator: DestinationsNavigator) {
|
||||||
@Destination
|
@Destination
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomListScreenNavigation(navigator: DestinationsNavigator) {
|
fun RoomListScreenNavigation(navigator: DestinationsNavigator) {
|
||||||
RoomListScreen(onSuccessLogout = {
|
RoomListScreen(
|
||||||
navigator.clearBackStack(LoginScreenNavigationDestination)
|
onRoomClicked = { roomId: RoomId ->
|
||||||
})
|
navigator.navigate(MessagesScreenNavigationDestination(roomId = roomId.value))
|
||||||
|
},
|
||||||
|
onSuccessLogout = {
|
||||||
|
navigator.navigate(LoginScreenNavigationDestination){
|
||||||
|
popUpTo(RoomListScreenNavigationDestination){
|
||||||
|
inclusive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@Destination
|
||||||
|
@Composable
|
||||||
|
fun MessagesScreenNavigation(roomId: String) {
|
||||||
|
MessagesScreen(roomId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
package io.element.android.x.features.login
|
package io.element.android.x.features.login
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import com.airbnb.mvrx.Fail
|
|
||||||
import com.airbnb.mvrx.Loading
|
|
||||||
import com.airbnb.mvrx.MavericksViewModel
|
import com.airbnb.mvrx.MavericksViewModel
|
||||||
import com.airbnb.mvrx.Success
|
|
||||||
import io.element.android.x.matrix.MatrixInstance
|
import io.element.android.x.matrix.MatrixInstance
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
@ -24,13 +20,11 @@ class LoginViewModel(initialState: LoginViewState) :
|
||||||
|
|
||||||
private fun handleSubmit() = withState { state ->
|
private fun handleSubmit() = withState { state ->
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
setState { copy(isLoggedIn = Loading()) }
|
suspend {
|
||||||
try {
|
|
||||||
matrix.login(state.homeserver, state.login, state.password)
|
matrix.login(state.homeserver, state.login, state.password)
|
||||||
setState { copy(isLoggedIn = Success(Unit)) }
|
Unit
|
||||||
} catch (throwable: Throwable) {
|
}.execute {
|
||||||
Log.e("Error", "Cannot login", throwable)
|
copy(isLoggedIn = it)
|
||||||
setState { copy(isLoggedIn = Fail(throwable)) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
plugins {
|
|
||||||
id 'com.android.library'
|
|
||||||
id 'org.jetbrains.kotlin.android'
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace 'io.element.android.x.features.messages'
|
|
||||||
compileSdk 32
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
minSdk 24
|
|
||||||
targetSdk 32
|
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
||||||
consumerProguardFiles "consumer-rules.pro"
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
minifyEnabled false
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = '1.8'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
|
|
||||||
implementation 'androidx.core:core-ktx:1.7.0'
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
|
||||||
implementation 'com.google.android.material:material:1.5.0'
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
|
||||||
}
|
|
||||||
19
features/messages/build.gradle.kts
Normal file
19
features/messages/build.gradle.kts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
plugins {
|
||||||
|
id("io.element.android-compose")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.x.features.messages"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":libraries:core"))
|
||||||
|
implementation(project(":libraries:matrix"))
|
||||||
|
implementation(project(":libraries:designsystem"))
|
||||||
|
implementation(libs.mavericks.compose)
|
||||||
|
implementation(libs.timber)
|
||||||
|
implementation(libs.datetime)
|
||||||
|
testImplementation("junit:junit:4.13.2")
|
||||||
|
androidTestImplementation("androidx.test.ext:junit:1.1.3")
|
||||||
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
|
||||||
|
}
|
||||||
2
features/messages/proguard-rules.pro
vendored
2
features/messages/proguard-rules.pro
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
# Add project specific ProGuard rules here.
|
# Add project specific ProGuard rules here.
|
||||||
# You can control the set of applied configuration files using the
|
# You can control the set of applied configuration files using the
|
||||||
# proguardFiles setting in build.gradle.
|
# proguardFiles setting in build.gradle.kts.
|
||||||
#
|
#
|
||||||
# For more details, see
|
# For more details, see
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
|
||||||
|
package io.element.android.x.features.messages
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import com.airbnb.mvrx.compose.collectAsState
|
||||||
|
import com.airbnb.mvrx.compose.mavericksViewModel
|
||||||
|
import io.element.android.x.core.data.LogCompositions
|
||||||
|
import io.element.android.x.features.messages.model.MessagesViewState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MessagesScreen(roomId: String) {
|
||||||
|
val viewModel: MessagesViewModel = mavericksViewModel(argsFactory = { roomId })
|
||||||
|
LogCompositions(tag = "MessagesScreen", msg = "Root")
|
||||||
|
val roomTitle by viewModel.collectAsState(prop1 = MessagesViewState::roomTitle)
|
||||||
|
MessagesContent(roomTitle)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MessagesContent(roomTitle: String) {
|
||||||
|
val appBarState = rememberTopAppBarState()
|
||||||
|
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(appBarState)
|
||||||
|
LogCompositions(tag = "RoomListScreen", msg = "Content")
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text(text = roomTitle) }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
content = { padding ->
|
||||||
|
Box(modifier = Modifier.padding(padding))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
package io.element.android.x.features.messages
|
||||||
|
|
||||||
|
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.parallelMap
|
||||||
|
import io.element.android.x.designsystem.components.avatar.AvatarData
|
||||||
|
import io.element.android.x.designsystem.components.avatar.AvatarSize
|
||||||
|
import io.element.android.x.features.messages.model.MessagesViewState
|
||||||
|
import io.element.android.x.matrix.MatrixClient
|
||||||
|
import io.element.android.x.matrix.MatrixInstance
|
||||||
|
import io.element.android.x.matrix.room.RoomSummary
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.matrix.rustcomponents.sdk.mediaSourceFromUrl
|
||||||
|
|
||||||
|
class MessagesViewModel(initialState: MessagesViewState) :
|
||||||
|
MavericksViewModel<MessagesViewState>(initialState) {
|
||||||
|
|
||||||
|
private val matrix = MatrixInstance.getInstance()
|
||||||
|
|
||||||
|
init {
|
||||||
|
handleInit()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleInit() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadAvatarData(
|
||||||
|
client: MatrixClient,
|
||||||
|
name: String,
|
||||||
|
url: String?,
|
||||||
|
size: AvatarSize = AvatarSize.MEDIUM
|
||||||
|
): AvatarData {
|
||||||
|
val mediaContent = url?.let {
|
||||||
|
val mediaSource = mediaSourceFromUrl(it)
|
||||||
|
client.loadMediaThumbnailForSource(mediaSource, size.value.toLong(), size.value.toLong())
|
||||||
|
}
|
||||||
|
return mediaContent?.fold(
|
||||||
|
{ it },
|
||||||
|
{ null }
|
||||||
|
).let { model ->
|
||||||
|
AvatarData(name.first().uppercase(), model, size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getClient(): MatrixClient {
|
||||||
|
return matrix.matrixClient().first().get()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package io.element.android.x.features.messages.model
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.MavericksState
|
||||||
|
import io.element.android.x.designsystem.components.avatar.AvatarData
|
||||||
|
import io.element.android.x.matrix.core.RoomId
|
||||||
|
|
||||||
|
data class MessagesViewState(
|
||||||
|
val roomId: String,
|
||||||
|
val roomTitle: String = "",
|
||||||
|
val roomAvatar: AvatarData? = null
|
||||||
|
) : MavericksState {
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
constructor(roomId: String) : this(roomId = roomId, roomTitle = "", roomAvatar = null)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -14,11 +14,13 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import com.airbnb.mvrx.Loading
|
||||||
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.core.data.LogCompositions
|
import io.element.android.x.core.data.LogCompositions
|
||||||
import io.element.android.x.designsystem.ElementXTheme
|
import io.element.android.x.designsystem.ElementXTheme
|
||||||
|
import io.element.android.x.designsystem.components.ProgressDialog
|
||||||
import io.element.android.x.designsystem.components.avatar.AvatarData
|
import io.element.android.x.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.x.features.roomlist.components.RoomItem
|
import io.element.android.x.features.roomlist.components.RoomItem
|
||||||
import io.element.android.x.features.roomlist.components.RoomListTopBar
|
import io.element.android.x.features.roomlist.components.RoomListTopBar
|
||||||
|
|
@ -46,7 +48,8 @@ fun RoomListScreen(
|
||||||
roomSummaries = roomSummaries().orEmpty(),
|
roomSummaries = roomSummaries().orEmpty(),
|
||||||
matrixUser = matrixUser(),
|
matrixUser = matrixUser(),
|
||||||
onRoomClicked = onRoomClicked,
|
onRoomClicked = onRoomClicked,
|
||||||
onLogoutClicked = viewModel::logout
|
onLogoutClicked = viewModel::logout,
|
||||||
|
isLoginOut = logoutAction is Loading
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,6 +59,7 @@ fun RoomListContent(
|
||||||
matrixUser: MatrixUser?,
|
matrixUser: MatrixUser?,
|
||||||
onRoomClicked: (RoomId) -> Unit,
|
onRoomClicked: (RoomId) -> Unit,
|
||||||
onLogoutClicked: () -> Unit,
|
onLogoutClicked: () -> Unit,
|
||||||
|
isLoginOut: Boolean,
|
||||||
) {
|
) {
|
||||||
val appBarState = rememberTopAppBarState()
|
val appBarState = rememberTopAppBarState()
|
||||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(appBarState)
|
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(appBarState)
|
||||||
|
|
@ -75,6 +79,9 @@ fun RoomListContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
if (isLoginOut) {
|
||||||
|
ProgressDialog("Login out...")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -86,7 +93,8 @@ private fun PreviewableRoomListContent() {
|
||||||
roomSummaries = stubbedRoomSummaries(),
|
roomSummaries = stubbedRoomSummaries(),
|
||||||
matrixUser = MatrixUser("User#1", avatarData = AvatarData("U")),
|
matrixUser = MatrixUser("User#1", avatarData = AvatarData("U")),
|
||||||
onRoomClicked = {},
|
onRoomClicked = {},
|
||||||
onLogoutClicked = {}
|
onLogoutClicked = {},
|
||||||
|
isLoginOut = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -99,7 +107,8 @@ private fun PreviewableDarkRoomListContent() {
|
||||||
roomSummaries = stubbedRoomSummaries(),
|
roomSummaries = stubbedRoomSummaries(),
|
||||||
matrixUser = MatrixUser("User#1", avatarData = AvatarData("U")),
|
matrixUser = MatrixUser("User#1", avatarData = AvatarData("U")),
|
||||||
onRoomClicked = {},
|
onRoomClicked = {},
|
||||||
onLogoutClicked = {}
|
onLogoutClicked = {},
|
||||||
|
isLoginOut = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
package io.element.android.x.features.roomlist
|
package io.element.android.x.features.roomlist
|
||||||
|
|
||||||
import com.airbnb.mvrx.Fail
|
|
||||||
import com.airbnb.mvrx.Loading
|
|
||||||
import com.airbnb.mvrx.MavericksViewModel
|
import com.airbnb.mvrx.MavericksViewModel
|
||||||
import com.airbnb.mvrx.Success
|
|
||||||
import io.element.android.x.core.data.parallelMap
|
import io.element.android.x.core.data.parallelMap
|
||||||
import io.element.android.x.designsystem.components.avatar.AvatarData
|
import io.element.android.x.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.x.designsystem.components.avatar.AvatarSize
|
import io.element.android.x.designsystem.components.avatar.AvatarSize
|
||||||
|
|
@ -14,6 +11,8 @@ import io.element.android.x.matrix.MatrixClient
|
||||||
import io.element.android.x.matrix.MatrixInstance
|
import io.element.android.x.matrix.MatrixInstance
|
||||||
import io.element.android.x.matrix.room.RoomSummary
|
import io.element.android.x.matrix.room.RoomSummary
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -47,7 +46,12 @@ class RoomListViewModel(initialState: RoomListViewState) :
|
||||||
val userAvatarUrl = client.loadUserAvatarURLString().getOrNull()
|
val userAvatarUrl = client.loadUserAvatarURLString().getOrNull()
|
||||||
val userDisplayName = client.loadUserDisplayName().getOrNull()
|
val userDisplayName = client.loadUserDisplayName().getOrNull()
|
||||||
val avatarData =
|
val avatarData =
|
||||||
loadAvatarData(client, userDisplayName ?: client.userId().value, userAvatarUrl, AvatarSize.SMALL)
|
loadAvatarData(
|
||||||
|
client,
|
||||||
|
userDisplayName ?: client.userId().value,
|
||||||
|
userAvatarUrl,
|
||||||
|
AvatarSize.SMALL
|
||||||
|
)
|
||||||
MatrixUser(
|
MatrixUser(
|
||||||
username = userDisplayName ?: client.userId().value,
|
username = userDisplayName ?: client.userId().value,
|
||||||
avatarUrl = userAvatarUrl,
|
avatarUrl = userAvatarUrl,
|
||||||
|
|
@ -101,7 +105,11 @@ class RoomListViewModel(initialState: RoomListViewState) :
|
||||||
): AvatarData {
|
): AvatarData {
|
||||||
val mediaContent = url?.let {
|
val mediaContent = url?.let {
|
||||||
val mediaSource = mediaSourceFromUrl(it)
|
val mediaSource = mediaSourceFromUrl(it)
|
||||||
client.loadMediaThumbnailForSource(mediaSource, size.value.toLong(), size.value.toLong())
|
client.loadMediaThumbnailForSource(
|
||||||
|
mediaSource,
|
||||||
|
size.value.toLong(),
|
||||||
|
size.value.toLong()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return mediaContent?.fold(
|
return mediaContent?.fold(
|
||||||
{ it },
|
{ it },
|
||||||
|
|
@ -113,18 +121,17 @@ class RoomListViewModel(initialState: RoomListViewState) :
|
||||||
|
|
||||||
private fun handleLogout() {
|
private fun handleLogout() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
setState { copy(logoutAction = Loading()) }
|
suspend {
|
||||||
try {
|
delay(2000)
|
||||||
getClient().logout()
|
getClient().logout()
|
||||||
setState { copy(logoutAction = Success(Unit)) }
|
}.execute {
|
||||||
} catch (throwable: Throwable) {
|
copy(logoutAction = it)
|
||||||
setState { copy(logoutAction = Fail(throwable)) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getClient(): MatrixClient {
|
private suspend fun getClient(): MatrixClient {
|
||||||
return matrix.restoreSession()!!
|
return matrix.matrixClient().first().get()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,12 @@
|
||||||
package io.element.android.x.designsystem
|
package io.element.android.x.designsystem
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.SideEffect
|
import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.toArgb
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalView
|
|
||||||
import androidx.core.view.WindowCompat
|
|
||||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
|
|
||||||
private val DarkColorScheme = darkColorScheme(
|
private val DarkColorScheme = darkColorScheme(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package io.element.android.x.designsystem.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ProgressDialog(text: String? = null, onDismiss: () -> Unit = {}) {
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colorScheme.onBackground,
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.background)
|
||||||
|
if (!text.isNullOrBlank()) {
|
||||||
|
Text(text = text, Modifier.padding(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,15 +4,20 @@ import android.content.Context
|
||||||
import io.element.android.x.core.data.CoroutineDispatchers
|
import io.element.android.x.core.data.CoroutineDispatchers
|
||||||
import io.element.android.x.matrix.session.SessionStore
|
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.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.matrix.rustcomponents.sdk.AuthenticationService
|
import org.matrix.rustcomponents.sdk.AuthenticationService
|
||||||
|
import org.matrix.rustcomponents.sdk.Client
|
||||||
import org.matrix.rustcomponents.sdk.ClientBuilder
|
import org.matrix.rustcomponents.sdk.ClientBuilder
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.Optional
|
||||||
|
|
||||||
class Matrix(
|
class Matrix(
|
||||||
|
coroutineScope: CoroutineScope,
|
||||||
context: Context,
|
context: Context,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val coroutineDispatchers = CoroutineDispatchers(
|
private val coroutineDispatchers = CoroutineDispatchers(
|
||||||
io = Dispatchers.IO,
|
io = Dispatchers.IO,
|
||||||
computation = Dispatchers.Default,
|
computation = Dispatchers.Default,
|
||||||
|
|
@ -20,9 +25,31 @@ class Matrix(
|
||||||
)
|
)
|
||||||
private val baseFolder = File(context.filesDir, "matrix")
|
private val baseFolder = File(context.filesDir, "matrix")
|
||||||
private val sessionStore = SessionStore(context)
|
private val sessionStore = SessionStore(context)
|
||||||
|
private val matrixClient = MutableStateFlow<Optional<MatrixClient>>(Optional.empty())
|
||||||
|
private val isLoggedIn = MutableStateFlow(false)
|
||||||
|
|
||||||
suspend fun restoreSession(): MatrixClient? {
|
init {
|
||||||
return sessionStore.getStoredData()
|
sessionStore.isLoggedIn()
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.onEach { isLoggedIn ->
|
||||||
|
this.isLoggedIn.value = isLoggedIn
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
matrixClient.value = Optional.empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.launchIn(coroutineScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isLoggedIn(): Flow<Boolean> {
|
||||||
|
return isLoggedIn
|
||||||
|
}
|
||||||
|
|
||||||
|
fun matrixClient(): Flow<Optional<MatrixClient>> {
|
||||||
|
return matrixClient
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun restoreSession() = withContext(coroutineDispatchers.io) {
|
||||||
|
sessionStore.getStoredData()
|
||||||
?.let { sessionData ->
|
?.let { sessionData ->
|
||||||
try {
|
try {
|
||||||
ClientBuilder()
|
ClientBuilder()
|
||||||
|
|
@ -36,15 +63,26 @@ class Matrix(
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}?.let {
|
}?.let {
|
||||||
MatrixClient(it, sessionStore, coroutineDispatchers)
|
createMatrixClient(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun login(homeserver: String, username: String, password: String): MatrixClient {
|
suspend fun login(homeserver: String, username: String, password: String): MatrixClient =
|
||||||
val authService = AuthenticationService(baseFolder.absolutePath)
|
withContext(coroutineDispatchers.io) {
|
||||||
authService.configureHomeserver(homeserver)
|
val authService = AuthenticationService(baseFolder.absolutePath)
|
||||||
val client = authService.login(username, password, "MatrixRustSDKSample", null)
|
authService.configureHomeserver(homeserver)
|
||||||
sessionStore.storeData(SessionStore.SessionData(client.userId(), client.restoreToken()))
|
val client = authService.login(username, password, "MatrixRustSDKSample", null)
|
||||||
return MatrixClient(client, sessionStore, coroutineDispatchers)
|
sessionStore.storeData(SessionStore.SessionData(client.userId(), client.restoreToken()))
|
||||||
|
createMatrixClient(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createMatrixClient(client: Client): MatrixClient {
|
||||||
|
return MatrixClient(
|
||||||
|
client = client,
|
||||||
|
sessionStore = sessionStore,
|
||||||
|
dispatchers = coroutineDispatchers
|
||||||
|
).also {
|
||||||
|
matrixClient.value = Optional.of(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -61,7 +61,6 @@ class MatrixClient internal constructor(
|
||||||
init {
|
init {
|
||||||
client.setDelegate(clientDelegate)
|
client.setDelegate(clientDelegate)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startSync() {
|
fun startSync() {
|
||||||
slidingSync.setObserver(slidingSyncObserver)
|
slidingSync.setObserver(slidingSyncObserver)
|
||||||
slidingSyncObserverToken = slidingSync.sync()
|
slidingSyncObserverToken = slidingSync.sync()
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package io.element.android.x.matrix
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
|
||||||
|
|
||||||
object MatrixInstance {
|
object MatrixInstance {
|
||||||
|
|
@ -9,7 +10,7 @@ object MatrixInstance {
|
||||||
private lateinit var instance: Matrix
|
private lateinit var instance: Matrix
|
||||||
|
|
||||||
fun init(context: Context) {
|
fun init(context: Context) {
|
||||||
instance = Matrix(context)
|
instance = Matrix(GlobalScope, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getInstance(): Matrix {
|
fun getInstance(): Matrix {
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,13 @@ import androidx.datastore.preferences.core.Preferences
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
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")
|
||||||
|
|
@ -25,6 +28,11 @@ internal class SessionStore(
|
||||||
|
|
||||||
private val store = context.dataStore
|
private val store = context.dataStore
|
||||||
|
|
||||||
|
fun isLoggedIn(): Flow<Boolean> {
|
||||||
|
return store.data.map { prefs ->
|
||||||
|
prefs[userIdPreference] != null && prefs[restoreTokenPreference] != null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun storeData(sessionData: SessionData) {
|
suspend fun storeData(sessionData: SessionData) {
|
||||||
store.edit { prefs ->
|
store.edit { prefs ->
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue