Browse Source

RoomDirectory : continue improving interactions

pull/2620/head
ganfra 6 months ago
parent
commit
b900818001
  1. 1
      features/roomdirectory/api/build.gradle.kts
  2. 28
      features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDescription.kt
  3. 56
      features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt
  4. 4
      features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryState.kt
  5. 8
      features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt
  6. 8
      features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt
  7. 20
      features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt
  8. 34
      features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDirectoryListState.kt
  9. 8
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryList.kt
  10. 4
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryService.kt
  11. 4
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
  12. 73
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryList.kt
  13. 6
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryService.kt
  14. 3
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/FakeRoomDirectoryService.kt

1
features/roomdirectory/api/build.gradle.kts

@ -25,4 +25,5 @@ android { @@ -25,4 +25,5 @@ android {
dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.designsystem)
}

28
features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDescription.kt

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
/*
* 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.features.roomdirectory.api
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.api.core.RoomId
data class RoomDescription(
val roomId: RoomId,
val name: String,
val description: String,
val avatarData: AvatarData,
val canBeJoined: Boolean,
)

56
features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt

@ -22,12 +22,12 @@ import androidx.compose.runtime.MutableState @@ -22,12 +22,12 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
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
import androidx.compose.runtime.setValue
import io.element.android.features.roomdirectory.impl.root.model.toUiModel
import io.element.android.features.roomdirectory.impl.root.model.RoomDirectoryListState
import io.element.android.features.roomdirectory.impl.root.model.toFeatureModel
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runUpdatingState
@ -36,9 +36,9 @@ import io.element.android.libraries.matrix.api.MatrixClient @@ -36,9 +36,9 @@ 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.RoomDirectoryService
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@ -47,34 +47,43 @@ import javax.inject.Inject @@ -47,34 +47,43 @@ import javax.inject.Inject
class RoomDirectoryPresenter @Inject constructor(
private val dispatchers: CoroutineDispatchers,
private val matrixClient: MatrixClient,
roomDirectoryService: RoomDirectoryService,
private val roomDirectoryService: RoomDirectoryService,
) : Presenter<RoomDirectoryState> {
private val roomDirectoryList = roomDirectoryService.createRoomDirectoryList()
@Composable
override fun present(): RoomDirectoryState {
var loadingMore by remember {
mutableStateOf(false)
}
var searchQuery by rememberSaveable {
mutableStateOf("")
mutableStateOf<String?>(null)
}
val allRooms by roomDirectoryList.collectItemsAsState()
val hasMoreToLoad by produceState(initialValue = true, allRooms) {
value = roomDirectoryList.hasMoreToLoad()
val coroutineScope = rememberCoroutineScope()
val roomDirectoryList = remember {
roomDirectoryService.createRoomDirectoryList(coroutineScope)
}
val listState by roomDirectoryList.collectState()
val joinRoomAction: MutableState<AsyncAction<RoomId>> = remember {
mutableStateOf(AsyncAction.Uninitialized)
}
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(searchQuery) {
if (searchQuery == null) return@LaunchedEffect
//debounce search query
delay(300)
//cancel load more right away
loadingMore = false
roomDirectoryList.filter(searchQuery, 20)
}
LaunchedEffect(loadingMore) {
if (loadingMore) {
roomDirectoryList.loadMore()
loadingMore = false
}
}
fun handleEvents(event: RoomDirectoryEvents) {
when (event) {
RoomDirectoryEvents.LoadMore -> {
coroutineScope.launch {
roomDirectoryList.loadMore()
}
loadingMore = true
}
is RoomDirectoryEvents.Search -> {
searchQuery = event.query
@ -89,9 +98,9 @@ class RoomDirectoryPresenter @Inject constructor( @@ -89,9 +98,9 @@ class RoomDirectoryPresenter @Inject constructor(
}
return RoomDirectoryState(
query = searchQuery,
roomDescriptions = allRooms,
displayLoadMoreIndicator = hasMoreToLoad,
query = searchQuery.orEmpty(),
roomDescriptions = listState.items,
displayLoadMoreIndicator = listState.hasMoreToLoad,
joinRoomAction = joinRoomAction.value,
eventSink = ::handleEvents
)
@ -104,11 +113,12 @@ class RoomDirectoryPresenter @Inject constructor( @@ -104,11 +113,12 @@ class RoomDirectoryPresenter @Inject constructor(
}
@Composable
private fun RoomDirectoryList.collectItemsAsState() = remember {
items.map { list ->
list
.map { roomDescription -> roomDescription.toUiModel() }
private fun RoomDirectoryList.collectState() = remember {
state.map {
val items = it.items
.map { roomDescription -> roomDescription.toFeatureModel() }
.toImmutableList()
RoomDirectoryListState(items = items, hasMoreToLoad = it.hasMoreToLoad)
}.flowOn(dispatchers.computation)
}.collectAsState(persistentListOf())
}.collectAsState(RoomDirectoryListState.Default)
}

4
features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryState.kt

@ -16,14 +16,14 @@ @@ -16,14 +16,14 @@
package io.element.android.features.roomdirectory.impl.root
import io.element.android.features.roomdirectory.impl.root.model.RoomDescriptionUiModel
import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.collections.immutable.ImmutableList
data class RoomDirectoryState(
val query: String,
val roomDescriptions: ImmutableList<RoomDescriptionUiModel>,
val roomDescriptions: ImmutableList<RoomDescription>,
val displayLoadMoreIndicator: Boolean,
val joinRoomAction: AsyncAction<RoomId>,
val eventSink: (RoomDirectoryEvents) -> Unit

8
features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt

@ -17,7 +17,7 @@ @@ -17,7 +17,7 @@
package io.element.android.features.roomdirectory.impl.root
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.roomdirectory.impl.root.model.RoomDescriptionUiModel
import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
@ -32,7 +32,7 @@ open class RoomDirectorySearchStateProvider : PreviewParameterProvider<RoomDirec @@ -32,7 +32,7 @@ open class RoomDirectorySearchStateProvider : PreviewParameterProvider<RoomDirec
aRoomDirectoryState(
query = "Element",
roomDescriptions = persistentListOf(
RoomDescriptionUiModel(
RoomDescription(
roomId = RoomId("@exa:matrix.org"),
name = "Element X Android",
description = "Element X is a secure, private and decentralized messenger.",
@ -44,7 +44,7 @@ open class RoomDirectorySearchStateProvider : PreviewParameterProvider<RoomDirec @@ -44,7 +44,7 @@ open class RoomDirectorySearchStateProvider : PreviewParameterProvider<RoomDirec
),
canBeJoined = true,
),
RoomDescriptionUiModel(
RoomDescription(
roomId = RoomId("@exi:matrix.org"),
name = "Element X iOS",
description = "Element X is a secure, private and decentralized messenger.",
@ -64,7 +64,7 @@ open class RoomDirectorySearchStateProvider : PreviewParameterProvider<RoomDirec @@ -64,7 +64,7 @@ open class RoomDirectorySearchStateProvider : PreviewParameterProvider<RoomDirec
fun aRoomDirectoryState(
query: String = "",
displayLoadMoreIndicator: Boolean = false,
roomDescriptions: ImmutableList<RoomDescriptionUiModel> = persistentListOf(),
roomDescriptions: ImmutableList<RoomDescription> = persistentListOf(),
joinRoomAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
) = RoomDirectoryState(
query = query,

8
features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt

@ -46,7 +46,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter @@ -46,7 +46,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.roomdirectory.impl.root.model.RoomDescriptionUiModel
import io.element.android.features.roomdirectory.api.RoomDescription
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.button.BackButton
@ -144,7 +144,7 @@ private fun RoomDirectoryContent( @@ -144,7 +144,7 @@ private fun RoomDirectoryContent(
@Composable
private fun RoomDirectoryRoomList(
roomDescriptions: ImmutableList<RoomDescriptionUiModel>,
roomDescriptions: ImmutableList<RoomDescription>,
displayLoadMoreIndicator: Boolean,
displayEmptyState: Boolean,
onResultClicked: (RoomId) -> Unit,
@ -185,7 +185,7 @@ private fun LoadMoreIndicator(modifier: Modifier = Modifier) { @@ -185,7 +185,7 @@ private fun LoadMoreIndicator(modifier: Modifier = Modifier) {
modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(8.dp),
.padding(24.dp),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(
@ -249,7 +249,7 @@ private fun SearchTextField( @@ -249,7 +249,7 @@ private fun SearchTextField(
@Composable
private fun RoomDirectoryRoomRow(
roomDescription: RoomDescriptionUiModel,
roomDescription: RoomDescription,
onClick: (RoomId) -> Unit,
modifier: Modifier = Modifier,
) {

20
features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescriptionUiModel.kt → features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt

@ -16,30 +16,22 @@ @@ -16,30 +16,22 @@
package io.element.android.features.roomdirectory.impl.root.model
import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription
import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription as MatrixRoomDescription
data class RoomDescriptionUiModel(
val roomId: RoomId,
val name: String,
val description: String,
val avatarData: AvatarData,
val canBeJoined: Boolean,
)
fun RoomDescription.toUiModel(): RoomDescriptionUiModel {
return RoomDescriptionUiModel(
fun MatrixRoomDescription.toFeatureModel(): RoomDescription {
return RoomDescription(
roomId = roomId,
name = name ?: "",
description = topic ?: "",
description = topic ?: alias ?: roomId.value,
avatarData = AvatarData(
id = roomId.value,
name = name ?: "",
url = avatarUrl,
size = AvatarSize.RoomDirectoryItem,
),
canBeJoined = joinRule == RoomDescription.JoinRule.PUBLIC,
canBeJoined = joinRule == MatrixRoomDescription.JoinRule.PUBLIC,
)
}

34
features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDirectoryListState.kt

@ -0,0 +1,34 @@ @@ -0,0 +1,34 @@
/*
* 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.features.roomdirectory.impl.root.model
import io.element.android.features.roomdirectory.api.RoomDescription
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
internal data class RoomDirectoryListState(
val hasMoreToLoad: Boolean,
val items: ImmutableList<RoomDescription>,
) {
companion object {
val Default = RoomDirectoryListState(
hasMoreToLoad = true,
items = persistentListOf()
)
}
}

8
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryList.kt

@ -21,6 +21,10 @@ import kotlinx.coroutines.flow.Flow @@ -21,6 +21,10 @@ import kotlinx.coroutines.flow.Flow
interface RoomDirectoryList {
suspend fun filter(filter: String?, batchSize: Int): Result<Unit>
suspend fun loadMore(): Result<Unit>
suspend fun hasMoreToLoad(): Boolean
val items: Flow<List<RoomDescription>>
val state: Flow<State>
data class State(
val hasMoreToLoad: Boolean,
val items: List<RoomDescription>,
)
}

4
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryService.kt

@ -16,6 +16,8 @@ @@ -16,6 +16,8 @@
package io.element.android.libraries.matrix.api.roomdirectory
import kotlinx.coroutines.CoroutineScope
interface RoomDirectoryService {
fun createRoomDirectoryList(): RoomDirectoryList
fun createRoomDirectoryList(scope: CoroutineScope): RoomDirectoryList
}

4
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt

@ -76,6 +76,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -76,6 +76,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@ -158,7 +159,6 @@ class RustMatrixClient( @@ -158,7 +159,6 @@ class RustMatrixClient(
private val roomDirectoryService = RustRoomDirectoryService(
client = client,
sessionCoroutineScope = sessionCoroutineScope,
sessionDispatcher = sessionDispatcher,
)
@ -441,7 +441,7 @@ class RustMatrixClient( @@ -441,7 +441,7 @@ class RustMatrixClient(
runCatching { client.removeAvatar() }
}
override suspend fun joinRoom(roomId: RoomId): Result<RoomId> = withContext(sessionDispatcher) {
override suspend fun joinRoom(roomId: RoomId): Result<RoomId> = withContext(sessionDispatcher) {
runCatching {
client.joinRoomById(roomId.value).destroy()
try {

73
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryList.kt

@ -19,64 +19,79 @@ package io.element.android.libraries.matrix.impl.roomdirectory @@ -19,64 +19,79 @@ 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.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.debounce
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 kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.milliseconds
import org.matrix.rustcomponents.sdk.RoomDirectorySearch as InnerRoomDirectorySearch
import org.matrix.rustcomponents.sdk.RoomDirectorySearch
import kotlin.coroutines.CoroutineContext
class RustRoomDirectoryList(
private val inner: InnerRoomDirectorySearch,
sessionCoroutineScope: CoroutineScope,
sessionDispatcher: CoroutineDispatcher,
private val inner: RoomDirectorySearch,
coroutineScope: CoroutineScope,
private val coroutineContext: CoroutineContext,
) : RoomDirectoryList {
private val _items = MutableSharedFlow<List<RoomDescription>>(replay = 1)
private val processor = RoomDirectorySearchProcessor(_items, sessionDispatcher, RoomDescriptionMapper())
private val hasMoreToLoad = MutableStateFlow(true)
private val items = MutableSharedFlow<List<RoomDescription>>(replay = 1)
private val processor = RoomDirectorySearchProcessor(items, coroutineContext, RoomDescriptionMapper())
init {
sessionCoroutineScope.launch(sessionDispatcher) {
inner
.resultsFlow()
.onEach { updates ->
processor.postUpdates(updates)
}
.launchIn(this)
}
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 try {
return execute {
inner.search(filter = filter, batchSize = batchSize.toUInt())
Result.success(Unit)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Result.failure(e)
}
}
override suspend fun loadMore(): Result<Unit> {
return try {
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()
}
}
override suspend fun hasMoreToLoad(): Boolean {
private suspend fun hasMoreToLoad(): Boolean {
return !inner.isAtLastPage()
}
@OptIn(FlowPreview::class)
override val items: Flow<List<RoomDescription>> = _items.debounce(200.milliseconds)
override val state: Flow<RoomDirectoryList.State> =
combine(hasMoreToLoad, items) { hasMoreToLoad, items ->
RoomDirectoryList.State(
hasMoreToLoad = hasMoreToLoad,
items = items
)
}
.flowOn(coroutineContext)
}

6
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryService.kt

@ -24,12 +24,10 @@ import org.matrix.rustcomponents.sdk.Client @@ -24,12 +24,10 @@ import org.matrix.rustcomponents.sdk.Client
class RustRoomDirectoryService(
private val client: Client,
private val sessionCoroutineScope: CoroutineScope,
private val sessionDispatcher: CoroutineDispatcher,
) : RoomDirectoryService {
override fun createRoomDirectoryList(): RoomDirectoryList {
return RustRoomDirectoryList(client.roomDirectorySearch(), sessionCoroutineScope, sessionDispatcher)
override fun createRoomDirectoryList(scope: CoroutineScope): RoomDirectoryList {
return RustRoomDirectoryList(client.roomDirectorySearch(), scope, sessionDispatcher)
}
}

3
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/FakeRoomDirectoryService.kt

@ -18,9 +18,10 @@ package io.element.android.libraries.matrix.test.roomdirectory @@ -18,9 +18,10 @@ package io.element.android.libraries.matrix.test.roomdirectory
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
import kotlinx.coroutines.CoroutineScope
class FakeRoomDirectoryService : RoomDirectoryService {
override fun createRoomDirectoryList(): RoomDirectoryList {
override fun createRoomDirectoryList(scope: CoroutineScope): RoomDirectoryList {
TODO("Not yet implemented")
}
}

Loading…
Cancel
Save