Room directory : implement simple join room

This commit is contained in:
ganfra 2024-03-26 12:32:15 +01:00
parent 83b6875394
commit 0700384ef0
10 changed files with 89 additions and 19 deletions

View file

@ -389,7 +389,7 @@ class LoggedInFlowNode @AssistedInject constructor(
NavTarget.RoomDirectorySearch -> { NavTarget.RoomDirectorySearch -> {
roomDirectoryEntryPoint.nodeBuilder(this, buildContext) roomDirectoryEntryPoint.nodeBuilder(this, buildContext)
.callback(object : RoomDirectoryEntryPoint.Callback { .callback(object : RoomDirectoryEntryPoint.Callback {
override fun onJoinRoom(roomId: RoomId) { override fun onOpenRoom(roomId: RoomId) {
coroutineScope.launch { attachRoom(roomId) } coroutineScope.launch { attachRoom(roomId) }
} }
}) })

View file

@ -32,7 +32,7 @@ interface RoomDirectoryEntryPoint : FeatureEntryPoint {
} }
interface Callback : Plugin { interface Callback : Plugin {
fun onJoinRoom(roomId: RoomId) fun onOpenRoom(roomId: RoomId)
} }
} }

View file

@ -16,7 +16,11 @@
package io.element.android.features.roomdirectory.impl.root package io.element.android.features.roomdirectory.impl.root
import io.element.android.libraries.matrix.api.core.RoomId
sealed interface RoomDirectoryEvents { sealed interface RoomDirectoryEvents {
data class JoinRoom(val roomId: RoomId) : RoomDirectoryEvents
data class Search(val query: String) : RoomDirectoryEvents data class Search(val query: String) : RoomDirectoryEvents
data object LoadMore : RoomDirectoryEvents data object LoadMore : RoomDirectoryEvents
data object JoinRoomDismissError : RoomDirectoryEvents
} }

View file

@ -36,9 +36,9 @@ class RoomDirectoryNode @AssistedInject constructor(
private val presenter: RoomDirectoryPresenter, private val presenter: RoomDirectoryPresenter,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
private fun onJoinRoom(roomId: RoomId) { private fun onRoomJoined(roomId: RoomId) {
plugins<RoomDirectoryEntryPoint.Callback>().forEach { plugins<RoomDirectoryEntryPoint.Callback>().forEach {
it.onJoinRoom(roomId) it.onOpenRoom(roomId)
} }
} }
@ -47,7 +47,7 @@ class RoomDirectoryNode @AssistedInject constructor(
val state = presenter.present() val state = presenter.present()
RoomDirectoryView( RoomDirectoryView(
state = state, state = state,
onJoinRoom = ::onJoinRoom, onRoomJoined = ::onRoomJoined,
onBackPressed = ::navigateUp, onBackPressed = ::navigateUp,
modifier = modifier modifier = modifier
) )

View file

@ -18,6 +18,7 @@ package io.element.android.features.roomdirectory.impl.root
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -27,13 +28,17 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import io.element.android.features.roomdirectory.impl.root.model.toUiModel import io.element.android.features.roomdirectory.impl.root.model.toUiModel
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runUpdatingState
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
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
@ -41,6 +46,7 @@ import javax.inject.Inject
class RoomDirectoryPresenter @Inject constructor( class RoomDirectoryPresenter @Inject constructor(
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
private val matrixClient: MatrixClient,
roomDirectoryService: RoomDirectoryService, roomDirectoryService: RoomDirectoryService,
) : Presenter<RoomDirectoryState> { ) : Presenter<RoomDirectoryState> {
@ -56,6 +62,9 @@ class RoomDirectoryPresenter @Inject constructor(
val hasMoreToLoad by produceState(initialValue = true, allRooms) { val hasMoreToLoad by produceState(initialValue = true, allRooms) {
value = roomDirectoryList.hasMoreToLoad() value = roomDirectoryList.hasMoreToLoad()
} }
val joinRoomAction: MutableState<AsyncAction<RoomId>> = remember {
mutableStateOf(AsyncAction.Uninitialized)
}
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
LaunchedEffect(searchQuery) { LaunchedEffect(searchQuery) {
roomDirectoryList.filter(searchQuery, 20) roomDirectoryList.filter(searchQuery, 20)
@ -70,6 +79,12 @@ class RoomDirectoryPresenter @Inject constructor(
is RoomDirectoryEvents.Search -> { is RoomDirectoryEvents.Search -> {
searchQuery = event.query searchQuery = event.query
} }
is RoomDirectoryEvents.JoinRoom -> {
coroutineScope.joinRoom(joinRoomAction, event.roomId)
}
RoomDirectoryEvents.JoinRoomDismissError -> {
joinRoomAction.value = AsyncAction.Uninitialized
}
} }
} }
@ -77,10 +92,17 @@ class RoomDirectoryPresenter @Inject constructor(
query = searchQuery, query = searchQuery,
roomDescriptions = allRooms, roomDescriptions = allRooms,
displayLoadMoreIndicator = hasMoreToLoad, displayLoadMoreIndicator = hasMoreToLoad,
joinRoomAction = joinRoomAction.value,
eventSink = ::handleEvents eventSink = ::handleEvents
) )
} }
private fun CoroutineScope.joinRoom(state: MutableState<AsyncAction<RoomId>>, roomId: RoomId) = launch {
state.runUpdatingState {
matrixClient.joinRoom(roomId)
}
}
@Composable @Composable
private fun RoomDirectoryList.collectItemsAsState() = remember { private fun RoomDirectoryList.collectItemsAsState() = remember {
items.map { list -> items.map { list ->

View file

@ -17,12 +17,15 @@
package io.element.android.features.roomdirectory.impl.root package io.element.android.features.roomdirectory.impl.root
import io.element.android.features.roomdirectory.impl.root.model.RoomDescriptionUiModel import io.element.android.features.roomdirectory.impl.root.model.RoomDescriptionUiModel
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
data class RoomDirectoryState( data class RoomDirectoryState(
val query: String, val query: String,
val roomDescriptions: ImmutableList<RoomDescriptionUiModel>, val roomDescriptions: ImmutableList<RoomDescriptionUiModel>,
val displayLoadMoreIndicator: Boolean, val displayLoadMoreIndicator: Boolean,
val joinRoomAction: AsyncAction<RoomId>,
val eventSink: (RoomDirectoryEvents) -> Unit val eventSink: (RoomDirectoryEvents) -> Unit
) { ) {
val displayEmptyState = roomDescriptions.isEmpty() && !displayLoadMoreIndicator val displayEmptyState = roomDescriptions.isEmpty() && !displayLoadMoreIndicator

View file

@ -18,9 +18,9 @@ package io.element.android.features.roomdirectory.impl.root
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.roomdirectory.impl.root.model.RoomDescriptionUiModel import io.element.android.features.roomdirectory.impl.root.model.RoomDescriptionUiModel
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -63,13 +63,13 @@ open class RoomDirectorySearchStateProvider : PreviewParameterProvider<RoomDirec
fun aRoomDirectoryState( fun aRoomDirectoryState(
query: String = "", query: String = "",
isSearchActive: Boolean = false,
displayLoadMoreIndicator: Boolean = false, displayLoadMoreIndicator: Boolean = false,
roomDescriptions: ImmutableList<RoomDescriptionUiModel> = persistentListOf(), roomDescriptions: ImmutableList<RoomDescriptionUiModel> = persistentListOf(),
searchResults: SearchBarResultState<ImmutableList<RoomDescriptionUiModel>> = SearchBarResultState.Initial(), joinRoomAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
) = RoomDirectoryState( ) = RoomDirectoryState(
query = query, query = query,
roomDescriptions = roomDescriptions, roomDescriptions = roomDescriptions,
displayLoadMoreIndicator = displayLoadMoreIndicator, displayLoadMoreIndicator = displayLoadMoreIndicator,
joinRoomAction = joinRoomAction,
eventSink = {}, eventSink = {},
) )

View file

@ -47,6 +47,7 @@ import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.roomdirectory.impl.root.model.RoomDescriptionUiModel import io.element.android.features.roomdirectory.impl.root.model.RoomDescriptionUiModel
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
@ -66,10 +67,15 @@ import kotlinx.collections.immutable.ImmutableList
@Composable @Composable
fun RoomDirectoryView( fun RoomDirectoryView(
state: RoomDirectoryState, state: RoomDirectoryState,
onRoomJoined: (RoomId) -> Unit,
onBackPressed: () -> Unit, onBackPressed: () -> Unit,
onJoinRoom: (RoomId) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
fun joinRoom(roomId: RoomId) {
state.eventSink(RoomDirectoryEvents.JoinRoom(roomId))
}
Scaffold( Scaffold(
modifier = modifier, modifier = modifier,
topBar = { topBar = {
@ -78,13 +84,19 @@ fun RoomDirectoryView(
content = { padding -> content = { padding ->
RoomDirectoryContent( RoomDirectoryContent(
state = state, state = state,
onResultClicked = onJoinRoom, onResultClicked = ::joinRoom,
modifier = Modifier modifier = Modifier
.padding(padding) .padding(padding)
.consumeWindowInsets(padding) .consumeWindowInsets(padding)
) )
} }
) )
AsyncActionView(
async = state.joinRoomAction,
onSuccess = onRoomJoined,
onErrorDismiss = {
state.eventSink(RoomDirectoryEvents.JoinRoomDismissError)
})
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -295,7 +307,7 @@ private fun RoomDirectoryRoomRow(
fun RoomDirectorySearchViewLightPreview(@PreviewParameter(RoomDirectorySearchStateProvider::class) state: RoomDirectoryState) = ElementPreview { fun RoomDirectorySearchViewLightPreview(@PreviewParameter(RoomDirectorySearchStateProvider::class) state: RoomDirectoryState) = ElementPreview {
RoomDirectoryView( RoomDirectoryView(
state = state, state = state,
onJoinRoom = {}, onRoomJoined = {},
onBackPressed = {}, onBackPressed = {},
) )
} }

View file

@ -59,6 +59,7 @@ interface MatrixClient : Closeable {
suspend fun setDisplayName(displayName: String): Result<Unit> suspend fun setDisplayName(displayName: String): Result<Unit>
suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result<Unit> suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result<Unit>
suspend fun removeAvatar(): Result<Unit> suspend fun removeAvatar(): Result<Unit>
suspend fun joinRoom(roomId: RoomId): Result<RoomId>
fun syncService(): SyncService fun syncService(): SyncService
fun sessionVerificationService(): SessionVerificationService fun sessionVerificationService(): SessionVerificationService
fun pushersService(): PushersService fun pushersService(): PushersService

View file

@ -73,6 +73,7 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -103,6 +104,8 @@ import org.matrix.rustcomponents.sdk.use
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import org.matrix.rustcomponents.sdk.CreateRoomParameters as RustCreateRoomParameters import org.matrix.rustcomponents.sdk.CreateRoomParameters as RustCreateRoomParameters
import org.matrix.rustcomponents.sdk.RoomPreset as RustRoomPreset import org.matrix.rustcomponents.sdk.RoomPreset as RustRoomPreset
import org.matrix.rustcomponents.sdk.RoomVisibility as RustRoomVisibility import org.matrix.rustcomponents.sdk.RoomVisibility as RustRoomVisibility
@ -320,6 +323,22 @@ class RustMatrixClient(
} }
} }
/**
* Wait for the room to be available in the room list.
* @param roomId the room id to wait for
* @param timeout the timeout to wait for the room to be available
* @throws TimeoutCancellationException if the room is not available after the timeout
*/
private suspend fun awaitRoom(roomId: RoomId, timeout: Duration) {
withTimeout(timeout) {
roomListService.allRooms.summaries
.filter { roomSummaries ->
roomSummaries.map { it.identifier() }.contains(roomId.value)
}
.first()
}
}
private suspend fun pairOfRoom(roomId: RoomId): Pair<RoomListItem, Room>? { private suspend fun pairOfRoom(roomId: RoomId): Pair<RoomListItem, Room>? {
val cachedRoomListItem = innerRoomListService.roomOrNull(roomId.value) val cachedRoomListItem = innerRoomListService.roomOrNull(roomId.value)
val fullRoom = cachedRoomListItem?.fullRoomWithTimeline(filter = eventFilters) val fullRoom = cachedRoomListItem?.fullRoomWithTimeline(filter = eventFilters)
@ -369,14 +388,11 @@ class RustMatrixClient(
powerLevelContentOverride = defaultRoomCreationPowerLevels, powerLevelContentOverride = defaultRoomCreationPowerLevels,
) )
val roomId = RoomId(client.createRoom(rustParams)) val roomId = RoomId(client.createRoom(rustParams))
// Wait to receive the room back from the sync but do not returns failure if it fails.
// Wait to receive the room back from the sync try {
withTimeout(30_000L) { awaitRoom(roomId, 30.seconds)
roomListService.allRooms.summaries } catch (e: Exception) {
.filter { roomSummaries -> Timber.e(e, "Timeout waiting for the room to be available in the room list")
roomSummaries.map { it.identifier() }.contains(roomId.value)
}
.first()
} }
roomId roomId
} }
@ -425,6 +441,18 @@ class RustMatrixClient(
runCatching { client.removeAvatar() } runCatching { client.removeAvatar() }
} }
override suspend fun joinRoom(roomId: RoomId): Result<RoomId> = withContext(sessionDispatcher) {
runCatching {
client.joinRoomById(roomId.value).destroy()
try {
awaitRoom(roomId, 10.seconds)
} catch (e: Exception) {
Timber.e(e, "Timeout waiting for the room to be available in the room list")
}
roomId
}
}
override fun syncService(): SyncService = rustSyncService override fun syncService(): SyncService = rustSyncService
override fun sessionVerificationService(): SessionVerificationService = verificationService override fun sessionVerificationService(): SessionVerificationService = verificationService