Merge pull request #2620 from element-hq/feature/fga/room_directory

Feature/fga/room directory
This commit is contained in:
ganfra 2024-03-29 16:54:27 +01:00 committed by GitHub
commit a9f326c81c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
78 changed files with 2114 additions and 41 deletions

View file

@ -35,6 +35,7 @@ import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
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.roomdirectory.RoomDirectoryService
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.awaitLoaded
import io.element.android.libraries.matrix.api.sync.SyncService
@ -53,6 +54,7 @@ import io.element.android.libraries.matrix.impl.room.MatrixRoomInfoMapper
import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber
import io.element.android.libraries.matrix.impl.room.RustMatrixRoom
import io.element.android.libraries.matrix.impl.roomdirectory.RustRoomDirectoryService
import io.element.android.libraries.matrix.impl.roomlist.RoomListFactory
import io.element.android.libraries.matrix.impl.roomlist.RustRoomListService
import io.element.android.libraries.matrix.impl.roomlist.fullRoomWithTimeline
@ -71,6 +73,7 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
@ -101,6 +104,8 @@ import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
import java.io.File
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.RoomPreset as RustRoomPreset
import org.matrix.rustcomponents.sdk.RoomVisibility as RustRoomVisibility
@ -150,6 +155,12 @@ class RustMatrixClient(
sessionCoroutineScope = sessionCoroutineScope,
dispatchers = dispatchers,
)
private val roomDirectoryService = RustRoomDirectoryService(
client = client,
sessionDispatcher = sessionDispatcher,
)
private val sessionDirectoryNameProvider = SessionDirectoryNameProvider()
private val isLoggingOut = AtomicBoolean(false)
@ -309,6 +320,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>? {
val cachedRoomListItem = innerRoomListService.roomOrNull(roomId.value)
val fullRoom = cachedRoomListItem?.fullRoomWithTimeline(filter = eventFilters)
@ -358,14 +385,11 @@ class RustMatrixClient(
powerLevelContentOverride = defaultRoomCreationPowerLevels,
)
val roomId = RoomId(client.createRoom(rustParams))
// Wait to receive the room back from the sync
withTimeout(30_000L) {
roomListService.allRooms.summaries
.filter { roomSummaries ->
roomSummaries.map { it.identifier() }.contains(roomId.value)
}
.first()
// Wait to receive the room back from the sync but do not returns failure if it fails.
try {
awaitRoom(roomId, 30.seconds)
} catch (e: Exception) {
Timber.e(e, "Timeout waiting for the room to be available in the room list")
}
roomId
}
@ -414,6 +438,18 @@ class RustMatrixClient(
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 sessionVerificationService(): SessionVerificationService = verificationService
@ -426,6 +462,8 @@ class RustMatrixClient(
override fun notificationSettingsService(): NotificationSettingsService = notificationSettingsService
override fun roomDirectoryService(): RoomDirectoryService = roomDirectoryService
override fun close() {
sessionCoroutineScope.cancel()
clientDelegateTaskHandle?.cancelAndDestroy()

View file

@ -26,6 +26,7 @@ import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import kotlinx.coroutines.CoroutineScope
@ -68,4 +69,9 @@ object SessionMatrixModule {
fun provideSessionCoroutineScope(matrixClient: MatrixClient): CoroutineScope {
return matrixClient.sessionCoroutineScope
}
@Provides
fun providesRoomDirectoryService(matrixClient: MatrixClient): RoomDirectoryService {
return matrixClient.roomDirectoryService()
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2024 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.libraries.matrix.impl.roomdirectory
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription
import org.matrix.rustcomponents.sdk.PublicRoomJoinRule
import org.matrix.rustcomponents.sdk.RoomDescription as RustRoomDescription
class RoomDescriptionMapper {
fun map(roomDescription: RustRoomDescription): RoomDescription {
return RoomDescription(
roomId = RoomId(roomDescription.roomId),
name = roomDescription.name,
topic = roomDescription.topic,
avatarUrl = roomDescription.avatarUrl,
alias = roomDescription.alias,
joinRule = when (roomDescription.joinRule) {
PublicRoomJoinRule.PUBLIC -> RoomDescription.JoinRule.PUBLIC
PublicRoomJoinRule.KNOCK -> RoomDescription.JoinRule.KNOCK
null -> RoomDescription.JoinRule.UNKNOWN
},
isWorldReadable = roomDescription.isWorldReadable,
numberOfMembers = roomDescription.joinedMembers.toLong(),
)
}
}

View file

@ -0,0 +1,45 @@
/*
* Copyright (c) 2024 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.libraries.matrix.impl.roomdirectory
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.catch
import org.matrix.rustcomponents.sdk.RoomDirectorySearch
import org.matrix.rustcomponents.sdk.RoomDirectorySearchEntriesListener
import org.matrix.rustcomponents.sdk.RoomDirectorySearchEntryUpdate
import timber.log.Timber
internal fun RoomDirectorySearch.resultsFlow(): Flow<List<RoomDirectorySearchEntryUpdate>> =
callbackFlow {
val listener = object : RoomDirectorySearchEntriesListener {
override fun onUpdate(roomEntriesUpdate: List<RoomDirectorySearchEntryUpdate>) {
trySendBlocking(roomEntriesUpdate)
}
}
val result = results(listener)
awaitClose {
result.cancelAndDestroy()
}
}.catch {
Timber.d(it, "timelineDiffFlow() failed")
}.buffer(Channel.UNLIMITED)

View file

@ -0,0 +1,96 @@
/*
* Copyright (c) 2024 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.libraries.matrix.impl.roomdirectory
import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.RoomDirectorySearchEntryUpdate
import timber.log.Timber
import kotlin.coroutines.CoroutineContext
class RoomDirectorySearchProcessor(
private val roomDescriptions: MutableSharedFlow<List<RoomDescription>>,
private val coroutineContext: CoroutineContext,
private val roomDescriptionMapper: RoomDescriptionMapper,
) {
private val mutex = Mutex()
suspend fun postUpdates(updates: List<RoomDirectorySearchEntryUpdate>) {
updateRoomDescriptions {
Timber.v("Update room descriptions from postUpdates (with ${updates.size} items) on ${Thread.currentThread()}")
updates.forEach { update ->
applyUpdate(update)
}
}
}
private fun MutableList<RoomDescription>.applyUpdate(update: RoomDirectorySearchEntryUpdate) {
when (update) {
is RoomDirectorySearchEntryUpdate.Append -> {
val roomSummaries = update.values.map(roomDescriptionMapper::map)
addAll(roomSummaries)
}
is RoomDirectorySearchEntryUpdate.PushBack -> {
val roomDescription = roomDescriptionMapper.map(update.value)
add(roomDescription)
}
is RoomDirectorySearchEntryUpdate.PushFront -> {
val roomDescription = roomDescriptionMapper.map(update.value)
add(0, roomDescription)
}
is RoomDirectorySearchEntryUpdate.Set -> {
val roomDescription = roomDescriptionMapper.map(update.value)
this[update.index.toInt()] = roomDescription
}
is RoomDirectorySearchEntryUpdate.Insert -> {
val roomDescription = roomDescriptionMapper.map(update.value)
add(update.index.toInt(), roomDescription)
}
is RoomDirectorySearchEntryUpdate.Remove -> {
removeAt(update.index.toInt())
}
is RoomDirectorySearchEntryUpdate.Reset -> {
clear()
addAll(update.values.map(roomDescriptionMapper::map))
}
RoomDirectorySearchEntryUpdate.PopBack -> {
removeLastOrNull()
}
RoomDirectorySearchEntryUpdate.PopFront -> {
removeFirstOrNull()
}
RoomDirectorySearchEntryUpdate.Clear -> {
clear()
}
is RoomDirectorySearchEntryUpdate.Truncate -> {
subList(update.length.toInt(), size).clear()
}
}
}
private suspend fun updateRoomDescriptions(block: suspend MutableList<RoomDescription>.() -> Unit) = withContext(coroutineContext) {
mutex.withLock {
val current = roomDescriptions.replayCache.lastOrNull()
val mutableRoomSummaries = current.orEmpty().toMutableList()
block(mutableRoomSummaries)
roomDescriptions.emit(mutableRoomSummaries)
}
}
}

View file

@ -0,0 +1,96 @@
/*
* Copyright (c) 2024 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.libraries.matrix.impl.roomdirectory
import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.matrix.rustcomponents.sdk.RoomDirectorySearch
import kotlin.coroutines.CoroutineContext
class RustRoomDirectoryList(
private val inner: RoomDirectorySearch,
coroutineScope: CoroutineScope,
private val coroutineContext: CoroutineContext,
) : RoomDirectoryList {
private val hasMoreToLoad = MutableStateFlow(true)
private val items = MutableSharedFlow<List<RoomDescription>>(replay = 1)
private val processor = RoomDirectorySearchProcessor(items, coroutineContext, RoomDescriptionMapper())
init {
launchIn(coroutineScope)
}
private fun launchIn(coroutineScope: CoroutineScope) {
inner
.resultsFlow()
.onEach { updates ->
processor.postUpdates(updates)
}
.flowOn(coroutineContext)
.launchIn(coroutineScope)
}
override suspend fun filter(filter: String?, batchSize: Int): Result<Unit> {
return execute {
inner.search(filter = filter, batchSize = batchSize.toUInt())
}
}
override suspend fun loadMore(): Result<Unit> {
return execute {
inner.nextPage()
}
}
private suspend fun execute(action: suspend () -> Unit): Result<Unit> {
return try {
// We always assume there is more to load until we know there isn't.
// As accessing hasMoreToLoad is otherwise blocked by the current action.
hasMoreToLoad.value = true
action()
Result.success(Unit)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Result.failure(e)
} finally {
hasMoreToLoad.value = hasMoreToLoad()
}
}
private suspend fun hasMoreToLoad(): Boolean {
return !inner.isAtLastPage()
}
override val state: Flow<RoomDirectoryList.State> =
combine(hasMoreToLoad, items) { hasMoreToLoad, items ->
RoomDirectoryList.State(
hasMoreToLoad = hasMoreToLoad,
items = items
)
}
.flowOn(coroutineContext)
}

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2024 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.libraries.matrix.impl.roomdirectory
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import org.matrix.rustcomponents.sdk.Client
class RustRoomDirectoryService(
private val client: Client,
private val sessionDispatcher: CoroutineDispatcher,
) : RoomDirectoryService {
override fun createRoomDirectoryList(scope: CoroutineScope): RoomDirectoryList {
return RustRoomDirectoryList(client.roomDirectorySearch(), scope, sessionDispatcher)
}
}