Merge pull request #2620 from element-hq/feature/fga/room_directory
Feature/fga/room directory
This commit is contained in:
commit
a9f326c81c
78 changed files with 2114 additions and 41 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue