Merge pull request #794 from vector-im/feature/fga/waiting_ss_room

Feature/fga/waiting ss room
This commit is contained in:
ganfra 2023-07-07 12:08:19 +02:00 committed by GitHub
commit f32a0ca1ad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 592 additions and 145 deletions

View file

@ -28,7 +28,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.core.plugin.Plugin
import io.element.android.appnav.LoggedInFlowNode
import io.element.android.appnav.RoomFlowNode
import io.element.android.appnav.room.RoomLoadedFlowNode
import io.element.android.appnav.RootFlowNode
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.architecture.createNode
@ -67,7 +67,7 @@ class MainNode(
}
}
private val roomFlowNodeCallback = object : RoomFlowNode.LifecycleCallback {
private val roomFlowNodeCallback = object : RoomLoadedFlowNode.LifecycleCallback {
override fun onFlowCreated(identifier: String, room: MatrixRoom) {
val component = bindings<RoomComponent.ParentBindings>().roomComponentBuilder().room(room).build()
mainDaggerComponentOwner.addComponent(identifier, component)

View file

@ -42,6 +42,8 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.appnav.loggedin.LoggedInNode
import io.element.android.appnav.room.RoomFlowNode
import io.element.android.appnav.room.RoomLoadedFlowNode
import io.element.android.features.analytics.api.AnalyticsEntryPoint
import io.element.android.features.createroom.api.CreateRoomEntryPoint
import io.element.android.features.invitelist.api.InviteListEntryPoint
@ -56,7 +58,6 @@ import io.element.android.libraries.architecture.animation.rememberDefaultTransi
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.MatrixClient
@ -138,6 +139,7 @@ class LoggedInFlowNode @AssistedInject constructor(
observeAnalyticsState()
lifecycle.subscribe(
onCreate = {
syncService.startSync()
plugins<LifecycleCallback>().forEach { it.onFlowCreated(id, inputs.matrixClient) }
val imageLoaderFactory = bindings<MatrixUIBindings>().loggedInImageLoaderFactory()
Coil.setImageLoader(imageLoaderFactory)
@ -194,7 +196,7 @@ class LoggedInFlowNode @AssistedInject constructor(
@Parcelize
data class Room(
val roomId: RoomId,
val initialElement: RoomFlowNode.NavTarget = RoomFlowNode.NavTarget.Messages
val initialElement: RoomLoadedFlowNode.NavTarget = RoomLoadedFlowNode.NavTarget.Messages
) : NavTarget
@Parcelize
@ -241,7 +243,7 @@ class LoggedInFlowNode @AssistedInject constructor(
}
override fun onRoomSettingsClicked(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId, initialElement = RoomFlowNode.NavTarget.RoomDetails))
backstack.push(NavTarget.Room(roomId, initialElement = RoomLoadedFlowNode.NavTarget.RoomDetails))
}
override fun onReportBugClicked() {
@ -254,24 +256,14 @@ class LoggedInFlowNode @AssistedInject constructor(
.build()
}
is NavTarget.Room -> {
val room = inputs.matrixClient.getRoom(roomId = navTarget.roomId)
if (room == null) {
// TODO CREATE UNKNOWN ROOM NODE
node(buildContext) {
Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(text = "Unknown room with id = ${navTarget.roomId}")
}
val nodeLifecycleCallbacks = plugins<NodeLifecycleCallback>()
val callback = object : RoomLoadedFlowNode.Callback {
override fun onForwardedToSingleRoom(roomId: RoomId) {
coroutineScope.launch { attachRoom(roomId) }
}
} else {
val nodeLifecycleCallbacks = plugins<NodeLifecycleCallback>()
val callback = object : RoomFlowNode.Callback {
override fun onForwardedToSingleRoom(roomId: RoomId) {
coroutineScope.launch { attachRoom(roomId) }
}
}
val inputs = RoomFlowNode.Inputs(room, initialElement = navTarget.initialElement)
createNode<RoomFlowNode>(buildContext, plugins = listOf(inputs, callback) + nodeLifecycleCallbacks)
}
val inputs = RoomFlowNode.Inputs(roomId = navTarget.roomId, initialElement = navTarget.initialElement)
createNode<RoomFlowNode>(buildContext, plugins = listOf(inputs, callback) + nodeLifecycleCallbacks)
}
NavTarget.Settings -> {
val callback = object : PreferencesEntryPoint.Callback {

View file

@ -0,0 +1,135 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.appnav.room
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.theme.placeholderBackground
import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun LoadingRoomNodeView(
state: LoadingRoomState,
hasNetworkConnection: Boolean,
onBackClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
Scaffold(
modifier = modifier,
contentWindowInsets = WindowInsets.systemBars,
topBar = {
Column {
ConnectivityIndicatorView(isOnline = hasNetworkConnection)
LoadingRoomTopBar(onBackClicked)
}
},
content = { padding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(16.dp), contentAlignment = Alignment.Center
) {
if (state is LoadingRoomState.Error) {
Text(
text = stringResource(id = CommonStrings.error_unknown),
color = ElementTheme.colors.textSecondary,
fontSize = 14.sp,
)
} else {
CircularProgressIndicator()
}
}
},
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun LoadingRoomTopBar(onBackClicked: () -> Unit) {
TopAppBar(
modifier = Modifier,
navigationIcon = {
BackButton(onClick = onBackClicked)
},
title = {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(AvatarSize.TimelineRoom.dp)
.align(Alignment.CenterVertically)
.background(color = ElementTheme.colors.placeholderBackground, shape = CircleShape)
)
Spacer(modifier = Modifier.width(8.dp))
PlaceholderAtom(width = 20.dp, height = 7.dp)
Spacer(modifier = Modifier.width(7.dp))
PlaceholderAtom(width = 45.dp, height = 7.dp)
}
},
)
}
@Preview
@Composable
fun LoadingRoomNodeViewLightPreview(@PreviewParameter(LoadingRoomStateProvider::class) state: LoadingRoomState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
fun LoadingRoomNodeViewDarkPreview(@PreviewParameter(LoadingRoomStateProvider::class) state: LoadingRoomState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
private fun ContentToPreview(state: LoadingRoomState) {
LoadingRoomNodeView(
state = state,
onBackClicked = {},
hasNetworkConnection = false
)
}

View file

@ -0,0 +1,66 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.appnav.room
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject
sealed interface LoadingRoomState {
object Loading : LoadingRoomState
object Error : LoadingRoomState
data class Loaded(val room: MatrixRoom) : LoadingRoomState
}
open class LoadingRoomStateProvider : PreviewParameterProvider<LoadingRoomState> {
override val values: Sequence<LoadingRoomState>
get() = sequenceOf(
LoadingRoomState.Loading,
LoadingRoomState.Error
)
}
@SingleIn(SessionScope::class)
class LoadingRoomStateFlowFactory @Inject constructor(private val matrixClient: MatrixClient) {
fun create(lifecycleScope: CoroutineScope, roomId: RoomId): StateFlow<LoadingRoomState> =
getRoomFlow(roomId)
.map { room ->
if (room != null) {
LoadingRoomState.Loaded(room)
} else {
LoadingRoomState.Error
}
}
.stateIn(lifecycleScope, SharingStarted.Eagerly, LoadingRoomState.Loading)
private fun getRoomFlow(roomId: RoomId): Flow<MatrixRoom?> = suspend {
matrixClient.getRoom(roomId = roomId)
}
.asFlow()
}

View file

@ -0,0 +1,140 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.appnav.room
import android.os.Parcelable
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.appnav.NodeLifecycleCallback
import io.element.android.appnav.safeRoot
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
class RoomFlowNode @AssistedInject constructor(
@Assisted val buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
loadingRoomStateFlowFactory: LoadingRoomStateFlowFactory,
private val networkMonitor: NetworkMonitor,
) :
BackstackNode<RoomFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Loading,
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
plugins = plugins
) {
data class Inputs(
val roomId: RoomId,
val initialElement: RoomLoadedFlowNode.NavTarget = RoomLoadedFlowNode.NavTarget.Messages,
) : NodeInputs
private val inputs: Inputs = inputs()
private val loadingRoomStateStateFlow = loadingRoomStateFlowFactory.create(lifecycleScope, inputs.roomId)
sealed interface NavTarget : Parcelable {
@Parcelize
object Loading : NavTarget
@Parcelize
object Loaded : NavTarget
}
override fun onBuilt() {
super.onBuilt()
loadingRoomStateStateFlow
.map {
it is LoadingRoomState.Loaded
}
.distinctUntilChanged()
.onEach { isLoaded ->
if (isLoaded) {
backstack.safeRoot(NavTarget.Loaded)
} else {
backstack.safeRoot(NavTarget.Loading)
}
}.launchIn(lifecycleScope)
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
NavTarget.Loaded -> {
val nodeLifecycleCallbacks = plugins<NodeLifecycleCallback>()
val roomFlowNodeCallback = plugins<RoomLoadedFlowNode.Callback>()
val awaitRoomState = loadingRoomStateStateFlow.value
if (awaitRoomState is LoadingRoomState.Loaded) {
val inputs = RoomLoadedFlowNode.Inputs(awaitRoomState.room, initialElement = inputs.initialElement)
createNode<RoomLoadedFlowNode>(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback + nodeLifecycleCallbacks)
} else {
loadingNode(buildContext, this::navigateUp)
}
}
NavTarget.Loading -> {
loadingNode(buildContext, this::navigateUp)
}
}
}
private fun loadingNode(buildContext: BuildContext, onBackClicked: () -> Unit) = node(buildContext) { modifier ->
val loadingRoomState by loadingRoomStateStateFlow.collectAsState()
val networkStatus by networkMonitor.connectivity.collectAsState()
LoadingRoomNodeView(
state = loadingRoomState,
hasNetworkConnection = networkStatus == NetworkStatus.Online,
modifier = modifier,
onBackClicked = onBackClicked
)
}
@Composable
override fun View(modifier: Modifier) {
Children(
navModel = backstack,
modifier = modifier,
)
}
}

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.appnav
package io.element.android.appnav.room
import android.os.Parcelable
import androidx.compose.runtime.Composable
@ -32,6 +32,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.appnav.NodeLifecycleCallback
import io.element.android.features.messages.api.MessagesEntryPoint
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
import io.element.android.libraries.architecture.BackstackNode
@ -52,14 +53,14 @@ import kotlinx.parcelize.Parcelize
import timber.log.Timber
@ContributesNode(SessionScope::class)
class RoomFlowNode @AssistedInject constructor(
class RoomLoadedFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val messagesEntryPoint: MessagesEntryPoint,
private val roomDetailsEntryPoint: RoomDetailsEntryPoint,
private val appNavigationStateService: AppNavigationStateService,
roomMembershipObserver: RoomMembershipObserver,
) : BackstackNode<RoomFlowNode.NavTarget>(
) : BackstackNode<RoomLoadedFlowNode.NavTarget>(
backstack = BackStack(
initialElement = plugins.filterIsInstance(Inputs::class.java).first().initialElement,
savedStateMap = buildContext.savedStateMap,

View file

@ -26,6 +26,7 @@ import com.bumble.appyx.navmodel.backstack.activeElement
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper
import com.google.common.truth.Truth
import io.element.android.appnav.room.RoomLoadedFlowNode
import io.element.android.features.messages.api.MessagesEntryPoint
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
import io.element.android.libraries.architecture.childNode
@ -76,7 +77,7 @@ class RoomFlowNodeTest {
plugins: List<Plugin>,
messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(),
roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(),
) = RoomFlowNode(
) = RoomLoadedFlowNode(
buildContext = BuildContext.root(savedStateMap = null),
plugins = plugins,
messagesEntryPoint = messagesEntryPoint,
@ -90,15 +91,15 @@ class RoomFlowNodeTest {
// GIVEN
val room = FakeMatrixRoom()
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
val inputs = RoomFlowNode.Inputs(room)
val inputs = RoomLoadedFlowNode.Inputs(room)
val roomFlowNode = aRoomFlowNode(listOf(inputs), fakeMessagesEntryPoint)
// WHEN
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
// THEN
Truth.assertThat(roomFlowNode.backstack.activeElement).isEqualTo(RoomFlowNode.NavTarget.Messages)
roomFlowNodeTestHelper.assertChildHasLifecycle(RoomFlowNode.NavTarget.Messages, Lifecycle.State.CREATED)
val messagesNode = roomFlowNode.childNode(RoomFlowNode.NavTarget.Messages)!!
Truth.assertThat(roomFlowNode.backstack.activeElement).isEqualTo(RoomLoadedFlowNode.NavTarget.Messages)
roomFlowNodeTestHelper.assertChildHasLifecycle(RoomLoadedFlowNode.NavTarget.Messages, Lifecycle.State.CREATED)
val messagesNode = roomFlowNode.childNode(RoomLoadedFlowNode.NavTarget.Messages)!!
Truth.assertThat(messagesNode.id).isEqualTo(fakeMessagesEntryPoint.nodeId)
}
@ -108,14 +109,14 @@ class RoomFlowNodeTest {
val room = FakeMatrixRoom()
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
val inputs = RoomFlowNode.Inputs(room)
val inputs = RoomLoadedFlowNode.Inputs(room)
val roomFlowNode = aRoomFlowNode(listOf(inputs), fakeMessagesEntryPoint, fakeRoomDetailsEntryPoint)
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
// WHEN
fakeMessagesEntryPoint.callback?.onRoomDetailsClicked()
// THEN
roomFlowNodeTestHelper.assertChildHasLifecycle(RoomFlowNode.NavTarget.RoomDetails, Lifecycle.State.CREATED)
val roomDetailsNode = roomFlowNode.childNode(RoomFlowNode.NavTarget.RoomDetails)!!
roomFlowNodeTestHelper.assertChildHasLifecycle(RoomLoadedFlowNode.NavTarget.RoomDetails, Lifecycle.State.CREATED)
val roomDetailsNode = roomFlowNode.childNode(RoomLoadedFlowNode.NavTarget.RoomDetails)!!
Truth.assertThat(roomDetailsNode.id).isEqualTo(fakeRoomDetailsEntryPoint.nodeId)
}
}

View file

@ -0,0 +1,79 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.appnav.room
import app.cash.turbine.test
import com.google.common.truth.Truth
import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
import kotlinx.coroutines.test.runTest
import org.junit.Test
class LoadingRoomStateFlowFactoryTest {
@Test
fun `flow should emit Loading and then Loaded when there is a room in cache`() = runTest {
val room = FakeMatrixRoom(sessionId= A_SESSION_ID, roomId = A_ROOM_ID)
val matrixClient = FakeMatrixClient(A_SESSION_ID).apply {
givenGetRoomResult(A_ROOM_ID, room)
}
val flowFactory = LoadingRoomStateFlowFactory(matrixClient)
flowFactory
.create(this, A_ROOM_ID)
.test {
Truth.assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading)
Truth.assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loaded(room))
}
}
@Test
fun `flow should emit Loading and then Loaded when there is a room in cache after SS is loaded`() = runTest {
val room = FakeMatrixRoom(sessionId= A_SESSION_ID, roomId = A_ROOM_ID)
val roomSummaryDataSource = FakeRoomSummaryDataSource()
val matrixClient = FakeMatrixClient(A_SESSION_ID, roomSummaryDataSource = roomSummaryDataSource)
val flowFactory = LoadingRoomStateFlowFactory(matrixClient)
flowFactory
.create(this, A_ROOM_ID)
.test {
Truth.assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading)
matrixClient.givenGetRoomResult(A_ROOM_ID, room)
roomSummaryDataSource.postLoadingState(RoomSummaryDataSource.LoadingState.Loaded(1))
Truth.assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loaded(room))
}
}
@Test
fun `flow should emit Loading and then Error when there is no room in cache after SS is loaded`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource()
val matrixClient = FakeMatrixClient(A_SESSION_ID, roomSummaryDataSource = roomSummaryDataSource)
val flowFactory = LoadingRoomStateFlowFactory(matrixClient)
flowFactory
.create(this, A_ROOM_ID)
.test {
Truth.assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading)
roomSummaryDataSource.postLoadingState(RoomSummaryDataSource.LoadingState.Loaded(1))
Truth.assertThat(awaitItem()).isEqualTo(LoadingRoomState.Error)
}
}
}

View file

@ -65,20 +65,9 @@ class CreateRoomRootPresenter @Inject constructor(
val localCoroutineScope = rememberCoroutineScope()
val startDmAction: MutableState<Async<RoomId>> = remember { mutableStateOf(Async.Uninitialized) }
fun startDm(matrixUser: MatrixUser) {
startDmAction.value = Async.Uninitialized
matrixClient.findDM(matrixUser.userId).use { existingDM ->
if (existingDM == null) {
localCoroutineScope.createDM(matrixUser, startDmAction)
} else {
startDmAction.value = Async.Success(existingDM.roomId)
}
}
}
fun handleEvents(event: CreateRoomRootEvents) {
when (event) {
is CreateRoomRootEvents.StartDM -> startDm(event.matrixUser)
is CreateRoomRootEvents.StartDM -> localCoroutineScope.startDm(event.matrixUser, startDmAction)
CreateRoomRootEvents.CancelStartDM -> startDmAction.value = Async.Uninitialized
}
}
@ -91,10 +80,20 @@ class CreateRoomRootPresenter @Inject constructor(
)
}
private fun CoroutineScope.createDM(user: MatrixUser, startDmAction: MutableState<Async<RoomId>>) = launch {
private fun CoroutineScope.startDm(matrixUser: MatrixUser, startDmAction: MutableState<Async<RoomId>>) = launch {
suspend {
matrixClient.createDM(user.userId).getOrThrow()
.also { analyticsService.capture(CreatedRoom(isDM = true)) }
matrixClient.findDM(matrixUser.userId).use { existingDM ->
existingDM?.roomId ?: createDM(matrixUser)
}
}.runCatchingUpdatingState(startDmAction)
}
private suspend fun createDM(user: MatrixUser): RoomId {
return matrixClient
.createDM(user.userId)
.onSuccess {
analyticsService.capture(CreatedRoom(isDM = true))
}
.getOrThrow()
}
}

View file

@ -176,7 +176,6 @@ class CreateRoomRootPresenterTests {
// Retry with success
fakeMatrixClient.givenCreateDmError(null)
stateAfterSecondAttempt.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
assertThat(awaitItem().startDmAction).isInstanceOf(Async.Uninitialized::class.java)
assertThat(awaitItem().startDmAction).isInstanceOf(Async.Loading::class.java)
val stateAfterRetryStartDM = awaitItem()
assertThat(stateAfterRetryStartDM.startDmAction).isInstanceOf(Async.Success::class.java)

View file

@ -78,7 +78,7 @@ class LeaveRoomPresenterImpl @Inject constructor(
}
}
private fun showLeaveRoomAlert(
private suspend fun showLeaveRoomAlert(
matrixClient: MatrixClient,
roomId: RoomId,
confirmation: MutableState<LeaveRoomState.Confirmation>,
@ -105,7 +105,7 @@ private suspend fun MatrixClient.leaveRoom(
room.leave().onSuccess {
roomMembershipObserver.notifyUserLeftRoom(room.roomId)
}.onFailure {
Timber.e(it, "Error while leaving room ${room.name} - ${room.roomId}")
Timber.e(it, "Error while leaving room ${room.displayName} - ${room.roomId}")
error.value = LeaveRoomState.Error.Shown
}
}

View file

@ -24,6 +24,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
@ -107,13 +108,12 @@ class MessagesPresenter @AssistedInject constructor(
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
val userHasPermissionToSendMessage by room.canSendEventAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value)
val roomName: MutableState<String?> = rememberSaveable {
mutableStateOf(null)
val roomName by produceState(initialValue = room.displayName, key1 = syncUpdateFlow.value){
value = room.displayName
}
val roomAvatar: MutableState<AvatarData?> = remember {
mutableStateOf(null)
val roomAvatar by produceState(initialValue = room.avatarData(), key1 = syncUpdateFlow.value){
value = room.avatarData()
}
var hasDismissedInviteDialog by rememberSaveable {
mutableStateOf(false)
}
@ -134,16 +134,6 @@ class MessagesPresenter @AssistedInject constructor(
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
LaunchedEffect(syncUpdateFlow.value) {
roomAvatar.value =
AvatarData(
id = room.roomId.value,
name = room.name,
url = room.avatarUrl,
size = AvatarSize.TimelineRoom
)
roomName.value = room.name
}
LaunchedEffect(composerState.mode.relatedEventId) {
timelineState.eventSink(TimelineEvents.SetHighlightedEvent(composerState.mode.relatedEventId))
}
@ -169,8 +159,8 @@ class MessagesPresenter @AssistedInject constructor(
return MessagesState(
roomId = room.roomId,
roomName = roomName.value,
roomAvatar = roomAvatar.value,
roomName = roomName,
roomAvatar = roomAvatar,
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
composerState = composerState,
timelineState = timelineState,
@ -185,6 +175,15 @@ class MessagesPresenter @AssistedInject constructor(
)
}
private fun MatrixRoom.avatarData(): AvatarData {
return AvatarData(
id = roomId.value,
name = displayName,
url = avatarUrl,
size = AvatarSize.TimelineRoom
)
}
private fun CoroutineScope.handleTimelineAction(
action: TimelineItemAction,
targetEvent: TimelineItem.Event,

View file

@ -30,8 +30,8 @@ import io.element.android.libraries.matrix.api.core.RoomId
@Immutable
data class MessagesState(
val roomId: RoomId,
val roomName: String?,
val roomAvatar: AvatarData?,
val roomName: String,
val roomAvatar: AvatarData,
val userHasPermissionToSendMessage: Boolean,
val composerState: MessageComposerState,
val timelineState: TimelineState,

View file

@ -288,8 +288,8 @@ fun MessagesViewContent(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MessagesViewTopBar(
roomTitle: String?,
roomAvatar: AvatarData?,
roomTitle: String,
roomAvatar: AvatarData,
modifier: Modifier = Modifier,
onRoomDetailsClicked: () -> Unit = {},
onBackPressed: () -> Unit = {},
@ -304,14 +304,12 @@ fun MessagesViewTopBar(
modifier = Modifier.clickable { onRoomDetailsClicked() },
verticalAlignment = Alignment.CenterVertically
) {
if (roomAvatar != null) {
Avatar(roomAvatar)
Spacer(modifier = Modifier.width(8.dp))
}
Avatar(roomAvatar)
Spacer(modifier = Modifier.width(8.dp))
Text(
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
text = roomTitle ?: "Unknown room",
text = roomTitle,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)

View file

@ -20,6 +20,7 @@ import android.net.Uri
import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.collect.Iterables.skip
import com.google.common.truth.Truth.assertThat
import io.element.android.features.analytics.test.FakeAnalyticsService
import io.element.android.features.messages.fixtures.aMessageEvent
@ -82,7 +83,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.roomId).isEqualTo(A_ROOM_ID)
}
@ -96,7 +97,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID))
assertThat(room.myReactions.count()).isEqualTo(1)
@ -117,7 +118,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID))
assertThat(room.myReactions.count()).isEqualTo(1)
@ -134,7 +135,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Forward, aMessageEvent()))
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
@ -150,7 +151,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Copy, event))
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
@ -164,10 +165,10 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent()))
skipItems(1)
val finalState = awaitItem()
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java)
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
@ -180,7 +181,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent(eventId = null)))
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
@ -195,7 +196,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
val mediaMessage = aMessageEvent(
content = TimelineItemImageContent(
@ -212,7 +213,7 @@ class MessagesPresenterTest {
)
)
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, mediaMessage))
skipItems(1)
val finalState = awaitItem()
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java)
val replyMode = finalState.composerState.mode as MessageComposerMode.Reply
@ -227,7 +228,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
val mediaMessage = aMessageEvent(
content = TimelineItemVideoContent(
@ -245,7 +246,7 @@ class MessagesPresenterTest {
)
)
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, mediaMessage))
skipItems(1)
val finalState = awaitItem()
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java)
val replyMode = finalState.composerState.mode as MessageComposerMode.Reply
@ -260,7 +261,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
val mediaMessage = aMessageEvent(
content = TimelineItemFileContent(
@ -273,7 +274,7 @@ class MessagesPresenterTest {
)
)
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, mediaMessage))
skipItems(1)
val finalState = awaitItem()
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java)
val replyMode = finalState.composerState.mode as MessageComposerMode.Reply
@ -288,10 +289,10 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent()))
skipItems(1)
val finalState = awaitItem()
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Edit::class.java)
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
@ -306,7 +307,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Redact, aMessageEvent()))
assertThat(matrixRoom.redactEventEventIdParam).isEqualTo(AN_EVENT_ID)
@ -321,7 +322,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.ReportContent, aMessageEvent()))
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
@ -335,7 +336,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.Dismiss)
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
@ -349,7 +350,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Developer, aMessageEvent()))
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
@ -364,7 +365,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(3)
val initialState = awaitItem()
// Initially the composer doesn't have focus, so we don't show the alert
@ -389,7 +390,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(3)
val initialState = awaitItem()
assertThat(initialState.showReinvitePrompt).isFalse()
@ -406,7 +407,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(3)
val initialState = awaitItem()
assertThat(initialState.showReinvitePrompt).isFalse()
@ -432,9 +433,10 @@ class MessagesPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
skipItems(1)
initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite))
skipItems(3)
skipItems(1)
val loadingState = awaitItem()
assertThat(loadingState.inviteProgress.isLoading()).isTrue()
@ -461,9 +463,10 @@ class MessagesPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
skipItems(1)
initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite))
skipItems(3)
skipItems(1)
val loadingState = awaitItem()
assertThat(loadingState.inviteProgress.isLoading()).isTrue()
@ -482,9 +485,10 @@ class MessagesPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
skipItems(1)
initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite))
skipItems(3)
skipItems(1)
val loadingState = awaitItem()
assertThat(loadingState.inviteProgress.isLoading()).isTrue()
@ -510,9 +514,9 @@ class MessagesPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
skipItems(1)
initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite))
skipItems(3)
skipItems(1)
val loadingState = awaitItem()
assertThat(loadingState.inviteProgress.isLoading()).isTrue()
@ -529,7 +533,7 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(1)
assertThat(awaitItem().userHasPermissionToSendMessage).isTrue()
}
}
@ -542,6 +546,8 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
// Default value
assertThat(awaitItem().userHasPermissionToSendMessage).isTrue()
skipItems(1)
assertThat(awaitItem().userHasPermissionToSendMessage).isFalse()
}

View file

@ -78,7 +78,7 @@ class RoomDetailsPresenter @Inject constructor(
return RoomDetailsState(
roomId = room.roomId.value,
roomName = room.name ?: room.displayName,
roomName = room.displayName,
roomAlias = room.alias,
roomAvatarUrl = room.avatarUrl,
roomTopic = topicState,

View file

@ -62,7 +62,7 @@ class RoomDetailsPresenterTests {
}.test {
val initialState = awaitItem()
assertThat(initialState.roomId).isEqualTo(room.roomId.value)
assertThat(initialState.roomName).isEqualTo(room.name)
assertThat(initialState.roomName).isEqualTo(room.displayName)
assertThat(initialState.roomAvatarUrl).isEqualTo(room.avatarUrl)
assertThat(initialState.roomTopic).isEqualTo(RoomTopicState.ExistingTopic(room.topic!!))
assertThat(initialState.memberCount).isEqualTo(room.joinedMemberCount)

View file

@ -172,7 +172,7 @@ class RoomListPresenter @Inject constructor(
// Safe to give bigger size than room list
val extendedRangeEnd = range.last + midExtendedRangeSize
val extendedRange = IntRange(extendedRangeStart, extendedRangeEnd)
client.roomSummaryDataSource.updateRoomListVisibleRange(extendedRange)
client.roomSummaryDataSource.updateAllRoomsVisibleRange(extendedRange)
}
private suspend fun mapRoomSummaries(

View file

@ -36,7 +36,7 @@ import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.roomListPlaceholder
import io.element.android.libraries.designsystem.theme.placeholderBackground
import io.element.android.libraries.theme.ElementTheme
/**
@ -56,7 +56,7 @@ internal fun RoomSummaryPlaceholderRow(
modifier = Modifier
.size(AvatarSize.RoomListItem.dp)
.align(Alignment.CenterVertically)
.background(color = ElementTheme.colors.roomListPlaceholder, shape = CircleShape)
.background(color = ElementTheme.colors.placeholderBackground, shape = CircleShape)
)
Column(
modifier = Modifier

View file

@ -29,7 +29,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.roomListPlaceholder
import io.element.android.libraries.designsystem.theme.placeholderBackground
import io.element.android.libraries.theme.ElementTheme
@Composable
@ -37,7 +37,7 @@ fun PlaceholderAtom(
width: Dp,
height: Dp,
modifier: Modifier = Modifier,
color: Color = ElementTheme.colors.roomListPlaceholder,
color: Color = ElementTheme.colors.placeholderBackground,
) {
Box(
modifier = modifier

View file

@ -42,7 +42,7 @@ fun MaterialTheme.roomListRoomMessageDate() = colorScheme.secondary
val SemanticColors.unreadIndicator
get() = iconAccentTertiary
val SemanticColors.roomListPlaceholder
val SemanticColors.placeholderBackground
get() = bgSubtleSecondary
// This color is not present in Semantic color, so put hard-coded value for now
@ -83,7 +83,7 @@ private fun ContentToPreview() {
"roomListRoomMessage" to MaterialTheme.roomListRoomMessage(),
"roomListRoomMessageDate" to MaterialTheme.roomListRoomMessageDate(),
"unreadIndicator" to ElementTheme.colors.unreadIndicator,
"roomListPlaceholder" to ElementTheme.colors.roomListPlaceholder,
"placeholderBackground" to ElementTheme.colors.placeholderBackground,
"messageFromMeBackground" to ElementTheme.colors.messageFromMeBackground,
"messageFromOtherBackground" to ElementTheme.colors.messageFromOtherBackground,
)

View file

@ -31,14 +31,16 @@ import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import kotlinx.coroutines.TimeoutCancellationException
import java.io.Closeable
import kotlin.time.Duration
interface MatrixClient : Closeable {
val sessionId: SessionId
val roomSummaryDataSource: RoomSummaryDataSource
val mediaLoader: MatrixMediaLoader
fun getRoom(roomId: RoomId): MatrixRoom?
fun findDM(userId: UserId): MatrixRoom?
suspend fun getRoom(roomId: RoomId): MatrixRoom?
suspend fun findDM(userId: UserId): MatrixRoom?
suspend fun ignoreUser(userId: UserId): Result<Unit>
suspend fun unignoreUser(userId: UserId): Result<Unit>
suspend fun createRoom(createRoomParams: CreateRoomParameters): Result<RoomId>

View file

@ -35,7 +35,6 @@ interface MatrixRoom : Closeable {
val sessionId: SessionId
val roomId: RoomId
val name: String?
val bestName: String
val displayName: String
val alias: String?
val alternativeAliases: List<String>

View file

@ -16,17 +16,34 @@
package io.element.android.libraries.matrix.api.room
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.withTimeout
import timber.log.Timber
import kotlin.time.Duration
interface RoomSummaryDataSource {
sealed class LoadingState {
object NotLoaded : LoadingState()
data class Loaded(val numberOfRooms: Int): LoadingState()
data class Loaded(val numberOfRooms: Int) : LoadingState()
}
fun updateAllRoomsVisibleRange(range: IntRange)
fun allRoomsLoadingState(): StateFlow<LoadingState>
fun allRooms(): StateFlow<List<RoomSummary>>
fun inviteRooms(): StateFlow<List<RoomSummary>>
fun updateRoomListVisibleRange(range: IntRange)
}
suspend fun RoomSummaryDataSource.awaitAllRoomsAreLoaded(timeout: Duration = Duration.INFINITE) {
try {
withTimeout(timeout) {
allRoomsLoadingState().firstOrNull {
it is RoomSummaryDataSource.LoadingState.Loaded
}
}
} catch (timeoutException: TimeoutCancellationException) {
Timber.v("AwaitAllRooms: no response after $timeout")
}
}

View file

@ -33,6 +33,7 @@ import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource
import io.element.android.libraries.matrix.api.room.awaitAllRoomsAreLoaded
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
@ -63,6 +64,8 @@ import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.ClientDelegate
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
import java.io.File
@ -91,7 +94,6 @@ class RustMatrixClient constructor(
)
private val notificationService = RustNotificationService(client)
private val clientDelegate = object : ClientDelegate {
override fun didReceiveAuthError(isSoftLogout: Boolean) {
//TODO handle this
@ -127,9 +129,16 @@ class RustMatrixClient constructor(
}.launchIn(sessionCoroutineScope)
}
override fun getRoom(roomId: RoomId): MatrixRoom? {
val roomListItem = roomListService.roomOrNull(roomId.value) ?: return null
val fullRoom = roomListItem.fullRoom()
override suspend fun getRoom(roomId: RoomId): MatrixRoom? {
// Check if already in memory...
var cachedPairOfRoom = pairOfRoom(roomId)
if (cachedPairOfRoom == null) {
//... otherwise, lets wait for the SS to load all rooms and check again.
roomSummaryDataSource.awaitAllRoomsAreLoaded()
cachedPairOfRoom = pairOfRoom(roomId)
}
if (cachedPairOfRoom == null) return null
val (roomListItem, fullRoom) = cachedPairOfRoom
return RustMatrixRoom(
sessionId = sessionId,
roomListItem = roomListItem,
@ -141,7 +150,17 @@ class RustMatrixClient constructor(
)
}
override fun findDM(userId: UserId): MatrixRoom? {
private suspend fun pairOfRoom(roomId: RoomId): Pair<RoomListItem, Room>? = withContext(dispatchers.io) {
val cachedRoomListItem = roomListService.roomOrNull(roomId.value)
val fullRoom = cachedRoomListItem?.fullRoom()
if (cachedRoomListItem == null || fullRoom == null) {
null
} else {
Pair(cachedRoomListItem, fullRoom)
}
}
override suspend fun findDM(userId: UserId): MatrixRoom? {
val roomId = client.getDmRoom(userId.value)?.use { RoomId(it.id()) }
return roomId?.let { getRoom(it) }
}

View file

@ -25,6 +25,7 @@ import org.matrix.rustcomponents.sdk.RoomList
import org.matrix.rustcomponents.sdk.RoomListEntriesListener
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
import org.matrix.rustcomponents.sdk.RoomListEntry
import org.matrix.rustcomponents.sdk.RoomListException
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.RoomListLoadingState
import org.matrix.rustcomponents.sdk.RoomListLoadingStateListener
@ -60,8 +61,8 @@ fun RoomList.entriesFlow(onInitialList: suspend (List<RoomListEntry>) -> Unit):
fun RoomListService.roomOrNull(roomId: String): RoomListItem? {
return try {
room(roomId)
} catch (failure: Throwable) {
Timber.e(failure, "Failed finding room with id=$roomId")
} catch (exception: RoomListException) {
Timber.e(exception, "Failed finding room with id=$roomId")
return null
}
}

View file

@ -132,11 +132,6 @@ class RustMatrixRoom(
return roomListItem.name()
}
override val bestName: String
get() {
return name?.takeIf { it.isNotEmpty() } ?: innerRoom.id()
}
override val displayName: String
get() {
return innerRoom.displayName()

View file

@ -90,7 +90,7 @@ internal class RustRoomSummaryDataSource(
return allRoomsLoadingState
}
override fun updateRoomListVisibleRange(range: IntRange) {
override fun updateAllRoomsVisibleRange(range: IntRange) {
Timber.v("setVisibleRange=$range")
sessionCoroutineScope.launch {
try {

View file

@ -64,11 +64,11 @@ class FakeMatrixClient(
private val getProfileResults = mutableMapOf<UserId, Result<MatrixUser>>()
private var uploadMediaResult: Result<String> = Result.success(AN_AVATAR_URL)
override fun getRoom(roomId: RoomId): MatrixRoom? {
override suspend fun getRoom(roomId: RoomId): MatrixRoom? {
return getRoomResults[roomId]
}
override fun findDM(userId: UserId): MatrixRoom? {
override suspend fun findDM(userId: UserId): MatrixRoom? {
return findDmResult
}

View file

@ -44,7 +44,6 @@ class FakeMatrixRoom(
override val sessionId: SessionId = A_SESSION_ID,
override val roomId: RoomId = A_ROOM_ID,
override val name: String? = null,
override val bestName: String = "",
override val displayName: String = "",
override val topic: String? = null,
override val avatarUrl: String? = null,

View file

@ -54,7 +54,7 @@ class FakeRoomSummaryDataSource : RoomSummaryDataSource {
var latestSlidingSyncRange: IntRange? = null
private set
override fun updateRoomListVisibleRange(range: IntRange) {
override fun updateAllRoomsVisibleRange(range: IntRange) {
latestSlidingSyncRange = range
}
}

View file

@ -36,7 +36,7 @@ import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.roomListPlaceholder
import io.element.android.libraries.designsystem.theme.placeholderBackground
import io.element.android.libraries.theme.ElementTheme
@Composable
@ -53,7 +53,7 @@ fun MatrixUserHeaderPlaceholder(
modifier = Modifier
.padding(vertical = 12.dp)
.size(AvatarSize.UserPreference.dp)
.background(color = ElementTheme.colors.roomListPlaceholder, shape = CircleShape)
.background(color = ElementTheme.colors.placeholderBackground, shape = CircleShape)
)
Spacer(modifier = Modifier.width(16.dp))
Column(

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:863eaa52c2c0f684d29ed6d6618f4fcd18c953381d6624bdeb70a484bb37520e
size 36819
oid sha256:e6a92140902cde5caf356028c3f67e4d9813672b4799c079de4b634b12f739ba
size 37014

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6523908f527a20ac68e3dad227f5987e348411629c045ea2a9af2cfe48a66867
size 36517
oid sha256:32d3246e87a7ce66191f9a84dc6b0b3106e044d510fdb0b69bad9260f7be6d4e
size 36716